diff --git a/CHANGELOG.md b/CHANGELOG.md index 270a877970..8e793b1bb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Schema: generic `@decorator` syntax in the schema DSL. This change is machinery only — no decorator is defined yet, and the shipped registry is empty. + ### Changed - Schema compilation: reduce memory usage when caveats are involved (https://github.com/authzed/spicedb/pull/3266) +- Schema: `use` flags are now validated against the deployment's allowed set at compile time. A bare `use import` (or `use expiration` where expiration is disabled) submitted to `WriteSchema` now errors where it previously compiled, because `WriteSchema` applies `DisallowImportFlag()` unconditionally. +- `generator.GenerateCaveatSource` changed signature from `(string, bool, error)` to `(string, []string, bool, error)`; it previously discarded the `use` flags a caveat requires. This is a breaking change for downstream Go consumers. ### Fixed - Postgres: read replicas no longer intermittently return `object definition not found` under load. The strict read-replica guard now verifies that the replica's snapshot has caught up to the revision being read (snapshot domination) instead of checking a single transaction id, and raises from within the read itself rather than from a trailing assertion, so a replica that catches up mid-query can no longer let an incomplete read through. In both cases the read correctly falls back to the primary. (https://github.com/authzed/spicedb/pull/3243) diff --git a/internal/datastore/proxy/schemacaching/types.go b/internal/datastore/proxy/schemacaching/types.go index 96faf66f67..8ef9588086 100644 --- a/internal/datastore/proxy/schemacaching/types.go +++ b/internal/datastore/proxy/schemacaching/types.go @@ -7,8 +7,14 @@ import "github.com/authzed/spicedb/pkg/schemadsl/compiler" // on-wire size, as returned by SizeVT. This was determined by testing // all existing definitions found in consistency tests and is // enforced via the estimatedsize_test. +// +// NOTE: the namespace multiplier was raised from 10 to 12 when decorators were +// added. A nil `Decorators` slice costs 24 bytes in memory on NamespaceDefinition, +// Relation and AllowedRelation, but contributes nothing to SizeVT, so the in-memory +// cost of a definition grew while its on-wire size did not. Any future field added +// to those messages has the same effect and may require raising this again. const ( - namespaceDefinitionSizeVTMultiplier = 10 + namespaceDefinitionSizeVTMultiplier = 12 namespaceDefinitionMinimumSize = 150 caveatDefinitionSizeVTMultiplier = 10 diff --git a/internal/services/v1/expreflection.go b/internal/services/v1/expreflection.go index db9b5cddc0..c5d4797502 100644 --- a/internal/services/v1/expreflection.go +++ b/internal/services/v1/expreflection.go @@ -429,6 +429,11 @@ func expConvertDiff( case nsdiff.NamespaceRemoved: return nil, spiceerrors.MustBugf("should be handled above") + case nsdiff.NamespaceDecoratorsChanged, nsdiff.RelationDecoratorsChanged: + // TODO: surface a decorator diff message once a decorator actually + // ships; there is no reflection API representation for one yet. + continue + default: return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) } @@ -513,6 +518,11 @@ func expConvertDiff( case caveatdiff.CaveatRemoved: return nil, spiceerrors.MustBugf("should be handled above") + case caveatdiff.CaveatDecoratorsChanged: + // TODO: surface a decorator diff message once a decorator actually + // ships; there is no reflection API representation for one yet. + continue + default: return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) } diff --git a/internal/services/v1/expreflection_test.go b/internal/services/v1/expreflection_test.go index 3a0ef698ca..3ba9d022f4 100644 --- a/internal/services/v1/expreflection_test.go +++ b/internal/services/v1/expreflection_test.go @@ -19,6 +19,7 @@ import ( "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/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/testutil" ) @@ -593,6 +594,75 @@ func TestExpConvertDiff(t *testing.T) { } } +// TestExpConvertDiffDecoratorOnlyChanges ensures that a schema change consisting solely of +// decorator changes (on a namespace, a relation and a caveat) is handled by expConvertDiff +// without hitting the `default` arm's spiceerrors.MustBugf, which panics in test binaries. +// The reflection API has no representation for a decorator diff yet, so the expected +// result is simply an empty diff list, not a panic. +func TestExpConvertDiffDecoratorOnlyChanges(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t, testutil.GoLeakIgnores()...) + }) + + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `use testdecorators + + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition resource { + relation viewer: user + }`, + }, compiler.AllowUnprefixedObjectType(), compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `use testdecorators + + @testcaveat + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + @testdef + definition user {} + + definition resource { + @testrel + relation viewer: user + }`, + }, compiler.AllowUnprefixedObjectType(), compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + es := diff.NewDiffableSchemaFromCompiledSchema(existingSchema) + cs := diff.NewDiffableSchemaFromCompiledSchema(comparisonSchema) + + schemaDiff, err := diff.DiffSchemas(es, cs, caveattypes.Default.TypeSet) + require.NoError(t, err) + + dl, err := dsfortesting.DataLayerForTesting(t, 100, 1*time.Second, 100*time.Minute) + require.NoError(t, err) + + ctx := t.Context() + ctx = datalayer.ContextWithDataLayer(ctx, dl) + + resp, err := expConvertDiff( + ctx, + schemaDiff, + &es, + &cs, + revisionparsing.MustParseRevisionForTest("1"), + caveattypes.Default.TypeSet, + ) + require.NoError(t, err) + require.Empty(t, resp.Diffs) +} + type expFilterCheck func(sf *expSchemaFilters) bool func TestExpSchemaFiltering(t *testing.T) { diff --git a/internal/services/v1/reflectionapi.go b/internal/services/v1/reflectionapi.go index 67d74316ec..b8eb0dc9e4 100644 --- a/internal/services/v1/reflectionapi.go +++ b/internal/services/v1/reflectionapi.go @@ -429,6 +429,11 @@ func convertDiff( case nsdiff.NamespaceRemoved: return nil, spiceerrors.MustBugf("should be handled above") + case nsdiff.NamespaceDecoratorsChanged, nsdiff.RelationDecoratorsChanged: + // TODO: surface a decorator diff message once a decorator actually + // ships; there is no reflection API representation for one yet. + continue + default: return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) } @@ -513,6 +518,11 @@ func convertDiff( case caveatdiff.CaveatRemoved: return nil, spiceerrors.MustBugf("should be handled above") + case caveatdiff.CaveatDecoratorsChanged: + // TODO: surface a decorator diff message once a decorator actually + // ships; there is no reflection API representation for one yet. + continue + default: return nil, spiceerrors.MustBugf("unexpected delta type %v", delta.Type) } diff --git a/internal/services/v1/reflectionapi_test.go b/internal/services/v1/reflectionapi_test.go index c3dadbc0bb..ced544280d 100644 --- a/internal/services/v1/reflectionapi_test.go +++ b/internal/services/v1/reflectionapi_test.go @@ -18,6 +18,7 @@ import ( "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/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/testutil" ) @@ -591,6 +592,71 @@ func TestConvertDiff(t *testing.T) { } } +// TestConvertDiffDecoratorOnlyChanges ensures that a schema change consisting solely of +// decorator changes (on a namespace, a relation and a caveat) is handled by convertDiff +// without hitting the `default` arm's spiceerrors.MustBugf, which panics in test binaries. +// The reflection API has no representation for a decorator diff yet, so the expected +// result is simply an empty diff list, not a panic. +func TestConvertDiffDecoratorOnlyChanges(t *testing.T) { + existingSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `use testdecorators + + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition resource { + relation viewer: user + }`, + }, compiler.AllowUnprefixedObjectType(), compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + comparisonSchema, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: `use testdecorators + + @testcaveat + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + @testdef + definition user {} + + definition resource { + @testrel + relation viewer: user + }`, + }, compiler.AllowUnprefixedObjectType(), compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + es := diff.NewDiffableSchemaFromCompiledSchema(existingSchema) + cs := diff.NewDiffableSchemaFromCompiledSchema(comparisonSchema) + + schemaDiff, err := diff.DiffSchemas(es, cs, caveattypes.Default.TypeSet) + require.NoError(t, err) + + dl, err := dsfortesting.DataLayerForTesting(t, 100, 1*time.Second, 100*time.Minute) + require.NoError(t, err) + + ctx := t.Context() + ctx = datalayer.ContextWithDataLayer(ctx, dl) + + resp, err := convertDiff( + ctx, + schemaDiff, + &es, + &cs, + revisionparsing.MustParseRevisionForTest("1"), + caveattypes.Default.TypeSet, + ) + require.NoError(t, err) + require.Empty(t, resp.Diffs) +} + type filterCheck func(sf *schemaFilters) bool func TestSchemaFiltering(t *testing.T) { diff --git a/pkg/diff/caveats/diff.go b/pkg/diff/caveats/diff.go index 50beb71636..8f905d2507 100644 --- a/pkg/diff/caveats/diff.go +++ b/pkg/diff/caveats/diff.go @@ -5,6 +5,9 @@ import ( "maps" "slices" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" + caveattypes "github.com/authzed/spicedb/pkg/caveats/types" "github.com/authzed/spicedb/pkg/genutil/mapz" nspkg "github.com/authzed/spicedb/pkg/namespace" @@ -35,6 +38,9 @@ const ( // CaveatExpressionChanged indicates that the expression of the caveat has changed. CaveatExpressionChanged DeltaType = "expression-has-changed" + + // CaveatDecoratorsChanged indicates that the decorators on the caveat changed. + CaveatDecoratorsChanged DeltaType = "caveat-decorators-changed" ) // Diff holds the diff between two caveats. @@ -107,6 +113,10 @@ func DiffCaveats(existing *core.CaveatDefinition, updated *core.CaveatDefinition }) } + if areDifferentDecorators(existing.GetDecorators(), updated.GetDecorators()) { + deltas = append(deltas, Delta{Type: CaveatDecoratorsChanged}) + } + existingParameterNames := mapz.NewSet(slices.Collect(maps.Keys(existing.ParameterTypes))...) updatedParameterNames := mapz.NewSet(slices.Collect(maps.Keys(updated.ParameterTypes))...) @@ -161,3 +171,10 @@ func DiffCaveats(existing *core.CaveatDefinition, updated *core.CaveatDefinition deltas: deltas, }, nil } + +// areDifferentDecorators returns whether the two sets of decorators differ. As with +// namespace and relation decorators, comparison is order-sensitive: decorators are stored +// in source order, and a reordering is a meaningful source-level change. +func areDifferentDecorators(existing []*core.Decorator, updated []*core.Decorator) bool { + return cmp.Diff(existing, updated, protocmp.Transform()) != "" +} diff --git a/pkg/diff/caveats/diff_test.go b/pkg/diff/caveats/diff_test.go index f8f273a201..61a176a607 100644 --- a/pkg/diff/caveats/diff_test.go +++ b/pkg/diff/caveats/diff_test.go @@ -277,3 +277,41 @@ func TestCaveatDiff(t *testing.T) { }) } } + +func TestDiffCaveatDecorators(t *testing.T) { + t.Parallel() + + withDecorator := func(name string) *core.Decorator { + return &core.Decorator{Name: name, RequiredFlag: "testdecorators"} + } + + newCaveat := func(decorators ...*core.Decorator) *core.CaveatDefinition { + cd := ns.MustCaveatDefinition( + caveats.MustEnvForVariablesWithDefaultTypeSet(map[string]caveattypes.VariableType{ + "someparam": caveattypes.Default.IntType, + }), + "somecaveat", + "true", + ) + cd.Decorators = decorators + return cd + } + + t.Run("caveat decorator added", func(t *testing.T) { + t.Parallel() + diff, err := DiffCaveats(newCaveat(), newCaveat(withDecorator("testcaveat")), caveattypes.Default.TypeSet) + require.NoError(t, err) + require.Equal(t, []Delta{{Type: CaveatDecoratorsChanged}}, diff.Deltas()) + }) + + t.Run("no decorator change produces no delta", func(t *testing.T) { + t.Parallel() + diff, err := DiffCaveats( + newCaveat(withDecorator("testcaveat")), + newCaveat(withDecorator("testcaveat")), + caveattypes.Default.TypeSet, + ) + require.NoError(t, err) + require.Empty(t, diff.Deltas()) + }) +} diff --git a/pkg/diff/namespace/diff.go b/pkg/diff/namespace/diff.go index 09601f3fac..56a5bb46e5 100644 --- a/pkg/diff/namespace/diff.go +++ b/pkg/diff/namespace/diff.go @@ -2,6 +2,8 @@ package namespace import ( "slices" + "strconv" + "strings" "github.com/google/go-cmp/cmp" "google.golang.org/protobuf/testing/protocmp" @@ -61,6 +63,13 @@ const ( // ChangedRelationComment indicates that the comment of the relation has changed in some way. ChangedRelationComment DeltaType = "changed-relation-comment" + + // NamespaceDecoratorsChanged indicates that the decorators on the namespace changed. + NamespaceDecoratorsChanged DeltaType = "namespace-decorators-changed" + + // RelationDecoratorsChanged indicates that the decorators on the relation or + // permission changed. + RelationDecoratorsChanged DeltaType = "relation-decorators-changed" ) // Diff holds the diff between two namespaces. @@ -130,6 +139,10 @@ func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceD }) } + if areDifferentDecorators(existing.GetDecorators(), updated.GetDecorators()) { + deltas = append(deltas, Delta{Type: NamespaceDecoratorsChanged}) + } + // Collect up relations and check. existingRels := map[string]*core.Relation{} existingRelNames := mapz.NewSet[string]() @@ -226,6 +239,10 @@ func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceD RelationName: shared, }) } + + if areDifferentDecorators(existingPerm.GetDecorators(), updatedPerm.GetDecorators()) { + deltas = append(deltas, Delta{Type: RelationDecoratorsChanged, RelationName: shared}) + } return nil }) @@ -251,6 +268,10 @@ func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceD }) } + if areDifferentDecorators(existingRel.GetDecorators(), updatedRel.GetDecorators()) { + deltas = append(deltas, Delta{Type: RelationDecoratorsChanged, RelationName: shared}) + } + // Compare type information. existingTypeInfo := existingRel.TypeInformation if existingTypeInfo == nil { @@ -267,13 +288,13 @@ func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceD allowedRelsBySource := map[string]*core.AllowedRelation{} for _, existingAllowed := range existingTypeInfo.AllowedDirectRelations { - source := schema.SourceForAllowedRelation(existingAllowed) + source := sourceForAllowedRelationWithDecorators(existingAllowed) allowedRelsBySource[source] = existingAllowed existingAllowedRels.Add(source) } for _, updatedAllowed := range updatedTypeInfo.AllowedDirectRelations { - source := schema.SourceForAllowedRelation(updatedAllowed) + source := sourceForAllowedRelationWithDecorators(updatedAllowed) allowedRelsBySource[source] = updatedAllowed updatedAllowedRels.Add(source) } @@ -320,3 +341,64 @@ func areDifferentExpressions(existing *core.UsersetRewrite, updated *core.Userse ) return delta != "" } + +// areDifferentDecorators returns whether the two sets of decorators differ. Decorators are +// compared in source order: `@a @b` and `@b @a` are considered different, because decorator +// order is meaningful source structure that a user could deliberately change (and, for +// decorators whose semantics depend on relative ordering, changing the order changes +// behavior). Within a single decorator, parameter order is not a source of false positives +// here, since `decorators.Validate` always emits parameters in the registry's canonical +// order, so two semantically identical decorators are always ordered identically. +func areDifferentDecorators(existing []*core.Decorator, updated []*core.Decorator) bool { + return cmp.Diff(existing, updated, protocmp.Transform()) != "" +} + +// sourceForAllowedRelationWithDecorators returns a deterministic key for an allowed +// relation that additionally folds in its decorators, for use solely as this package's +// set-membership key when diffing the allowed types of a relation. +// +// schema.SourceForAllowedRelation itself must stay decorator-free: it is also used by +// schema.Definition.HasAllowedRelation to validate a relationship write's subject type, +// compared against a decorator-free AllowedRelation synthesized fresh from the write +// (relationship writes have no decorator syntax at all), and by diagnostic/display code +// (internal/relationships/errors.go, internal/services/shared/schema.go). None of those +// other callers should have decorators enter their comparison or their output text. This +// diff package is the only caller that needs decorators baked into the comparison key, so +// the decorator-aware rendering is kept local to this file instead of living in the shared +// helper. +func sourceForAllowedRelationWithDecorators(allowedRelation *core.AllowedRelation) string { + var decoratorsBuilder strings.Builder + for _, decorator := range allowedRelation.GetDecorators() { + decoratorsBuilder.WriteString("@") + decoratorsBuilder.WriteString(decorator.GetName()) + for _, param := range decorator.GetParameters() { + decoratorsBuilder.WriteString("|") + decoratorsBuilder.WriteString(param.GetName()) + decoratorsBuilder.WriteString("=") + decoratorsBuilder.WriteString(decoratorParameterSource(param)) + } + decoratorsBuilder.WriteString(" ") + } + return decoratorsBuilder.String() + schema.SourceForAllowedRelation(allowedRelation) +} + +// decoratorParameterSource returns a deterministic, total string representation of a +// decorator parameter's value, for use as part of the diff key above. It is deliberately +// not DSL-valid syntax (it is never parsed back) and deliberately does not use +// proto.Message.String(), whose output is explicitly documented as unstable across +// protobuf releases; relying on it here would make the diff key change even when the +// schema itself had not. +func decoratorParameterSource(param *core.DecoratorParameter) string { + switch value := param.GetValue().(type) { + case *core.DecoratorParameter_IntValue: + return strconv.FormatInt(value.IntValue, 10) + case *core.DecoratorParameter_StringValue: + return strconv.Quote(value.StringValue) + case *core.DecoratorParameter_BoolValue: + return strconv.FormatBool(value.BoolValue) + case *core.DecoratorParameter_EnumValue: + return value.EnumValue + default: + return "" + } +} diff --git a/pkg/diff/namespace/diff_test.go b/pkg/diff/namespace/diff_test.go index 2e8b68dbf3..43d665046f 100644 --- a/pkg/diff/namespace/diff_test.go +++ b/pkg/diff/namespace/diff_test.go @@ -582,3 +582,90 @@ func TestNamespaceDiff(t *testing.T) { }) } } + +func TestDiffDecorators(t *testing.T) { + t.Parallel() + + withDecorator := func(name string) *core.Decorator { + return &core.Decorator{Name: name, RequiredFlag: "testdecorators"} + } + + t.Run("definition decorator added", func(t *testing.T) { + t.Parallel() + diff, err := DiffNamespaces( + &core.NamespaceDefinition{Name: "document"}, + &core.NamespaceDefinition{Name: "document", Decorators: []*core.Decorator{withDecorator("testdef")}}, + ) + require.NoError(t, err) + require.Equal(t, []Delta{{Type: NamespaceDecoratorsChanged}}, diff.Deltas()) + }) + + t.Run("relation decorator changed", func(t *testing.T) { + t.Parallel() + existing := &core.NamespaceDefinition{ + Name: "document", + Relation: []*core.Relation{{Name: "viewer"}}, + } + updated := &core.NamespaceDefinition{ + Name: "document", + Relation: []*core.Relation{{Name: "viewer", Decorators: []*core.Decorator{withDecorator("testrel")}}}, + } + diff, err := DiffNamespaces(existing, updated) + require.NoError(t, err) + require.Contains(t, diff.Deltas(), Delta{Type: RelationDecoratorsChanged, RelationName: "viewer"}) + }) + + t.Run("permission decorator changed", func(t *testing.T) { + t.Parallel() + newPerm := func(decorators []*core.Decorator) *core.Relation { + perm := ns.MustRelation("view", ns.Union(ns.ComputedUserset("viewer"))) + perm.Decorators = decorators + return perm + } + + existing := &core.NamespaceDefinition{ + Name: "document", + Relation: []*core.Relation{newPerm(nil)}, + } + updated := &core.NamespaceDefinition{ + Name: "document", + Relation: []*core.Relation{newPerm([]*core.Decorator{withDecorator("testrel")})}, + } + diff, err := DiffNamespaces(existing, updated) + require.NoError(t, err) + require.Contains(t, diff.Deltas(), Delta{Type: RelationDecoratorsChanged, RelationName: "view"}) + }) + + t.Run("subject type decorator produces add and remove", func(t *testing.T) { + t.Parallel() + allowed := func(ds ...*core.Decorator) *core.Relation { + return &core.Relation{ + Name: "viewer", + TypeInformation: &core.TypeInformation{ + AllowedDirectRelations: []*core.AllowedRelation{{ + Namespace: "user", + RelationOrWildcard: &core.AllowedRelation_Relation{Relation: "..."}, + Decorators: ds, + }}, + }, + } + } + + diff, err := DiffNamespaces( + &core.NamespaceDefinition{Name: "document", Relation: []*core.Relation{allowed()}}, + &core.NamespaceDefinition{Name: "document", Relation: []*core.Relation{allowed(withDecorator("testsub"))}}, + ) + require.NoError(t, err) + require.Len(t, diff.Deltas(), 2) + }) + + t.Run("no decorator change produces no delta", func(t *testing.T) { + t.Parallel() + ns := func() *core.NamespaceDefinition { + return &core.NamespaceDefinition{Name: "document", Decorators: []*core.Decorator{withDecorator("testdef")}} + } + diff, err := DiffNamespaces(ns(), ns()) + require.NoError(t, err) + require.Empty(t, diff.Deltas()) + }) +} diff --git a/pkg/proto/core/v1/core.pb.go b/pkg/proto/core/v1/core.pb.go index 4478de0487..88dbdadf71 100644 --- a/pkg/proto/core/v1/core.pb.go +++ b/pkg/proto/core/v1/core.pb.go @@ -189,7 +189,7 @@ func (x ReachabilityEntrypoint_ReachabilityEntrypointKind) Number() protoreflect // Deprecated: Use ReachabilityEntrypoint_ReachabilityEntrypointKind.Descriptor instead. func (ReachabilityEntrypoint_ReachabilityEntrypointKind) EnumDescriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{18, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{20, 0} } type ReachabilityEntrypoint_EntrypointResultStatus int32 @@ -242,7 +242,7 @@ func (x ReachabilityEntrypoint_EntrypointResultStatus) Number() protoreflect.Enu // Deprecated: Use ReachabilityEntrypoint_EntrypointResultStatus.Descriptor instead. func (ReachabilityEntrypoint_EntrypointResultStatus) EnumDescriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{18, 1} + return file_core_v1_core_proto_rawDescGZIP(), []int{20, 1} } type FunctionedTupleToUserset_Function int32 @@ -291,7 +291,7 @@ func (x FunctionedTupleToUserset_Function) Number() protoreflect.EnumNumber { // Deprecated: Use FunctionedTupleToUserset_Function.Descriptor instead. func (FunctionedTupleToUserset_Function) EnumDescriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{28, 0} } type ComputedUserset_Object int32 @@ -337,7 +337,7 @@ func (x ComputedUserset_Object) Number() protoreflect.EnumNumber { // Deprecated: Use ComputedUserset_Object.Descriptor instead. func (ComputedUserset_Object) EnumDescriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{27, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{29, 0} } type CaveatOperation_Operation int32 @@ -389,7 +389,7 @@ func (x CaveatOperation_Operation) Number() protoreflect.EnumNumber { // Deprecated: Use CaveatOperation_Operation.Descriptor instead. func (CaveatOperation_Operation) EnumDescriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{30, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{32, 0} } type RelationTuple struct { @@ -605,8 +605,10 @@ type CaveatDefinition struct { Metadata *Metadata `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"` // * source_position contains the position of the caveat in the source schema, if any SourcePosition *SourcePosition `protobuf:"bytes,5,opt,name=source_position,json=sourcePosition,proto3" json:"source_position,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // * decorators are the decorators applied to this caveat + Decorators []*Decorator `protobuf:"bytes,6,rep,name=decorators,proto3" json:"decorators,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CaveatDefinition) Reset() { @@ -674,6 +676,13 @@ func (x *CaveatDefinition) GetSourcePosition() *SourcePosition { return nil } +func (x *CaveatDefinition) GetDecorators() []*Decorator { + if x != nil { + return x.Decorators + } + return nil +} + type CaveatTypeReference struct { state protoimpl.MessageState `protogen:"open.v1"` TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` @@ -1232,6 +1241,198 @@ func (x *Metadata) GetMetadataMessage() []*anypb.Any { return nil } +// * +// Decorator is a `@name(param: value)` annotation applied to a definition, +// relation, permission, caveat or subject type in a schema. +type Decorator struct { + state protoimpl.MessageState `protogen:"open.v1"` + // * name is the decorator's name, without the leading `@` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // * parameters are the decorator's arguments, in source order + Parameters []*DecoratorParameter `protobuf:"bytes,2,rep,name=parameters,proto3" json:"parameters,omitempty"` + // * + // required_flag is the `use` feature flag that enables this decorator. It is stored + // so that schema generation can re-emit the necessary `use` lines without consulting + // the decorator registry. + RequiredFlag string `protobuf:"bytes,3,opt,name=required_flag,json=requiredFlag,proto3" json:"required_flag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Decorator) Reset() { + *x = Decorator{} + mi := &file_core_v1_core_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Decorator) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Decorator) ProtoMessage() {} + +func (x *Decorator) ProtoReflect() protoreflect.Message { + mi := &file_core_v1_core_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Decorator.ProtoReflect.Descriptor instead. +func (*Decorator) Descriptor() ([]byte, []int) { + return file_core_v1_core_proto_rawDescGZIP(), []int{14} +} + +func (x *Decorator) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Decorator) GetParameters() []*DecoratorParameter { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *Decorator) GetRequiredFlag() string { + if x != nil { + return x.RequiredFlag + } + return "" +} + +// * DecoratorParameter is a single named argument to a Decorator. +type DecoratorParameter struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Types that are valid to be assigned to Value: + // + // *DecoratorParameter_IntValue + // *DecoratorParameter_StringValue + // *DecoratorParameter_BoolValue + // *DecoratorParameter_EnumValue + Value isDecoratorParameter_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DecoratorParameter) Reset() { + *x = DecoratorParameter{} + mi := &file_core_v1_core_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DecoratorParameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DecoratorParameter) ProtoMessage() {} + +func (x *DecoratorParameter) ProtoReflect() protoreflect.Message { + mi := &file_core_v1_core_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DecoratorParameter.ProtoReflect.Descriptor instead. +func (*DecoratorParameter) Descriptor() ([]byte, []int) { + return file_core_v1_core_proto_rawDescGZIP(), []int{15} +} + +func (x *DecoratorParameter) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DecoratorParameter) GetValue() isDecoratorParameter_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *DecoratorParameter) GetIntValue() int64 { + if x != nil { + if x, ok := x.Value.(*DecoratorParameter_IntValue); ok { + return x.IntValue + } + } + return 0 +} + +func (x *DecoratorParameter) GetStringValue() string { + if x != nil { + if x, ok := x.Value.(*DecoratorParameter_StringValue); ok { + return x.StringValue + } + } + return "" +} + +func (x *DecoratorParameter) GetBoolValue() bool { + if x != nil { + if x, ok := x.Value.(*DecoratorParameter_BoolValue); ok { + return x.BoolValue + } + } + return false +} + +func (x *DecoratorParameter) GetEnumValue() string { + if x != nil { + if x, ok := x.Value.(*DecoratorParameter_EnumValue); ok { + return x.EnumValue + } + } + return "" +} + +type isDecoratorParameter_Value interface { + isDecoratorParameter_Value() +} + +type DecoratorParameter_IntValue struct { + IntValue int64 `protobuf:"varint,2,opt,name=int_value,json=intValue,proto3,oneof"` +} + +type DecoratorParameter_StringValue struct { + StringValue string `protobuf:"bytes,3,opt,name=string_value,json=stringValue,proto3,oneof"` +} + +type DecoratorParameter_BoolValue struct { + BoolValue bool `protobuf:"varint,4,opt,name=bool_value,json=boolValue,proto3,oneof"` +} + +type DecoratorParameter_EnumValue struct { + EnumValue string `protobuf:"bytes,5,opt,name=enum_value,json=enumValue,proto3,oneof"` +} + +func (*DecoratorParameter_IntValue) isDecoratorParameter_Value() {} + +func (*DecoratorParameter_StringValue) isDecoratorParameter_Value() {} + +func (*DecoratorParameter_BoolValue) isDecoratorParameter_Value() {} + +func (*DecoratorParameter_EnumValue) isDecoratorParameter_Value() {} + // * // NamespaceDefinition represents a single definition of an object type type NamespaceDefinition struct { @@ -1244,13 +1445,15 @@ type NamespaceDefinition struct { Metadata *Metadata `protobuf:"bytes,3,opt,name=metadata,proto3" json:"metadata,omitempty"` // * source_position contains the position of the namespace in the source schema, if any SourcePosition *SourcePosition `protobuf:"bytes,4,opt,name=source_position,json=sourcePosition,proto3" json:"source_position,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // * decorators are the decorators applied to this definition + Decorators []*Decorator `protobuf:"bytes,5,rep,name=decorators,proto3" json:"decorators,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NamespaceDefinition) Reset() { *x = NamespaceDefinition{} - mi := &file_core_v1_core_proto_msgTypes[14] + mi := &file_core_v1_core_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1262,7 +1465,7 @@ func (x *NamespaceDefinition) String() string { func (*NamespaceDefinition) ProtoMessage() {} func (x *NamespaceDefinition) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[14] + mi := &file_core_v1_core_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1275,7 +1478,7 @@ func (x *NamespaceDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use NamespaceDefinition.ProtoReflect.Descriptor instead. func (*NamespaceDefinition) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{14} + return file_core_v1_core_proto_rawDescGZIP(), []int{16} } func (x *NamespaceDefinition) GetName() string { @@ -1306,6 +1509,13 @@ func (x *NamespaceDefinition) GetSourcePosition() *SourcePosition { return nil } +func (x *NamespaceDefinition) GetDecorators() []*Decorator { + if x != nil { + return x.Decorators + } + return nil +} + // * // Relation represents the definition of a relation or permission under a namespace. type Relation struct { @@ -1324,13 +1534,15 @@ type Relation struct { SourcePosition *SourcePosition `protobuf:"bytes,5,opt,name=source_position,json=sourcePosition,proto3" json:"source_position,omitempty"` AliasingRelation string `protobuf:"bytes,6,opt,name=aliasing_relation,json=aliasingRelation,proto3" json:"aliasing_relation,omitempty"` CanonicalCacheKey string `protobuf:"bytes,7,opt,name=canonical_cache_key,json=canonicalCacheKey,proto3" json:"canonical_cache_key,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // * decorators are the decorators applied to this relation or permission + Decorators []*Decorator `protobuf:"bytes,8,rep,name=decorators,proto3" json:"decorators,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Relation) Reset() { *x = Relation{} - mi := &file_core_v1_core_proto_msgTypes[15] + mi := &file_core_v1_core_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1342,7 +1554,7 @@ func (x *Relation) String() string { func (*Relation) ProtoMessage() {} func (x *Relation) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[15] + mi := &file_core_v1_core_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1355,7 +1567,7 @@ func (x *Relation) ProtoReflect() protoreflect.Message { // Deprecated: Use Relation.ProtoReflect.Descriptor instead. func (*Relation) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{15} + return file_core_v1_core_proto_rawDescGZIP(), []int{17} } func (x *Relation) GetName() string { @@ -1407,6 +1619,13 @@ func (x *Relation) GetCanonicalCacheKey() string { return "" } +func (x *Relation) GetDecorators() []*Decorator { + if x != nil { + return x.Decorators + } + return nil +} + // * // ReachabilityGraph is a serialized form of a reachability graph, representing how a relation can // be reached from one or more subject types. @@ -1455,7 +1674,7 @@ type ReachabilityGraph struct { func (x *ReachabilityGraph) Reset() { *x = ReachabilityGraph{} - mi := &file_core_v1_core_proto_msgTypes[16] + mi := &file_core_v1_core_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1467,7 +1686,7 @@ func (x *ReachabilityGraph) String() string { func (*ReachabilityGraph) ProtoMessage() {} func (x *ReachabilityGraph) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[16] + mi := &file_core_v1_core_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1480,7 +1699,7 @@ func (x *ReachabilityGraph) ProtoReflect() protoreflect.Message { // Deprecated: Use ReachabilityGraph.ProtoReflect.Descriptor instead. func (*ReachabilityGraph) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{16} + return file_core_v1_core_proto_rawDescGZIP(), []int{18} } func (x *ReachabilityGraph) GetEntrypointsBySubjectType() map[string]*ReachabilityEntrypoints { @@ -1519,7 +1738,7 @@ type ReachabilityEntrypoints struct { func (x *ReachabilityEntrypoints) Reset() { *x = ReachabilityEntrypoints{} - mi := &file_core_v1_core_proto_msgTypes[17] + mi := &file_core_v1_core_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1531,7 +1750,7 @@ func (x *ReachabilityEntrypoints) String() string { func (*ReachabilityEntrypoints) ProtoMessage() {} func (x *ReachabilityEntrypoints) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[17] + mi := &file_core_v1_core_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1544,7 +1763,7 @@ func (x *ReachabilityEntrypoints) ProtoReflect() protoreflect.Message { // Deprecated: Use ReachabilityEntrypoints.ProtoReflect.Descriptor instead. func (*ReachabilityEntrypoints) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{17} + return file_core_v1_core_proto_rawDescGZIP(), []int{19} } func (x *ReachabilityEntrypoints) GetEntrypoints() []*ReachabilityEntrypoint { @@ -1597,7 +1816,7 @@ type ReachabilityEntrypoint struct { func (x *ReachabilityEntrypoint) Reset() { *x = ReachabilityEntrypoint{} - mi := &file_core_v1_core_proto_msgTypes[18] + mi := &file_core_v1_core_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1609,7 +1828,7 @@ func (x *ReachabilityEntrypoint) String() string { func (*ReachabilityEntrypoint) ProtoMessage() {} func (x *ReachabilityEntrypoint) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[18] + mi := &file_core_v1_core_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1622,7 +1841,7 @@ func (x *ReachabilityEntrypoint) ProtoReflect() protoreflect.Message { // Deprecated: Use ReachabilityEntrypoint.ProtoReflect.Descriptor instead. func (*ReachabilityEntrypoint) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{18} + return file_core_v1_core_proto_rawDescGZIP(), []int{20} } func (x *ReachabilityEntrypoint) GetKind() ReachabilityEntrypoint_ReachabilityEntrypointKind { @@ -1674,7 +1893,7 @@ type TypeInformation struct { func (x *TypeInformation) Reset() { *x = TypeInformation{} - mi := &file_core_v1_core_proto_msgTypes[19] + mi := &file_core_v1_core_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1686,7 +1905,7 @@ func (x *TypeInformation) String() string { func (*TypeInformation) ProtoMessage() {} func (x *TypeInformation) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[19] + mi := &file_core_v1_core_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1699,7 +1918,7 @@ func (x *TypeInformation) ProtoReflect() protoreflect.Message { // Deprecated: Use TypeInformation.ProtoReflect.Descriptor instead. func (*TypeInformation) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{19} + return file_core_v1_core_proto_rawDescGZIP(), []int{21} } func (x *TypeInformation) GetAllowedDirectRelations() []*AllowedRelation { @@ -1731,13 +1950,15 @@ type AllowedRelation struct { // * // required_expiration defines the required expiration on this relation. RequiredExpiration *ExpirationTrait `protobuf:"bytes,7,opt,name=required_expiration,json=requiredExpiration,proto3" json:"required_expiration,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // * decorators are the decorators applied to this subject type + Decorators []*Decorator `protobuf:"bytes,8,rep,name=decorators,proto3" json:"decorators,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AllowedRelation) Reset() { *x = AllowedRelation{} - mi := &file_core_v1_core_proto_msgTypes[20] + mi := &file_core_v1_core_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1749,7 +1970,7 @@ func (x *AllowedRelation) String() string { func (*AllowedRelation) ProtoMessage() {} func (x *AllowedRelation) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[20] + mi := &file_core_v1_core_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1762,7 +1983,7 @@ func (x *AllowedRelation) ProtoReflect() protoreflect.Message { // Deprecated: Use AllowedRelation.ProtoReflect.Descriptor instead. func (*AllowedRelation) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{20} + return file_core_v1_core_proto_rawDescGZIP(), []int{22} } func (x *AllowedRelation) GetNamespace() string { @@ -1818,6 +2039,13 @@ func (x *AllowedRelation) GetRequiredExpiration() *ExpirationTrait { return nil } +func (x *AllowedRelation) GetDecorators() []*Decorator { + if x != nil { + return x.Decorators + } + return nil +} + type isAllowedRelation_RelationOrWildcard interface { isAllowedRelation_RelationOrWildcard() } @@ -1844,7 +2072,7 @@ type ExpirationTrait struct { func (x *ExpirationTrait) Reset() { *x = ExpirationTrait{} - mi := &file_core_v1_core_proto_msgTypes[21] + mi := &file_core_v1_core_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1856,7 +2084,7 @@ func (x *ExpirationTrait) String() string { func (*ExpirationTrait) ProtoMessage() {} func (x *ExpirationTrait) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[21] + mi := &file_core_v1_core_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1869,7 +2097,7 @@ func (x *ExpirationTrait) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpirationTrait.ProtoReflect.Descriptor instead. func (*ExpirationTrait) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{21} + return file_core_v1_core_proto_rawDescGZIP(), []int{23} } // * @@ -1885,7 +2113,7 @@ type AllowedCaveat struct { func (x *AllowedCaveat) Reset() { *x = AllowedCaveat{} - mi := &file_core_v1_core_proto_msgTypes[22] + mi := &file_core_v1_core_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1897,7 +2125,7 @@ func (x *AllowedCaveat) String() string { func (*AllowedCaveat) ProtoMessage() {} func (x *AllowedCaveat) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[22] + mi := &file_core_v1_core_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1910,7 +2138,7 @@ func (x *AllowedCaveat) ProtoReflect() protoreflect.Message { // Deprecated: Use AllowedCaveat.ProtoReflect.Descriptor instead. func (*AllowedCaveat) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{22} + return file_core_v1_core_proto_rawDescGZIP(), []int{24} } func (x *AllowedCaveat) GetCaveatName() string { @@ -1935,7 +2163,7 @@ type UsersetRewrite struct { func (x *UsersetRewrite) Reset() { *x = UsersetRewrite{} - mi := &file_core_v1_core_proto_msgTypes[23] + mi := &file_core_v1_core_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1947,7 +2175,7 @@ func (x *UsersetRewrite) String() string { func (*UsersetRewrite) ProtoMessage() {} func (x *UsersetRewrite) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[23] + mi := &file_core_v1_core_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1960,7 +2188,7 @@ func (x *UsersetRewrite) ProtoReflect() protoreflect.Message { // Deprecated: Use UsersetRewrite.ProtoReflect.Descriptor instead. func (*UsersetRewrite) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{23} + return file_core_v1_core_proto_rawDescGZIP(), []int{25} } func (x *UsersetRewrite) GetRewriteOperation() isUsersetRewrite_RewriteOperation { @@ -2035,7 +2263,7 @@ type SetOperation struct { func (x *SetOperation) Reset() { *x = SetOperation{} - mi := &file_core_v1_core_proto_msgTypes[24] + mi := &file_core_v1_core_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2047,7 +2275,7 @@ func (x *SetOperation) String() string { func (*SetOperation) ProtoMessage() {} func (x *SetOperation) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[24] + mi := &file_core_v1_core_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2060,7 +2288,7 @@ func (x *SetOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOperation.ProtoReflect.Descriptor instead. func (*SetOperation) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{24} + return file_core_v1_core_proto_rawDescGZIP(), []int{26} } func (x *SetOperation) GetChild() []*SetOperation_Child { @@ -2081,7 +2309,7 @@ type TupleToUserset struct { func (x *TupleToUserset) Reset() { *x = TupleToUserset{} - mi := &file_core_v1_core_proto_msgTypes[25] + mi := &file_core_v1_core_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2093,7 +2321,7 @@ func (x *TupleToUserset) String() string { func (*TupleToUserset) ProtoMessage() {} func (x *TupleToUserset) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[25] + mi := &file_core_v1_core_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2106,7 +2334,7 @@ func (x *TupleToUserset) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleToUserset.ProtoReflect.Descriptor instead. func (*TupleToUserset) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{25} + return file_core_v1_core_proto_rawDescGZIP(), []int{27} } func (x *TupleToUserset) GetTupleset() *TupleToUserset_Tupleset { @@ -2142,7 +2370,7 @@ type FunctionedTupleToUserset struct { func (x *FunctionedTupleToUserset) Reset() { *x = FunctionedTupleToUserset{} - mi := &file_core_v1_core_proto_msgTypes[26] + mi := &file_core_v1_core_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2154,7 +2382,7 @@ func (x *FunctionedTupleToUserset) String() string { func (*FunctionedTupleToUserset) ProtoMessage() {} func (x *FunctionedTupleToUserset) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[26] + mi := &file_core_v1_core_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2167,7 +2395,7 @@ func (x *FunctionedTupleToUserset) ProtoReflect() protoreflect.Message { // Deprecated: Use FunctionedTupleToUserset.ProtoReflect.Descriptor instead. func (*FunctionedTupleToUserset) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{26} + return file_core_v1_core_proto_rawDescGZIP(), []int{28} } func (x *FunctionedTupleToUserset) GetFunction() FunctionedTupleToUserset_Function { @@ -2209,7 +2437,7 @@ type ComputedUserset struct { func (x *ComputedUserset) Reset() { *x = ComputedUserset{} - mi := &file_core_v1_core_proto_msgTypes[27] + mi := &file_core_v1_core_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2221,7 +2449,7 @@ func (x *ComputedUserset) String() string { func (*ComputedUserset) ProtoMessage() {} func (x *ComputedUserset) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[27] + mi := &file_core_v1_core_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2234,7 +2462,7 @@ func (x *ComputedUserset) ProtoReflect() protoreflect.Message { // Deprecated: Use ComputedUserset.ProtoReflect.Descriptor instead. func (*ComputedUserset) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{27} + return file_core_v1_core_proto_rawDescGZIP(), []int{29} } func (x *ComputedUserset) GetObject() ComputedUserset_Object { @@ -2268,7 +2496,7 @@ type SourcePosition struct { func (x *SourcePosition) Reset() { *x = SourcePosition{} - mi := &file_core_v1_core_proto_msgTypes[28] + mi := &file_core_v1_core_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2280,7 +2508,7 @@ func (x *SourcePosition) String() string { func (*SourcePosition) ProtoMessage() {} func (x *SourcePosition) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[28] + mi := &file_core_v1_core_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2293,7 +2521,7 @@ func (x *SourcePosition) ProtoReflect() protoreflect.Message { // Deprecated: Use SourcePosition.ProtoReflect.Descriptor instead. func (*SourcePosition) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{28} + return file_core_v1_core_proto_rawDescGZIP(), []int{30} } func (x *SourcePosition) GetZeroIndexedLineNumber() uint64 { @@ -2323,7 +2551,7 @@ type CaveatExpression struct { func (x *CaveatExpression) Reset() { *x = CaveatExpression{} - mi := &file_core_v1_core_proto_msgTypes[29] + mi := &file_core_v1_core_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2335,7 +2563,7 @@ func (x *CaveatExpression) String() string { func (*CaveatExpression) ProtoMessage() {} func (x *CaveatExpression) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[29] + mi := &file_core_v1_core_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2348,7 +2576,7 @@ func (x *CaveatExpression) ProtoReflect() protoreflect.Message { // Deprecated: Use CaveatExpression.ProtoReflect.Descriptor instead. func (*CaveatExpression) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{29} + return file_core_v1_core_proto_rawDescGZIP(), []int{31} } func (x *CaveatExpression) GetOperationOrCaveat() isCaveatExpression_OperationOrCaveat { @@ -2402,7 +2630,7 @@ type CaveatOperation struct { func (x *CaveatOperation) Reset() { *x = CaveatOperation{} - mi := &file_core_v1_core_proto_msgTypes[30] + mi := &file_core_v1_core_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2414,7 +2642,7 @@ func (x *CaveatOperation) String() string { func (*CaveatOperation) ProtoMessage() {} func (x *CaveatOperation) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[30] + mi := &file_core_v1_core_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2427,7 +2655,7 @@ func (x *CaveatOperation) ProtoReflect() protoreflect.Message { // Deprecated: Use CaveatOperation.ProtoReflect.Descriptor instead. func (*CaveatOperation) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{30} + return file_core_v1_core_proto_rawDescGZIP(), []int{32} } func (x *CaveatOperation) GetOp() CaveatOperation_Operation { @@ -2465,7 +2693,7 @@ type RelationshipFilter struct { func (x *RelationshipFilter) Reset() { *x = RelationshipFilter{} - mi := &file_core_v1_core_proto_msgTypes[31] + mi := &file_core_v1_core_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2477,7 +2705,7 @@ func (x *RelationshipFilter) String() string { func (*RelationshipFilter) ProtoMessage() {} func (x *RelationshipFilter) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[31] + mi := &file_core_v1_core_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2490,7 +2718,7 @@ func (x *RelationshipFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use RelationshipFilter.ProtoReflect.Descriptor instead. func (*RelationshipFilter) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{31} + return file_core_v1_core_proto_rawDescGZIP(), []int{33} } func (x *RelationshipFilter) GetResourceType() string { @@ -2543,7 +2771,7 @@ type SubjectFilter struct { func (x *SubjectFilter) Reset() { *x = SubjectFilter{} - mi := &file_core_v1_core_proto_msgTypes[32] + mi := &file_core_v1_core_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2555,7 +2783,7 @@ func (x *SubjectFilter) String() string { func (*SubjectFilter) ProtoMessage() {} func (x *SubjectFilter) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[32] + mi := &file_core_v1_core_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2568,7 +2796,7 @@ func (x *SubjectFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectFilter.ProtoReflect.Descriptor instead. func (*SubjectFilter) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{32} + return file_core_v1_core_proto_rawDescGZIP(), []int{34} } func (x *SubjectFilter) GetSubjectType() string { @@ -2606,7 +2834,7 @@ type StoredSchema struct { func (x *StoredSchema) Reset() { *x = StoredSchema{} - mi := &file_core_v1_core_proto_msgTypes[33] + mi := &file_core_v1_core_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2618,7 +2846,7 @@ func (x *StoredSchema) String() string { func (*StoredSchema) ProtoMessage() {} func (x *StoredSchema) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[33] + mi := &file_core_v1_core_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2631,7 +2859,7 @@ func (x *StoredSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredSchema.ProtoReflect.Descriptor instead. func (*StoredSchema) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{33} + return file_core_v1_core_proto_rawDescGZIP(), []int{35} } func (x *StoredSchema) GetVersion() uint32 { @@ -2675,7 +2903,7 @@ type AllowedRelation_PublicWildcard struct { func (x *AllowedRelation_PublicWildcard) Reset() { *x = AllowedRelation_PublicWildcard{} - mi := &file_core_v1_core_proto_msgTypes[37] + mi := &file_core_v1_core_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2687,7 +2915,7 @@ func (x *AllowedRelation_PublicWildcard) String() string { func (*AllowedRelation_PublicWildcard) ProtoMessage() {} func (x *AllowedRelation_PublicWildcard) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[37] + mi := &file_core_v1_core_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2700,7 +2928,7 @@ func (x *AllowedRelation_PublicWildcard) ProtoReflect() protoreflect.Message { // Deprecated: Use AllowedRelation_PublicWildcard.ProtoReflect.Descriptor instead. func (*AllowedRelation_PublicWildcard) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{20, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{22, 0} } type SetOperation_Child struct { @@ -2728,7 +2956,7 @@ type SetOperation_Child struct { func (x *SetOperation_Child) Reset() { *x = SetOperation_Child{} - mi := &file_core_v1_core_proto_msgTypes[38] + mi := &file_core_v1_core_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2740,7 +2968,7 @@ func (x *SetOperation_Child) String() string { func (*SetOperation_Child) ProtoMessage() {} func (x *SetOperation_Child) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[38] + mi := &file_core_v1_core_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2753,7 +2981,7 @@ func (x *SetOperation_Child) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOperation_Child.ProtoReflect.Descriptor instead. func (*SetOperation_Child) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{24, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0} } func (x *SetOperation_Child) GetChildType() isSetOperation_Child_ChildType { @@ -2896,7 +3124,7 @@ type SetOperation_Child_This struct { func (x *SetOperation_Child_This) Reset() { *x = SetOperation_Child_This{} - mi := &file_core_v1_core_proto_msgTypes[39] + mi := &file_core_v1_core_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2908,7 +3136,7 @@ func (x *SetOperation_Child_This) String() string { func (*SetOperation_Child_This) ProtoMessage() {} func (x *SetOperation_Child_This) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[39] + mi := &file_core_v1_core_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2921,7 +3149,7 @@ func (x *SetOperation_Child_This) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOperation_Child_This.ProtoReflect.Descriptor instead. func (*SetOperation_Child_This) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{24, 0, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0, 0} } type SetOperation_Child_Nil struct { @@ -2932,7 +3160,7 @@ type SetOperation_Child_Nil struct { func (x *SetOperation_Child_Nil) Reset() { *x = SetOperation_Child_Nil{} - mi := &file_core_v1_core_proto_msgTypes[40] + mi := &file_core_v1_core_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2944,7 +3172,7 @@ func (x *SetOperation_Child_Nil) String() string { func (*SetOperation_Child_Nil) ProtoMessage() {} func (x *SetOperation_Child_Nil) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[40] + mi := &file_core_v1_core_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2957,7 +3185,7 @@ func (x *SetOperation_Child_Nil) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOperation_Child_Nil.ProtoReflect.Descriptor instead. func (*SetOperation_Child_Nil) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{24, 0, 1} + return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0, 1} } // `self` refers to the resource-as-a-subject in a permission computation. @@ -2969,7 +3197,7 @@ type SetOperation_Child_Self struct { func (x *SetOperation_Child_Self) Reset() { *x = SetOperation_Child_Self{} - mi := &file_core_v1_core_proto_msgTypes[41] + mi := &file_core_v1_core_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2981,7 +3209,7 @@ func (x *SetOperation_Child_Self) String() string { func (*SetOperation_Child_Self) ProtoMessage() {} func (x *SetOperation_Child_Self) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[41] + mi := &file_core_v1_core_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2994,7 +3222,7 @@ func (x *SetOperation_Child_Self) ProtoReflect() protoreflect.Message { // Deprecated: Use SetOperation_Child_Self.ProtoReflect.Descriptor instead. func (*SetOperation_Child_Self) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{24, 0, 2} + return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0, 2} } type TupleToUserset_Tupleset struct { @@ -3006,7 +3234,7 @@ type TupleToUserset_Tupleset struct { func (x *TupleToUserset_Tupleset) Reset() { *x = TupleToUserset_Tupleset{} - mi := &file_core_v1_core_proto_msgTypes[42] + mi := &file_core_v1_core_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3018,7 +3246,7 @@ func (x *TupleToUserset_Tupleset) String() string { func (*TupleToUserset_Tupleset) ProtoMessage() {} func (x *TupleToUserset_Tupleset) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[42] + mi := &file_core_v1_core_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3031,7 +3259,7 @@ func (x *TupleToUserset_Tupleset) ProtoReflect() protoreflect.Message { // Deprecated: Use TupleToUserset_Tupleset.ProtoReflect.Descriptor instead. func (*TupleToUserset_Tupleset) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{25, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{27, 0} } func (x *TupleToUserset_Tupleset) GetRelation() string { @@ -3050,7 +3278,7 @@ type FunctionedTupleToUserset_Tupleset struct { func (x *FunctionedTupleToUserset_Tupleset) Reset() { *x = FunctionedTupleToUserset_Tupleset{} - mi := &file_core_v1_core_proto_msgTypes[43] + mi := &file_core_v1_core_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3062,7 +3290,7 @@ func (x *FunctionedTupleToUserset_Tupleset) String() string { func (*FunctionedTupleToUserset_Tupleset) ProtoMessage() {} func (x *FunctionedTupleToUserset_Tupleset) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[43] + mi := &file_core_v1_core_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3075,7 +3303,7 @@ func (x *FunctionedTupleToUserset_Tupleset) ProtoReflect() protoreflect.Message // Deprecated: Use FunctionedTupleToUserset_Tupleset.ProtoReflect.Descriptor instead. func (*FunctionedTupleToUserset_Tupleset) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{26, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{28, 0} } func (x *FunctionedTupleToUserset_Tupleset) GetRelation() string { @@ -3094,7 +3322,7 @@ type SubjectFilter_RelationFilter struct { func (x *SubjectFilter_RelationFilter) Reset() { *x = SubjectFilter_RelationFilter{} - mi := &file_core_v1_core_proto_msgTypes[44] + mi := &file_core_v1_core_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3106,7 +3334,7 @@ func (x *SubjectFilter_RelationFilter) String() string { func (*SubjectFilter_RelationFilter) ProtoMessage() {} func (x *SubjectFilter_RelationFilter) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[44] + mi := &file_core_v1_core_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3119,7 +3347,7 @@ func (x *SubjectFilter_RelationFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use SubjectFilter_RelationFilter.ProtoReflect.Descriptor instead. func (*SubjectFilter_RelationFilter) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{32, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{34, 0} } func (x *SubjectFilter_RelationFilter) GetRelation() string { @@ -3147,7 +3375,7 @@ type StoredSchema_V1StoredSchema struct { func (x *StoredSchema_V1StoredSchema) Reset() { *x = StoredSchema_V1StoredSchema{} - mi := &file_core_v1_core_proto_msgTypes[45] + mi := &file_core_v1_core_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3159,7 +3387,7 @@ func (x *StoredSchema_V1StoredSchema) String() string { func (*StoredSchema_V1StoredSchema) ProtoMessage() {} func (x *StoredSchema_V1StoredSchema) ProtoReflect() protoreflect.Message { - mi := &file_core_v1_core_proto_msgTypes[45] + mi := &file_core_v1_core_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3172,7 +3400,7 @@ func (x *StoredSchema_V1StoredSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use StoredSchema_V1StoredSchema.ProtoReflect.Descriptor instead. func (*StoredSchema_V1StoredSchema) Descriptor() ([]byte, []int) { - return file_core_v1_core_proto_rawDescGZIP(), []int{33, 0} + return file_core_v1_core_proto_rawDescGZIP(), []int{35, 0} } func (x *StoredSchema_V1StoredSchema) GetSchemaText() string { @@ -3221,7 +3449,7 @@ const file_core_v1_core_proto_rawDesc = "" + "\x14ContextualizedCaveat\x12V\n" + "\vcaveat_name\x18\x01 \x01(\tB5\xbaH2r0(\x80\x012+^(([a-zA-Z0-9_][a-zA-Z0-9/_|-]{0,127})|\\*)$R\n" + "caveatName\x129\n" + - "\acontext\x18\x02 \x01(\v2\x17.google.protobuf.StructB\x06\xbaH\x03\xc8\x01\x00R\acontext\"\xd4\x03\n" + + "\acontext\x18\x02 \x01(\v2\x17.google.protobuf.StructB\x06\xbaH\x03\xc8\x01\x00R\acontext\"\x88\x04\n" + "\x10CaveatDefinition\x12I\n" + "\x04name\x18\x01 \x01(\tB5\xbaH2r0(\x80\x012+^(([a-zA-Z0-9_][a-zA-Z0-9/_|-]{0,127})|\\*)$R\x04name\x12?\n" + "\x15serialized_expression\x18\x02 \x01(\fB\n" + @@ -3229,7 +3457,10 @@ const file_core_v1_core_proto_rawDesc = "" + "\x0fparameter_types\x18\x03 \x03(\v2-.core.v1.CaveatDefinition.ParameterTypesEntryB\n" + "\xbaH\a\x9a\x01\x04\b\x01\x10\x14R\x0eparameterTypes\x12-\n" + "\bmetadata\x18\x04 \x01(\v2\x11.core.v1.MetadataR\bmetadata\x12@\n" + - "\x0fsource_position\x18\x05 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x1a_\n" + + "\x0fsource_position\x18\x05 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x122\n" + + "\n" + + "decorators\x18\x06 \x03(\v2\x12.core.v1.DecoratorR\n" + + "decorators\x1a_\n" + "\x13ParameterTypesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x122\n" + "\x05value\x18\x02 \x01(\v2\x1c.core.v1.CaveatTypeReferenceR\x05value:\x028\x01\"}\n" + @@ -3278,12 +3509,30 @@ const file_core_v1_core_proto_rawDesc = "" + "\x0eDirectSubjects\x122\n" + "\bsubjects\x18\x01 \x03(\v2\x16.core.v1.DirectSubjectR\bsubjects\"\xb7\x01\n" + "\bMetadata\x12\xaa\x01\n" + - "\x10metadata_message\x18\x01 \x03(\v2\x14.google.protobuf.AnyBi\xbaHf\xc8\x01\x01\x92\x01`\b\x01\"\\\xc8\x01\x01\xa2\x01V\x12&type.googleapis.com/impl.v1.DocComment\x12,type.googleapis.com/impl.v1.RelationMetadataR\x0fmetadataMessage\"\x93\x02\n" + + "\x10metadata_message\x18\x01 \x03(\v2\x14.google.protobuf.AnyBi\xbaHf\xc8\x01\x01\x92\x01`\b\x01\"\\\xc8\x01\x01\xa2\x01V\x12&type.googleapis.com/impl.v1.DocComment\x12,type.googleapis.com/impl.v1.RelationMetadataR\x0fmetadataMessage\"\xaa\x01\n" + + "\tDecorator\x12;\n" + + "\x04name\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{0,62}[a-z0-9]$R\x04name\x12;\n" + + "\n" + + "parameters\x18\x02 \x03(\v2\x1b.core.v1.DecoratorParameterR\n" + + "parameters\x12#\n" + + "\rrequired_flag\x18\x03 \x01(\tR\frequiredFlag\"\xe0\x01\n" + + "\x12DecoratorParameter\x12;\n" + + "\x04name\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{0,62}[a-z0-9]$R\x04name\x12\x1d\n" + + "\tint_value\x18\x02 \x01(\x03H\x00R\bintValue\x12#\n" + + "\fstring_value\x18\x03 \x01(\tH\x00R\vstringValue\x12\x1f\n" + + "\n" + + "bool_value\x18\x04 \x01(\bH\x00R\tboolValue\x12\x1f\n" + + "\n" + + "enum_value\x18\x05 \x01(\tH\x00R\tenumValueB\a\n" + + "\x05value\"\xc7\x02\n" + "\x13NamespaceDefinition\x12\\\n" + "\x04name\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,62}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12-\n" + "\brelation\x18\x02 \x03(\v2\x11.core.v1.RelationR\brelation\x12-\n" + "\bmetadata\x18\x03 \x01(\v2\x11.core.v1.MetadataR\bmetadata\x12@\n" + - "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\"\x9c\x03\n" + + "\x0fsource_position\x18\x04 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x122\n" + + "\n" + + "decorators\x18\x05 \x03(\v2\x12.core.v1.DecoratorR\n" + + "decorators\"\xd0\x03\n" + "\bRelation\x12;\n" + "\x04name\x18\x01 \x01(\tB'\xbaH$r\"(@2\x1e^[a-z][a-z0-9_]{1,62}[a-z0-9]$R\x04name\x12@\n" + "\x0fuserset_rewrite\x18\x02 \x01(\v2\x17.core.v1.UsersetRewriteR\x0eusersetRewrite\x12C\n" + @@ -3291,7 +3540,10 @@ const file_core_v1_core_proto_rawDesc = "" + "\bmetadata\x18\x04 \x01(\v2\x11.core.v1.MetadataR\bmetadata\x12@\n" + "\x0fsource_position\x18\x05 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x12+\n" + "\x11aliasing_relation\x18\x06 \x01(\tR\x10aliasingRelation\x12.\n" + - "\x13canonical_cache_key\x18\a \x01(\tR\x11canonicalCacheKey\"\xf4\x03\n" + + "\x13canonical_cache_key\x18\a \x01(\tR\x11canonicalCacheKey\x122\n" + + "\n" + + "decorators\x18\b \x03(\v2\x12.core.v1.DecoratorR\n" + + "decorators\"\xf4\x03\n" + "\x11ReachabilityGraph\x12w\n" + "\x1bentrypoints_by_subject_type\x18\x01 \x03(\v28.core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntryR\x18entrypointsBySubjectType\x12\x83\x01\n" + "\x1fentrypoints_by_subject_relation\x18\x02 \x03(\v2<.core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntryR\x1centrypointsBySubjectRelation\x1am\n" + @@ -3320,14 +3572,17 @@ const file_core_v1_core_proto_rawDesc = "" + "\x1cREACHABLE_CONDITIONAL_RESULT\x10\x00\x12\x1b\n" + "\x17DIRECT_OPERATION_RESULT\x10\x01J\x04\b\x03\x10\x04\"e\n" + "\x0fTypeInformation\x12R\n" + - "\x18allowed_direct_relations\x18\x01 \x03(\v2\x18.core.v1.AllowedRelationR\x16allowedDirectRelations\"\x95\x04\n" + + "\x18allowed_direct_relations\x18\x01 \x03(\v2\x18.core.v1.AllowedRelationR\x16allowedDirectRelations\"\xc9\x04\n" + "\x0fAllowedRelation\x12f\n" + "\tnamespace\x18\x01 \x01(\tBH\xbaHErC(\x80\x012>^([a-z][a-z0-9_]{1,61}[a-z0-9]/)*[a-z][a-z0-9_]{1,62}[a-z0-9]$R\tnamespace\x12N\n" + "\brelation\x18\x03 \x01(\tB0\xbaH-r+(@2'^(\\.\\.\\.|[a-z][a-z0-9_]{1,62}[a-z0-9])$H\x00R\brelation\x12R\n" + "\x0fpublic_wildcard\x18\x04 \x01(\v2'.core.v1.AllowedRelation.PublicWildcardH\x00R\x0epublicWildcard\x12@\n" + "\x0fsource_position\x18\x05 \x01(\v2\x17.core.v1.SourcePositionR\x0esourcePosition\x12?\n" + "\x0frequired_caveat\x18\x06 \x01(\v2\x16.core.v1.AllowedCaveatR\x0erequiredCaveat\x12I\n" + - "\x13required_expiration\x18\a \x01(\v2\x18.core.v1.ExpirationTraitR\x12requiredExpiration\x1a\x10\n" + + "\x13required_expiration\x18\a \x01(\v2\x18.core.v1.ExpirationTraitR\x12requiredExpiration\x122\n" + + "\n" + + "decorators\x18\b \x03(\v2\x12.core.v1.DecoratorR\n" + + "decorators\x1a\x10\n" + "\x0ePublicWildcardB\x16\n" + "\x14relation_or_wildcard\"\x11\n" + "\x0fExpirationTrait\"0\n" + @@ -3442,7 +3697,7 @@ func file_core_v1_core_proto_rawDescGZIP() []byte { } var file_core_v1_core_proto_enumTypes = make([]protoimpl.EnumInfo, 7) -var file_core_v1_core_proto_msgTypes = make([]protoimpl.MessageInfo, 48) +var file_core_v1_core_proto_msgTypes = make([]protoimpl.MessageInfo, 50) var file_core_v1_core_proto_goTypes = []any{ (RelationTupleUpdate_Operation)(0), // 0: core.v1.RelationTupleUpdate.Operation (SetOperationUserset_Operation)(0), // 1: core.v1.SetOperationUserset.Operation @@ -3465,128 +3720,135 @@ var file_core_v1_core_proto_goTypes = []any{ (*DirectSubject)(nil), // 18: core.v1.DirectSubject (*DirectSubjects)(nil), // 19: core.v1.DirectSubjects (*Metadata)(nil), // 20: core.v1.Metadata - (*NamespaceDefinition)(nil), // 21: core.v1.NamespaceDefinition - (*Relation)(nil), // 22: core.v1.Relation - (*ReachabilityGraph)(nil), // 23: core.v1.ReachabilityGraph - (*ReachabilityEntrypoints)(nil), // 24: core.v1.ReachabilityEntrypoints - (*ReachabilityEntrypoint)(nil), // 25: core.v1.ReachabilityEntrypoint - (*TypeInformation)(nil), // 26: core.v1.TypeInformation - (*AllowedRelation)(nil), // 27: core.v1.AllowedRelation - (*ExpirationTrait)(nil), // 28: core.v1.ExpirationTrait - (*AllowedCaveat)(nil), // 29: core.v1.AllowedCaveat - (*UsersetRewrite)(nil), // 30: core.v1.UsersetRewrite - (*SetOperation)(nil), // 31: core.v1.SetOperation - (*TupleToUserset)(nil), // 32: core.v1.TupleToUserset - (*FunctionedTupleToUserset)(nil), // 33: core.v1.FunctionedTupleToUserset - (*ComputedUserset)(nil), // 34: core.v1.ComputedUserset - (*SourcePosition)(nil), // 35: core.v1.SourcePosition - (*CaveatExpression)(nil), // 36: core.v1.CaveatExpression - (*CaveatOperation)(nil), // 37: core.v1.CaveatOperation - (*RelationshipFilter)(nil), // 38: core.v1.RelationshipFilter - (*SubjectFilter)(nil), // 39: core.v1.SubjectFilter - (*StoredSchema)(nil), // 40: core.v1.StoredSchema - nil, // 41: core.v1.CaveatDefinition.ParameterTypesEntry - nil, // 42: core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry - nil, // 43: core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry - (*AllowedRelation_PublicWildcard)(nil), // 44: core.v1.AllowedRelation.PublicWildcard - (*SetOperation_Child)(nil), // 45: core.v1.SetOperation.Child - (*SetOperation_Child_This)(nil), // 46: core.v1.SetOperation.Child.This - (*SetOperation_Child_Nil)(nil), // 47: core.v1.SetOperation.Child.Nil - (*SetOperation_Child_Self)(nil), // 48: core.v1.SetOperation.Child.Self - (*TupleToUserset_Tupleset)(nil), // 49: core.v1.TupleToUserset.Tupleset - (*FunctionedTupleToUserset_Tupleset)(nil), // 50: core.v1.FunctionedTupleToUserset.Tupleset - (*SubjectFilter_RelationFilter)(nil), // 51: core.v1.SubjectFilter.RelationFilter - (*StoredSchema_V1StoredSchema)(nil), // 52: core.v1.StoredSchema.V1StoredSchema - nil, // 53: core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry - nil, // 54: core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry - (*timestamppb.Timestamp)(nil), // 55: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 56: google.protobuf.Struct - (*anypb.Any)(nil), // 57: google.protobuf.Any + (*Decorator)(nil), // 21: core.v1.Decorator + (*DecoratorParameter)(nil), // 22: core.v1.DecoratorParameter + (*NamespaceDefinition)(nil), // 23: core.v1.NamespaceDefinition + (*Relation)(nil), // 24: core.v1.Relation + (*ReachabilityGraph)(nil), // 25: core.v1.ReachabilityGraph + (*ReachabilityEntrypoints)(nil), // 26: core.v1.ReachabilityEntrypoints + (*ReachabilityEntrypoint)(nil), // 27: core.v1.ReachabilityEntrypoint + (*TypeInformation)(nil), // 28: core.v1.TypeInformation + (*AllowedRelation)(nil), // 29: core.v1.AllowedRelation + (*ExpirationTrait)(nil), // 30: core.v1.ExpirationTrait + (*AllowedCaveat)(nil), // 31: core.v1.AllowedCaveat + (*UsersetRewrite)(nil), // 32: core.v1.UsersetRewrite + (*SetOperation)(nil), // 33: core.v1.SetOperation + (*TupleToUserset)(nil), // 34: core.v1.TupleToUserset + (*FunctionedTupleToUserset)(nil), // 35: core.v1.FunctionedTupleToUserset + (*ComputedUserset)(nil), // 36: core.v1.ComputedUserset + (*SourcePosition)(nil), // 37: core.v1.SourcePosition + (*CaveatExpression)(nil), // 38: core.v1.CaveatExpression + (*CaveatOperation)(nil), // 39: core.v1.CaveatOperation + (*RelationshipFilter)(nil), // 40: core.v1.RelationshipFilter + (*SubjectFilter)(nil), // 41: core.v1.SubjectFilter + (*StoredSchema)(nil), // 42: core.v1.StoredSchema + nil, // 43: core.v1.CaveatDefinition.ParameterTypesEntry + nil, // 44: core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry + nil, // 45: core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry + (*AllowedRelation_PublicWildcard)(nil), // 46: core.v1.AllowedRelation.PublicWildcard + (*SetOperation_Child)(nil), // 47: core.v1.SetOperation.Child + (*SetOperation_Child_This)(nil), // 48: core.v1.SetOperation.Child.This + (*SetOperation_Child_Nil)(nil), // 49: core.v1.SetOperation.Child.Nil + (*SetOperation_Child_Self)(nil), // 50: core.v1.SetOperation.Child.Self + (*TupleToUserset_Tupleset)(nil), // 51: core.v1.TupleToUserset.Tupleset + (*FunctionedTupleToUserset_Tupleset)(nil), // 52: core.v1.FunctionedTupleToUserset.Tupleset + (*SubjectFilter_RelationFilter)(nil), // 53: core.v1.SubjectFilter.RelationFilter + (*StoredSchema_V1StoredSchema)(nil), // 54: core.v1.StoredSchema.V1StoredSchema + nil, // 55: core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry + nil, // 56: core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry + (*timestamppb.Timestamp)(nil), // 57: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 58: google.protobuf.Struct + (*anypb.Any)(nil), // 59: google.protobuf.Any } var file_core_v1_core_proto_depIdxs = []int32{ 12, // 0: core.v1.RelationTuple.resource_and_relation:type_name -> core.v1.ObjectAndRelation 12, // 1: core.v1.RelationTuple.subject:type_name -> core.v1.ObjectAndRelation 9, // 2: core.v1.RelationTuple.caveat:type_name -> core.v1.ContextualizedCaveat 8, // 3: core.v1.RelationTuple.integrity:type_name -> core.v1.RelationshipIntegrity - 55, // 4: core.v1.RelationTuple.optional_expiration_time:type_name -> google.protobuf.Timestamp - 55, // 5: core.v1.RelationshipIntegrity.hashed_at:type_name -> google.protobuf.Timestamp - 56, // 6: core.v1.ContextualizedCaveat.context:type_name -> google.protobuf.Struct - 41, // 7: core.v1.CaveatDefinition.parameter_types:type_name -> core.v1.CaveatDefinition.ParameterTypesEntry + 57, // 4: core.v1.RelationTuple.optional_expiration_time:type_name -> google.protobuf.Timestamp + 57, // 5: core.v1.RelationshipIntegrity.hashed_at:type_name -> google.protobuf.Timestamp + 58, // 6: core.v1.ContextualizedCaveat.context:type_name -> google.protobuf.Struct + 43, // 7: core.v1.CaveatDefinition.parameter_types:type_name -> core.v1.CaveatDefinition.ParameterTypesEntry 20, // 8: core.v1.CaveatDefinition.metadata:type_name -> core.v1.Metadata - 35, // 9: core.v1.CaveatDefinition.source_position:type_name -> core.v1.SourcePosition - 11, // 10: core.v1.CaveatTypeReference.child_types:type_name -> core.v1.CaveatTypeReference - 0, // 11: core.v1.RelationTupleUpdate.operation:type_name -> core.v1.RelationTupleUpdate.Operation - 7, // 12: core.v1.RelationTupleUpdate.tuple:type_name -> core.v1.RelationTuple - 17, // 13: core.v1.RelationTupleTreeNode.intermediate_node:type_name -> core.v1.SetOperationUserset - 19, // 14: core.v1.RelationTupleTreeNode.leaf_node:type_name -> core.v1.DirectSubjects - 12, // 15: core.v1.RelationTupleTreeNode.expanded:type_name -> core.v1.ObjectAndRelation - 36, // 16: core.v1.RelationTupleTreeNode.caveat_expression:type_name -> core.v1.CaveatExpression - 1, // 17: core.v1.SetOperationUserset.operation:type_name -> core.v1.SetOperationUserset.Operation - 16, // 18: core.v1.SetOperationUserset.child_nodes:type_name -> core.v1.RelationTupleTreeNode - 12, // 19: core.v1.DirectSubject.subject:type_name -> core.v1.ObjectAndRelation - 36, // 20: core.v1.DirectSubject.caveat_expression:type_name -> core.v1.CaveatExpression - 18, // 21: core.v1.DirectSubjects.subjects:type_name -> core.v1.DirectSubject - 57, // 22: core.v1.Metadata.metadata_message:type_name -> google.protobuf.Any - 22, // 23: core.v1.NamespaceDefinition.relation:type_name -> core.v1.Relation - 20, // 24: core.v1.NamespaceDefinition.metadata:type_name -> core.v1.Metadata - 35, // 25: core.v1.NamespaceDefinition.source_position:type_name -> core.v1.SourcePosition - 30, // 26: core.v1.Relation.userset_rewrite:type_name -> core.v1.UsersetRewrite - 26, // 27: core.v1.Relation.type_information:type_name -> core.v1.TypeInformation - 20, // 28: core.v1.Relation.metadata:type_name -> core.v1.Metadata - 35, // 29: core.v1.Relation.source_position:type_name -> core.v1.SourcePosition - 42, // 30: core.v1.ReachabilityGraph.entrypoints_by_subject_type:type_name -> core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry - 43, // 31: core.v1.ReachabilityGraph.entrypoints_by_subject_relation:type_name -> core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry - 25, // 32: core.v1.ReachabilityEntrypoints.entrypoints:type_name -> core.v1.ReachabilityEntrypoint - 13, // 33: core.v1.ReachabilityEntrypoints.subject_relation:type_name -> core.v1.RelationReference - 2, // 34: core.v1.ReachabilityEntrypoint.kind:type_name -> core.v1.ReachabilityEntrypoint.ReachabilityEntrypointKind - 13, // 35: core.v1.ReachabilityEntrypoint.target_relation:type_name -> core.v1.RelationReference - 3, // 36: core.v1.ReachabilityEntrypoint.result_status:type_name -> core.v1.ReachabilityEntrypoint.EntrypointResultStatus - 27, // 37: core.v1.TypeInformation.allowed_direct_relations:type_name -> core.v1.AllowedRelation - 44, // 38: core.v1.AllowedRelation.public_wildcard:type_name -> core.v1.AllowedRelation.PublicWildcard - 35, // 39: core.v1.AllowedRelation.source_position:type_name -> core.v1.SourcePosition - 29, // 40: core.v1.AllowedRelation.required_caveat:type_name -> core.v1.AllowedCaveat - 28, // 41: core.v1.AllowedRelation.required_expiration:type_name -> core.v1.ExpirationTrait - 31, // 42: core.v1.UsersetRewrite.union:type_name -> core.v1.SetOperation - 31, // 43: core.v1.UsersetRewrite.intersection:type_name -> core.v1.SetOperation - 31, // 44: core.v1.UsersetRewrite.exclusion:type_name -> core.v1.SetOperation - 35, // 45: core.v1.UsersetRewrite.source_position:type_name -> core.v1.SourcePosition - 45, // 46: core.v1.SetOperation.child:type_name -> core.v1.SetOperation.Child - 49, // 47: core.v1.TupleToUserset.tupleset:type_name -> core.v1.TupleToUserset.Tupleset - 34, // 48: core.v1.TupleToUserset.computed_userset:type_name -> core.v1.ComputedUserset - 35, // 49: core.v1.TupleToUserset.source_position:type_name -> core.v1.SourcePosition - 4, // 50: core.v1.FunctionedTupleToUserset.function:type_name -> core.v1.FunctionedTupleToUserset.Function - 50, // 51: core.v1.FunctionedTupleToUserset.tupleset:type_name -> core.v1.FunctionedTupleToUserset.Tupleset - 34, // 52: core.v1.FunctionedTupleToUserset.computed_userset:type_name -> core.v1.ComputedUserset - 35, // 53: core.v1.FunctionedTupleToUserset.source_position:type_name -> core.v1.SourcePosition - 5, // 54: core.v1.ComputedUserset.object:type_name -> core.v1.ComputedUserset.Object - 35, // 55: core.v1.ComputedUserset.source_position:type_name -> core.v1.SourcePosition - 37, // 56: core.v1.CaveatExpression.operation:type_name -> core.v1.CaveatOperation - 9, // 57: core.v1.CaveatExpression.caveat:type_name -> core.v1.ContextualizedCaveat - 6, // 58: core.v1.CaveatOperation.op:type_name -> core.v1.CaveatOperation.Operation - 36, // 59: core.v1.CaveatOperation.children:type_name -> core.v1.CaveatExpression - 39, // 60: core.v1.RelationshipFilter.optional_subject_filter:type_name -> core.v1.SubjectFilter - 51, // 61: core.v1.SubjectFilter.optional_relation:type_name -> core.v1.SubjectFilter.RelationFilter - 52, // 62: core.v1.StoredSchema.v1:type_name -> core.v1.StoredSchema.V1StoredSchema - 11, // 63: core.v1.CaveatDefinition.ParameterTypesEntry.value:type_name -> core.v1.CaveatTypeReference - 24, // 64: core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry.value:type_name -> core.v1.ReachabilityEntrypoints - 24, // 65: core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry.value:type_name -> core.v1.ReachabilityEntrypoints - 46, // 66: core.v1.SetOperation.Child._this:type_name -> core.v1.SetOperation.Child.This - 34, // 67: core.v1.SetOperation.Child.computed_userset:type_name -> core.v1.ComputedUserset - 32, // 68: core.v1.SetOperation.Child.tuple_to_userset:type_name -> core.v1.TupleToUserset - 30, // 69: core.v1.SetOperation.Child.userset_rewrite:type_name -> core.v1.UsersetRewrite - 33, // 70: core.v1.SetOperation.Child.functioned_tuple_to_userset:type_name -> core.v1.FunctionedTupleToUserset - 47, // 71: core.v1.SetOperation.Child._nil:type_name -> core.v1.SetOperation.Child.Nil - 48, // 72: core.v1.SetOperation.Child._self:type_name -> core.v1.SetOperation.Child.Self - 35, // 73: core.v1.SetOperation.Child.source_position:type_name -> core.v1.SourcePosition - 53, // 74: core.v1.StoredSchema.V1StoredSchema.namespace_definitions:type_name -> core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry - 54, // 75: core.v1.StoredSchema.V1StoredSchema.caveat_definitions:type_name -> core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry - 21, // 76: core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry.value:type_name -> core.v1.NamespaceDefinition - 10, // 77: core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry.value:type_name -> core.v1.CaveatDefinition - 78, // [78:78] is the sub-list for method output_type - 78, // [78:78] is the sub-list for method input_type - 78, // [78:78] is the sub-list for extension type_name - 78, // [78:78] is the sub-list for extension extendee - 0, // [0:78] is the sub-list for field type_name + 37, // 9: core.v1.CaveatDefinition.source_position:type_name -> core.v1.SourcePosition + 21, // 10: core.v1.CaveatDefinition.decorators:type_name -> core.v1.Decorator + 11, // 11: core.v1.CaveatTypeReference.child_types:type_name -> core.v1.CaveatTypeReference + 0, // 12: core.v1.RelationTupleUpdate.operation:type_name -> core.v1.RelationTupleUpdate.Operation + 7, // 13: core.v1.RelationTupleUpdate.tuple:type_name -> core.v1.RelationTuple + 17, // 14: core.v1.RelationTupleTreeNode.intermediate_node:type_name -> core.v1.SetOperationUserset + 19, // 15: core.v1.RelationTupleTreeNode.leaf_node:type_name -> core.v1.DirectSubjects + 12, // 16: core.v1.RelationTupleTreeNode.expanded:type_name -> core.v1.ObjectAndRelation + 38, // 17: core.v1.RelationTupleTreeNode.caveat_expression:type_name -> core.v1.CaveatExpression + 1, // 18: core.v1.SetOperationUserset.operation:type_name -> core.v1.SetOperationUserset.Operation + 16, // 19: core.v1.SetOperationUserset.child_nodes:type_name -> core.v1.RelationTupleTreeNode + 12, // 20: core.v1.DirectSubject.subject:type_name -> core.v1.ObjectAndRelation + 38, // 21: core.v1.DirectSubject.caveat_expression:type_name -> core.v1.CaveatExpression + 18, // 22: core.v1.DirectSubjects.subjects:type_name -> core.v1.DirectSubject + 59, // 23: core.v1.Metadata.metadata_message:type_name -> google.protobuf.Any + 22, // 24: core.v1.Decorator.parameters:type_name -> core.v1.DecoratorParameter + 24, // 25: core.v1.NamespaceDefinition.relation:type_name -> core.v1.Relation + 20, // 26: core.v1.NamespaceDefinition.metadata:type_name -> core.v1.Metadata + 37, // 27: core.v1.NamespaceDefinition.source_position:type_name -> core.v1.SourcePosition + 21, // 28: core.v1.NamespaceDefinition.decorators:type_name -> core.v1.Decorator + 32, // 29: core.v1.Relation.userset_rewrite:type_name -> core.v1.UsersetRewrite + 28, // 30: core.v1.Relation.type_information:type_name -> core.v1.TypeInformation + 20, // 31: core.v1.Relation.metadata:type_name -> core.v1.Metadata + 37, // 32: core.v1.Relation.source_position:type_name -> core.v1.SourcePosition + 21, // 33: core.v1.Relation.decorators:type_name -> core.v1.Decorator + 44, // 34: core.v1.ReachabilityGraph.entrypoints_by_subject_type:type_name -> core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry + 45, // 35: core.v1.ReachabilityGraph.entrypoints_by_subject_relation:type_name -> core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry + 27, // 36: core.v1.ReachabilityEntrypoints.entrypoints:type_name -> core.v1.ReachabilityEntrypoint + 13, // 37: core.v1.ReachabilityEntrypoints.subject_relation:type_name -> core.v1.RelationReference + 2, // 38: core.v1.ReachabilityEntrypoint.kind:type_name -> core.v1.ReachabilityEntrypoint.ReachabilityEntrypointKind + 13, // 39: core.v1.ReachabilityEntrypoint.target_relation:type_name -> core.v1.RelationReference + 3, // 40: core.v1.ReachabilityEntrypoint.result_status:type_name -> core.v1.ReachabilityEntrypoint.EntrypointResultStatus + 29, // 41: core.v1.TypeInformation.allowed_direct_relations:type_name -> core.v1.AllowedRelation + 46, // 42: core.v1.AllowedRelation.public_wildcard:type_name -> core.v1.AllowedRelation.PublicWildcard + 37, // 43: core.v1.AllowedRelation.source_position:type_name -> core.v1.SourcePosition + 31, // 44: core.v1.AllowedRelation.required_caveat:type_name -> core.v1.AllowedCaveat + 30, // 45: core.v1.AllowedRelation.required_expiration:type_name -> core.v1.ExpirationTrait + 21, // 46: core.v1.AllowedRelation.decorators:type_name -> core.v1.Decorator + 33, // 47: core.v1.UsersetRewrite.union:type_name -> core.v1.SetOperation + 33, // 48: core.v1.UsersetRewrite.intersection:type_name -> core.v1.SetOperation + 33, // 49: core.v1.UsersetRewrite.exclusion:type_name -> core.v1.SetOperation + 37, // 50: core.v1.UsersetRewrite.source_position:type_name -> core.v1.SourcePosition + 47, // 51: core.v1.SetOperation.child:type_name -> core.v1.SetOperation.Child + 51, // 52: core.v1.TupleToUserset.tupleset:type_name -> core.v1.TupleToUserset.Tupleset + 36, // 53: core.v1.TupleToUserset.computed_userset:type_name -> core.v1.ComputedUserset + 37, // 54: core.v1.TupleToUserset.source_position:type_name -> core.v1.SourcePosition + 4, // 55: core.v1.FunctionedTupleToUserset.function:type_name -> core.v1.FunctionedTupleToUserset.Function + 52, // 56: core.v1.FunctionedTupleToUserset.tupleset:type_name -> core.v1.FunctionedTupleToUserset.Tupleset + 36, // 57: core.v1.FunctionedTupleToUserset.computed_userset:type_name -> core.v1.ComputedUserset + 37, // 58: core.v1.FunctionedTupleToUserset.source_position:type_name -> core.v1.SourcePosition + 5, // 59: core.v1.ComputedUserset.object:type_name -> core.v1.ComputedUserset.Object + 37, // 60: core.v1.ComputedUserset.source_position:type_name -> core.v1.SourcePosition + 39, // 61: core.v1.CaveatExpression.operation:type_name -> core.v1.CaveatOperation + 9, // 62: core.v1.CaveatExpression.caveat:type_name -> core.v1.ContextualizedCaveat + 6, // 63: core.v1.CaveatOperation.op:type_name -> core.v1.CaveatOperation.Operation + 38, // 64: core.v1.CaveatOperation.children:type_name -> core.v1.CaveatExpression + 41, // 65: core.v1.RelationshipFilter.optional_subject_filter:type_name -> core.v1.SubjectFilter + 53, // 66: core.v1.SubjectFilter.optional_relation:type_name -> core.v1.SubjectFilter.RelationFilter + 54, // 67: core.v1.StoredSchema.v1:type_name -> core.v1.StoredSchema.V1StoredSchema + 11, // 68: core.v1.CaveatDefinition.ParameterTypesEntry.value:type_name -> core.v1.CaveatTypeReference + 26, // 69: core.v1.ReachabilityGraph.EntrypointsBySubjectTypeEntry.value:type_name -> core.v1.ReachabilityEntrypoints + 26, // 70: core.v1.ReachabilityGraph.EntrypointsBySubjectRelationEntry.value:type_name -> core.v1.ReachabilityEntrypoints + 48, // 71: core.v1.SetOperation.Child._this:type_name -> core.v1.SetOperation.Child.This + 36, // 72: core.v1.SetOperation.Child.computed_userset:type_name -> core.v1.ComputedUserset + 34, // 73: core.v1.SetOperation.Child.tuple_to_userset:type_name -> core.v1.TupleToUserset + 32, // 74: core.v1.SetOperation.Child.userset_rewrite:type_name -> core.v1.UsersetRewrite + 35, // 75: core.v1.SetOperation.Child.functioned_tuple_to_userset:type_name -> core.v1.FunctionedTupleToUserset + 49, // 76: core.v1.SetOperation.Child._nil:type_name -> core.v1.SetOperation.Child.Nil + 50, // 77: core.v1.SetOperation.Child._self:type_name -> core.v1.SetOperation.Child.Self + 37, // 78: core.v1.SetOperation.Child.source_position:type_name -> core.v1.SourcePosition + 55, // 79: core.v1.StoredSchema.V1StoredSchema.namespace_definitions:type_name -> core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry + 56, // 80: core.v1.StoredSchema.V1StoredSchema.caveat_definitions:type_name -> core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry + 23, // 81: core.v1.StoredSchema.V1StoredSchema.NamespaceDefinitionsEntry.value:type_name -> core.v1.NamespaceDefinition + 10, // 82: core.v1.StoredSchema.V1StoredSchema.CaveatDefinitionsEntry.value:type_name -> core.v1.CaveatDefinition + 83, // [83:83] is the sub-list for method output_type + 83, // [83:83] is the sub-list for method input_type + 83, // [83:83] is the sub-list for extension type_name + 83, // [83:83] is the sub-list for extension extendee + 0, // [0:83] is the sub-list for field type_name } func init() { file_core_v1_core_proto_init() } @@ -3598,23 +3860,29 @@ func file_core_v1_core_proto_init() { (*RelationTupleTreeNode_IntermediateNode)(nil), (*RelationTupleTreeNode_LeafNode)(nil), } - file_core_v1_core_proto_msgTypes[20].OneofWrappers = []any{ + file_core_v1_core_proto_msgTypes[15].OneofWrappers = []any{ + (*DecoratorParameter_IntValue)(nil), + (*DecoratorParameter_StringValue)(nil), + (*DecoratorParameter_BoolValue)(nil), + (*DecoratorParameter_EnumValue)(nil), + } + file_core_v1_core_proto_msgTypes[22].OneofWrappers = []any{ (*AllowedRelation_Relation)(nil), (*AllowedRelation_PublicWildcard_)(nil), } - file_core_v1_core_proto_msgTypes[23].OneofWrappers = []any{ + file_core_v1_core_proto_msgTypes[25].OneofWrappers = []any{ (*UsersetRewrite_Union)(nil), (*UsersetRewrite_Intersection)(nil), (*UsersetRewrite_Exclusion)(nil), } - file_core_v1_core_proto_msgTypes[29].OneofWrappers = []any{ + file_core_v1_core_proto_msgTypes[31].OneofWrappers = []any{ (*CaveatExpression_Operation)(nil), (*CaveatExpression_Caveat)(nil), } - file_core_v1_core_proto_msgTypes[33].OneofWrappers = []any{ + file_core_v1_core_proto_msgTypes[35].OneofWrappers = []any{ (*StoredSchema_V1)(nil), } - file_core_v1_core_proto_msgTypes[38].OneofWrappers = []any{ + file_core_v1_core_proto_msgTypes[40].OneofWrappers = []any{ (*SetOperation_Child_XThis)(nil), (*SetOperation_Child_ComputedUserset)(nil), (*SetOperation_Child_TupleToUserset)(nil), @@ -3629,7 +3897,7 @@ func file_core_v1_core_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_core_v1_core_proto_rawDesc), len(file_core_v1_core_proto_rawDesc)), NumEnums: 7, - NumMessages: 48, + NumMessages: 50, NumExtensions: 0, NumServices: 0, }, diff --git a/pkg/proto/core/v1/core_vtproto.pb.go b/pkg/proto/core/v1/core_vtproto.pb.go index ea58868c5c..50021cbf6b 100644 --- a/pkg/proto/core/v1/core_vtproto.pb.go +++ b/pkg/proto/core/v1/core_vtproto.pb.go @@ -107,6 +107,13 @@ func (m *CaveatDefinition) CloneVT() *CaveatDefinition { } r.ParameterTypes = tmpContainer } + if rhs := m.Decorators; rhs != nil { + tmpContainer := make([]*Decorator, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Decorators = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -343,6 +350,89 @@ func (m *Metadata) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *Decorator) CloneVT() *Decorator { + if m == nil { + return (*Decorator)(nil) + } + r := new(Decorator) + r.Name = m.Name + r.RequiredFlag = m.RequiredFlag + if rhs := m.Parameters; rhs != nil { + tmpContainer := make([]*DecoratorParameter, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Parameters = tmpContainer + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *Decorator) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *DecoratorParameter) CloneVT() *DecoratorParameter { + if m == nil { + return (*DecoratorParameter)(nil) + } + r := new(DecoratorParameter) + r.Name = m.Name + if m.Value != nil { + r.Value = m.Value.(interface { + CloneVT() isDecoratorParameter_Value + }).CloneVT() + } + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *DecoratorParameter) CloneMessageVT() proto.Message { + return m.CloneVT() +} + +func (m *DecoratorParameter_IntValue) CloneVT() isDecoratorParameter_Value { + if m == nil { + return (*DecoratorParameter_IntValue)(nil) + } + r := new(DecoratorParameter_IntValue) + r.IntValue = m.IntValue + return r +} + +func (m *DecoratorParameter_StringValue) CloneVT() isDecoratorParameter_Value { + if m == nil { + return (*DecoratorParameter_StringValue)(nil) + } + r := new(DecoratorParameter_StringValue) + r.StringValue = m.StringValue + return r +} + +func (m *DecoratorParameter_BoolValue) CloneVT() isDecoratorParameter_Value { + if m == nil { + return (*DecoratorParameter_BoolValue)(nil) + } + r := new(DecoratorParameter_BoolValue) + r.BoolValue = m.BoolValue + return r +} + +func (m *DecoratorParameter_EnumValue) CloneVT() isDecoratorParameter_Value { + if m == nil { + return (*DecoratorParameter_EnumValue)(nil) + } + r := new(DecoratorParameter_EnumValue) + r.EnumValue = m.EnumValue + return r +} + func (m *NamespaceDefinition) CloneVT() *NamespaceDefinition { if m == nil { return (*NamespaceDefinition)(nil) @@ -358,6 +448,13 @@ func (m *NamespaceDefinition) CloneVT() *NamespaceDefinition { } r.Relation = tmpContainer } + if rhs := m.Decorators; rhs != nil { + tmpContainer := make([]*Decorator, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Decorators = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -381,6 +478,13 @@ func (m *Relation) CloneVT() *Relation { r.SourcePosition = m.SourcePosition.CloneVT() r.AliasingRelation = m.AliasingRelation r.CanonicalCacheKey = m.CanonicalCacheKey + if rhs := m.Decorators; rhs != nil { + tmpContainer := make([]*Decorator, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Decorators = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -521,6 +625,13 @@ func (m *AllowedRelation) CloneVT() *AllowedRelation { CloneVT() isAllowedRelation_RelationOrWildcard }).CloneVT() } + if rhs := m.Decorators; rhs != nil { + tmpContainer := make([]*Decorator, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.Decorators = tmpContainer + } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -1202,6 +1313,23 @@ func (this *CaveatDefinition) EqualVT(that *CaveatDefinition) bool { if !this.SourcePosition.EqualVT(that.SourcePosition) { return false } + if len(this.Decorators) != len(that.Decorators) { + return false + } + for i, vx := range this.Decorators { + vy := that.Decorators[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &Decorator{} + } + if q == nil { + q = &Decorator{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -1544,6 +1672,144 @@ func (this *Metadata) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *Decorator) EqualVT(that *Decorator) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Name != that.Name { + return false + } + if len(this.Parameters) != len(that.Parameters) { + return false + } + for i, vx := range this.Parameters { + vy := that.Parameters[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &DecoratorParameter{} + } + if q == nil { + q = &DecoratorParameter{} + } + if !p.EqualVT(q) { + return false + } + } + } + if this.RequiredFlag != that.RequiredFlag { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *Decorator) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*Decorator) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *DecoratorParameter) EqualVT(that *DecoratorParameter) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Value == nil && that.Value != nil { + return false + } else if this.Value != nil { + if that.Value == nil { + return false + } + if !this.Value.(interface { + EqualVT(isDecoratorParameter_Value) bool + }).EqualVT(that.Value) { + return false + } + } + if this.Name != that.Name { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *DecoratorParameter) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*DecoratorParameter) + if !ok { + return false + } + return this.EqualVT(that) +} +func (this *DecoratorParameter_IntValue) EqualVT(thatIface isDecoratorParameter_Value) bool { + that, ok := thatIface.(*DecoratorParameter_IntValue) + if !ok { + return false + } + if this == that { + return true + } + if this == nil && that != nil || this != nil && that == nil { + return false + } + if this.IntValue != that.IntValue { + return false + } + return true +} + +func (this *DecoratorParameter_StringValue) EqualVT(thatIface isDecoratorParameter_Value) bool { + that, ok := thatIface.(*DecoratorParameter_StringValue) + if !ok { + return false + } + if this == that { + return true + } + if this == nil && that != nil || this != nil && that == nil { + return false + } + if this.StringValue != that.StringValue { + return false + } + return true +} + +func (this *DecoratorParameter_BoolValue) EqualVT(thatIface isDecoratorParameter_Value) bool { + that, ok := thatIface.(*DecoratorParameter_BoolValue) + if !ok { + return false + } + if this == that { + return true + } + if this == nil && that != nil || this != nil && that == nil { + return false + } + if this.BoolValue != that.BoolValue { + return false + } + return true +} + +func (this *DecoratorParameter_EnumValue) EqualVT(thatIface isDecoratorParameter_Value) bool { + that, ok := thatIface.(*DecoratorParameter_EnumValue) + if !ok { + return false + } + if this == that { + return true + } + if this == nil && that != nil || this != nil && that == nil { + return false + } + if this.EnumValue != that.EnumValue { + return false + } + return true +} + func (this *NamespaceDefinition) EqualVT(that *NamespaceDefinition) bool { if this == that { return true @@ -1576,6 +1842,23 @@ func (this *NamespaceDefinition) EqualVT(that *NamespaceDefinition) bool { if !this.SourcePosition.EqualVT(that.SourcePosition) { return false } + if len(this.Decorators) != len(that.Decorators) { + return false + } + for i, vx := range this.Decorators { + vy := that.Decorators[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &Decorator{} + } + if q == nil { + q = &Decorator{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -1613,6 +1896,23 @@ func (this *Relation) EqualVT(that *Relation) bool { if this.CanonicalCacheKey != that.CanonicalCacheKey { return false } + if len(this.Decorators) != len(that.Decorators) { + return false + } + for i, vx := range this.Decorators { + vy := that.Decorators[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &Decorator{} + } + if q == nil { + q = &Decorator{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -1828,6 +2128,23 @@ func (this *AllowedRelation) EqualVT(that *AllowedRelation) bool { if !this.RequiredExpiration.EqualVT(that.RequiredExpiration) { return false } + if len(this.Decorators) != len(that.Decorators) { + return false + } + for i, vx := range this.Decorators { + vy := that.Decorators[i] + if p, q := vx, vy; p != q { + if p == nil { + p = &Decorator{} + } + if q == nil { + q = &Decorator{} + } + if !p.EqualVT(q) { + return false + } + } + } return string(this.unknownFields) == string(that.unknownFields) } @@ -2982,6 +3299,18 @@ func (m *CaveatDefinition) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Decorators) > 0 { + for iNdEx := len(m.Decorators) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Decorators[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x32 + } + } if m.SourcePosition != nil { size, err := m.SourcePosition.MarshalToSizedBufferVT(dAtA[:i]) if err != nil { @@ -3583,7 +3912,7 @@ func (m *Metadata) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *NamespaceDefinition) MarshalVT() (dAtA []byte, err error) { +func (m *Decorator) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil } @@ -3596,12 +3925,12 @@ func (m *NamespaceDefinition) MarshalVT() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *NamespaceDefinition) MarshalToVT(dAtA []byte) (int, error) { +func (m *Decorator) MarshalToVT(dAtA []byte) (int, error) { size := m.SizeVT() return m.MarshalToSizedBufferVT(dAtA[:size]) } -func (m *NamespaceDefinition) MarshalToSizedBufferVT(dAtA []byte) (int, error) { +func (m *Decorator) MarshalToSizedBufferVT(dAtA []byte) (int, error) { if m == nil { return 0, nil } @@ -3613,29 +3942,16 @@ func (m *NamespaceDefinition) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if m.SourcePosition != nil { - size, err := m.SourcePosition.MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x22 - } - if m.Metadata != nil { - size, err := m.Metadata.MarshalToSizedBufferVT(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + if len(m.RequiredFlag) > 0 { + i -= len(m.RequiredFlag) + copy(dAtA[i:], m.RequiredFlag) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.RequiredFlag))) i-- dAtA[i] = 0x1a } - if len(m.Relation) > 0 { - for iNdEx := len(m.Relation) - 1; iNdEx >= 0; iNdEx-- { - size, err := m.Relation[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if len(m.Parameters) > 0 { + for iNdEx := len(m.Parameters) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Parameters[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) if err != nil { return 0, err } @@ -3655,7 +3971,197 @@ func (m *NamespaceDefinition) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *Relation) MarshalVT() (dAtA []byte, err error) { +func (m *DecoratorParameter) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *DecoratorParameter) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *DecoratorParameter) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if vtmsg, ok := m.Value.(interface { + MarshalToSizedBufferVT([]byte) (int, error) + }); ok { + size, err := vtmsg.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *DecoratorParameter_IntValue) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *DecoratorParameter_IntValue) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + i := len(dAtA) + i = protohelpers.EncodeVarint(dAtA, i, uint64(m.IntValue)) + i-- + dAtA[i] = 0x10 + return len(dAtA) - i, nil +} +func (m *DecoratorParameter_StringValue) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *DecoratorParameter_StringValue) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + i := len(dAtA) + i -= len(m.StringValue) + copy(dAtA[i:], m.StringValue) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.StringValue))) + i-- + dAtA[i] = 0x1a + return len(dAtA) - i, nil +} +func (m *DecoratorParameter_BoolValue) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *DecoratorParameter_BoolValue) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + i := len(dAtA) + i-- + if m.BoolValue { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + return len(dAtA) - i, nil +} +func (m *DecoratorParameter_EnumValue) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *DecoratorParameter_EnumValue) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + i := len(dAtA) + i -= len(m.EnumValue) + copy(dAtA[i:], m.EnumValue) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.EnumValue))) + i-- + dAtA[i] = 0x2a + return len(dAtA) - i, nil +} +func (m *NamespaceDefinition) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *NamespaceDefinition) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *NamespaceDefinition) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if len(m.Decorators) > 0 { + for iNdEx := len(m.Decorators) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Decorators[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x2a + } + } + if m.SourcePosition != nil { + size, err := m.SourcePosition.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x22 + } + if m.Metadata != nil { + size, err := m.Metadata.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if len(m.Relation) > 0 { + for iNdEx := len(m.Relation) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Relation[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x12 + } + } + if len(m.Name) > 0 { + i -= len(m.Name) + copy(dAtA[i:], m.Name) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Name))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *Relation) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil } @@ -3685,6 +4191,18 @@ func (m *Relation) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Decorators) > 0 { + for iNdEx := len(m.Decorators) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Decorators[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x42 + } + } if len(m.CanonicalCacheKey) > 0 { i -= len(m.CanonicalCacheKey) copy(dAtA[i:], m.CanonicalCacheKey) @@ -4072,6 +4590,18 @@ func (m *AllowedRelation) MarshalToSizedBufferVT(dAtA []byte) (int, error) { } i -= size } + if len(m.Decorators) > 0 { + for iNdEx := len(m.Decorators) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Decorators[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x42 + } + } if m.RequiredExpiration != nil { size, err := m.RequiredExpiration.MarshalToSizedBufferVT(dAtA[:i]) if err != nil { @@ -5601,6 +6131,12 @@ func (m *CaveatDefinition) SizeVT() (n int) { l = m.SourcePosition.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Decorators) > 0 { + for _, e := range m.Decorators { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -5814,6 +6350,85 @@ func (m *Metadata) SizeVT() (n int) { return n } +func (m *Decorator) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if len(m.Parameters) > 0 { + for _, e := range m.Parameters { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + l = len(m.RequiredFlag) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + +func (m *DecoratorParameter) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Name) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if vtmsg, ok := m.Value.(interface{ SizeVT() int }); ok { + n += vtmsg.SizeVT() + } + n += len(m.unknownFields) + return n +} + +func (m *DecoratorParameter_IntValue) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 1 + protohelpers.SizeOfVarint(uint64(m.IntValue)) + return n +} +func (m *DecoratorParameter_StringValue) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.StringValue) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + return n +} +func (m *DecoratorParameter_BoolValue) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + n += 2 + return n +} +func (m *DecoratorParameter_EnumValue) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.EnumValue) + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + return n +} func (m *NamespaceDefinition) SizeVT() (n int) { if m == nil { return 0 @@ -5838,6 +6453,12 @@ func (m *NamespaceDefinition) SizeVT() (n int) { l = m.SourcePosition.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Decorators) > 0 { + for _, e := range m.Decorators { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -5876,6 +6497,12 @@ func (m *Relation) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + if len(m.Decorators) > 0 { + for _, e := range m.Decorators { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } n += len(m.unknownFields) return n } @@ -6019,7 +6646,13 @@ func (m *AllowedRelation) SizeVT() (n int) { l = m.RequiredExpiration.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } - n += len(m.unknownFields) + if len(m.Decorators) > 0 { + for _, e := range m.Decorators { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + } + n += len(m.unknownFields) return n } @@ -7409,6 +8042,40 @@ func (m *CaveatDefinition) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Decorators", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Decorators = append(m.Decorators, &Decorator{}) + if err := m.Decorators[len(m.Decorators)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -8601,6 +9268,343 @@ func (m *Metadata) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *Decorator) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Decorator: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Decorator: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Parameters", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Parameters = append(m.Parameters, &DecoratorParameter{}) + if err := m.Parameters[len(m.Parameters)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RequiredFlag", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RequiredFlag = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *DecoratorParameter) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: DecoratorParameter: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: DecoratorParameter: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Name = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IntValue", wireType) + } + var v int64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Value = &DecoratorParameter_IntValue{IntValue: v} + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StringValue", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Value = &DecoratorParameter_StringValue{StringValue: string(dAtA[iNdEx:postIndex])} + iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field BoolValue", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + b := bool(v != 0) + m.Value = &DecoratorParameter_BoolValue{BoolValue: b} + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field EnumValue", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Value = &DecoratorParameter_EnumValue{EnumValue: string(dAtA[iNdEx:postIndex])} + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *NamespaceDefinition) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -8768,6 +9772,40 @@ func (m *NamespaceDefinition) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Decorators", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Decorators = append(m.Decorators, &Decorator{}) + if err := m.Decorators[len(m.Decorators)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -9059,6 +10097,40 @@ func (m *Relation) UnmarshalVT(dAtA []byte) error { } m.CanonicalCacheKey = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Decorators", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Decorators = append(m.Decorators, &Decorator{}) + if err := m.Decorators[len(m.Decorators)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -10110,6 +11182,40 @@ func (m *AllowedRelation) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Decorators", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Decorators = append(m.Decorators, &Decorator{}) + if err := m.Decorators[len(m.Decorators)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/pkg/proto/core/v1/decorator_test.go b/pkg/proto/core/v1/decorator_test.go new file mode 100644 index 0000000000..d9ee50ac81 --- /dev/null +++ b/pkg/proto/core/v1/decorator_test.go @@ -0,0 +1,48 @@ +package corev1 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDecoratorRoundTrips(t *testing.T) { + ns := &NamespaceDefinition{ + Name: "document", + Decorators: []*Decorator{ + { + Name: "testdef", + RequiredFlag: "testdecorators", + Parameters: []*DecoratorParameter{ + {Name: "count", Value: &DecoratorParameter_IntValue{IntValue: -16}}, + {Name: "label", Value: &DecoratorParameter_StringValue{StringValue: "hi"}}, + {Name: "on", Value: &DecoratorParameter_BoolValue{BoolValue: true}}, + {Name: "mode", Value: &DecoratorParameter_EnumValue{EnumValue: "hash"}}, + }, + }, + }, + } + + encoded, err := ns.MarshalVT() + require.NoError(t, err) + + decoded := &NamespaceDefinition{} + require.NoError(t, decoded.UnmarshalVT(encoded)) + + require.Len(t, decoded.GetDecorators(), 1) + d := decoded.GetDecorators()[0] + require.Equal(t, "testdef", d.GetName()) + require.Equal(t, "testdecorators", d.GetRequiredFlag()) + require.Len(t, d.GetParameters(), 4) + require.Equal(t, int64(-16), d.GetParameters()[0].GetIntValue()) + require.Equal(t, "hi", d.GetParameters()[1].GetStringValue()) + require.True(t, d.GetParameters()[2].GetBoolValue()) + require.Equal(t, "hash", d.GetParameters()[3].GetEnumValue()) +} + +func TestDecoratorsOnAllSites(t *testing.T) { + require.Empty(t, (&NamespaceDefinition{}).GetDecorators()) + require.Empty(t, (&Relation{}).GetDecorators()) + require.Empty(t, (&CaveatDefinition{}).GetDecorators()) + require.Empty(t, (&AllowedRelation{}).GetDecorators()) +} diff --git a/pkg/schema/definition_test.go b/pkg/schema/definition_test.go index 68550a2f60..bb3bbc07a0 100644 --- a/pkg/schema/definition_test.go +++ b/pkg/schema/definition_test.go @@ -11,6 +11,7 @@ import ( ns "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/tuple" ) @@ -1551,3 +1552,54 @@ func TestTypeSystemAccessors(t *testing.T) { }) } } + +// TestHasAllowedRelationIgnoresSubjectTypeDecorators is a regression test for a bug where +// SourceForAllowedRelation briefly included decorators in its output. HasAllowedRelation's +// sole production caller (internal/relationships/validation.go, ValidateRelationshipForCreateOrTouch) +// synthesizes a fresh, decorator-free *core.AllowedRelation from the relationship being +// written -- relationship writes have no decorator syntax at all, so that synthesized value +// can never carry decorators. If SourceForAllowedRelation folded decorators into its output, +// any subject type decorated in the schema (e.g. a future `@circular` on a subject type) +// would make every valid relationship write to that subject type fail validation with +// "subjects of type `X` are not allowed on relation", because the schema's allowed-relation +// source string (decorated) would never match the write's synthesized one (undecorated). +// +// This test mirrors exactly what validation.go builds -- a plain ns.AllowedRelation with no +// decorators -- and checks it against a schema where the matching subject type carries a +// decorator, asserting the write is still considered valid. +func TestHasAllowedRelationIgnoresSubjectTypeDecorators(t *testing.T) { + schemaString := `use testdecorators + +definition user {} + +definition document { + relation viewer: @testsub user +}` + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: schemaString, + }, compiler.AllowUnprefixedObjectType(), compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + // Confirm the schema actually compiled the decorator onto the subject type, so this + // test would fail loudly (rather than vacuously pass) if that stopped being true. + doc := compiled.ObjectDefinitions[1] + require.Equal(t, "document", doc.Name) + allowed := doc.Relation[0].TypeInformation.AllowedDirectRelations + require.Len(t, allowed, 1) + require.Len(t, allowed[0].GetDecorators(), 1, "fixture must have a subject-type decorator for this regression test to be meaningful") + + ctx := t.Context() + resolver := ResolverForCompiledSchema(compiled) + ts := NewTypeSystem(resolver) + vts, err := ts.GetValidatedDefinition(ctx, "document") + require.NoError(t, err) + + // Mirrors internal/relationships/validation.go's ns.AllowedRelationWithCaveat(...) call: + // a plain, decorator-free AllowedRelation built from the relationship being written. + result, err := vts.HasAllowedRelation("viewer", ns.AllowedRelation("user", "...")) + require.NoError(t, err) + require.Equal(t, AllowedRelationValid, result, + "a relationship write to a subject type must remain valid regardless of decorators declared on that subject type in the schema") +} diff --git a/pkg/schemadsl/compiler/compiler.go b/pkg/schemadsl/compiler/compiler.go index 694e99bac3..f026278e37 100644 --- a/pkg/schemadsl/compiler/compiler.go +++ b/pkg/schemadsl/compiler/compiler.go @@ -11,8 +11,10 @@ import ( caveattypes "github.com/authzed/spicedb/pkg/caveats/types" "github.com/authzed/spicedb/pkg/genutil/mapz" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/dslshape" "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/authzed/spicedb/pkg/schemadsl/lexer" "github.com/authzed/spicedb/pkg/schemadsl/parser" ) @@ -55,10 +57,11 @@ func (cs CompiledSchema) SourcePositionToRunePosition(source input.Source, posit } type config struct { - skipValidation bool - objectTypePrefix *string - allowedFlags *mapz.Set[string] - caveatTypeSet *caveattypes.TypeSet + skipValidation bool + objectTypePrefix *string + allowedFlags *mapz.Set[string] + caveatTypeSet *caveattypes.TypeSet + decoratorRegistry decorators.Registry // In an import context, this is the FS containing // the importing schema (as opposed to imported schemas) @@ -83,6 +86,12 @@ func CaveatTypeSet(cts *caveattypes.TypeSet) Option { return func(cfg *config) { cfg.caveatTypeSet = cts } } +// WithDecoratorRegistry sets the registry used to validate decorators. Defaults to +// decorators.DefaultRegistry. +func WithDecoratorRegistry(r decorators.Registry) Option { + return func(cfg *config) { cfg.decoratorRegistry = r } +} + // Config that supplies the root source folder for compilation. Required // for relative import syntax to work properly. func SourceFolder(sourceFolder string) Option { @@ -103,8 +112,15 @@ const ( importFlag = "import" ) +// allowedFlags builds the default set of `use` flags a schema may declare from the lexer's +// own registry (lexer.AllUseFlags), rather than maintaining a second, independent list here. +// This keeps the two in sync: whatever the lexer recognizes as a flag is allowed by default, +// and callers use the Disallow* options to remove specific flags for their deployment. In a +// production binary this yields exactly the same five flags as before (expiration, self, +// typechecking, partial, import); in test binaries it additionally includes the decorators +// test flag registered by lexer/flags.go, which is required for that flag to compile at all. func allowedFlags() *mapz.Set[string] { - return mapz.NewSet(expirationFlag, selfFlag, typeCheckingFlag, partialFlag, importFlag) + return mapz.NewSet(lexer.AllUseFlags...) } func DisallowExpirationFlag() Option { @@ -119,6 +135,15 @@ func DisallowImportFlag() Option { } } +// DisallowFlags removes the named `use` flags from the set a schema may declare. +func DisallowFlags(names ...string) Option { + return func(cfg *config) { + for _, name := range names { + cfg.allowedFlags.Delete(name) + } + } +} + type Option func(*config) type ObjectPrefixOption func(*config) @@ -126,7 +151,8 @@ type ObjectPrefixOption func(*config) // Compile compilers the input schema into a set of namespace definition protos. func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*CompiledSchema, error) { cfg := &config{ - allowedFlags: allowedFlags(), + allowedFlags: allowedFlags(), + decoratorRegistry: decorators.DefaultRegistry, } prefix(cfg) // required option @@ -174,8 +200,10 @@ func Compile(schema InputSchema, prefix ObjectPrefixOption, opts ...Option) (*Co enabledFlags: mapz.NewSet[string](), existingNames: mapz.NewSet[string](), compiledPartials: initialCompiledPartials, + partialDecorators: make(map[string][]*core.Decorator), unresolvedPartials: mapz.NewMultiMap[string, *dslNode](), caveatTypeSet: caveatTypeSet, + decoratorRegistry: cfg.decoratorRegistry, }, root) if err != nil { var withNodeError withNodeError diff --git a/pkg/schemadsl/compiler/compiler_test.go b/pkg/schemadsl/compiler/compiler_test.go index c7a3e7ea52..d2c765790d 100644 --- a/pkg/schemadsl/compiler/compiler_test.go +++ b/pkg/schemadsl/compiler/compiler_test.go @@ -2,6 +2,7 @@ package compiler import ( "os" + "slices" "testing" "github.com/stretchr/testify/require" @@ -11,9 +12,12 @@ import ( "github.com/authzed/spicedb/pkg/caveats" caveattypes "github.com/authzed/spicedb/pkg/caveats/types" + "github.com/authzed/spicedb/pkg/genutil/mapz" "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/authzed/spicedb/pkg/schemadsl/lexer" "github.com/authzed/spicedb/pkg/testutil" ) @@ -1598,3 +1602,155 @@ func TestCompileWithCustomCaveatTypeSet(t *testing.T) { }, AllowUnprefixedObjectType(), CaveatTypeSet(sts.TypeSet)) require.NoError(t, err) } + +func TestDisallowFlags(t *testing.T) { + t.Parallel() + + _, err := Compile(InputSchema{ + Source: input.Source("test"), + SchemaString: "use expiration\ndefinition user {}", + }, AllowUnprefixedObjectType(), DisallowFlags("expiration")) + + require.Error(t, err) + require.ErrorContains(t, err, "the `expiration` flag is not allowed") +} + +// TestUseFlagsCollectedBeforePartials pins the ordering guarantee that `use` flags are fully +// collected into tctx.enabledFlags before collectPartials runs, which Task 7 decorators inside +// partial bodies will depend on. +// +// This drives translate() directly (rather than through the public Compile entry point) using +// a hand-built translationContext, so it can install testBeforePartialCollection and observe +// tctx.enabledFlags at the exact moment collectPartials is about to run. A black-box test +// through Compile alone cannot distinguish "flags collected before partials" from "flags +// collected at some other point before Compile returns": nothing in the current partial- +// translation call chain (collectPartials -> translatePartial -> translateRelationsAndPermissions +// -> ... -> addWithExpiration) reads tctx.enabledFlags; addWithExpiration checks +// tctx.allowedFlags (the deployment's permission list) instead, so the ordering invariant this +// test is named for would otherwise have zero coverage. +// +// Verified this fails as intended: temporarily commenting out the `collectUseFlags(tctx, root)` +// call in translate leaves enabledFlags empty at collection time (nothing else populates it, +// since the main loop's NodeTypeUseFlag case is a bare continue), and +// enabledFlagsBeforePartials below came back empty, failing the require.ElementsMatch. The +// call was restored immediately after confirming this. +func TestUseFlagsCollectedBeforePartials(t *testing.T) { + t.Parallel() + + schemaString := `use expiration +use partial + +partial base { + relation viewer: user with expiration +} + +definition user {} + +definition document { + ...base +}` + + root, mapper, err := parseSchema(InputSchema{ + Source: input.Source("test"), + SchemaString: schemaString, + }) + require.NoError(t, err) + + var enabledFlagsBeforePartials []string + + var tctx *translationContext + tctx = &translationContext{ + objectTypePrefix: new(string), // unprefixed, as with AllowUnprefixedObjectType() + mapper: mapper, + allowedFlags: mapz.NewSet("expiration", "partial"), + enabledFlags: mapz.NewSet[string](), + existingNames: mapz.NewSet[string](), + compiledPartials: make(map[string][]*core.Relation), + unresolvedPartials: mapz.NewMultiMap[string, *dslNode](), + caveatTypeSet: caveattypes.TypeSetOrDefault(nil), + testBeforePartialCollection: func() { + enabledFlagsBeforePartials = tctx.enabledFlags.AsSlice() + }, + } + + compiled, err := translate(tctx, root) + require.NoError(t, err) + require.Len(t, compiled.ObjectDefinitions, 2) + + require.ElementsMatch(t, []string{"expiration", "partial"}, enabledFlagsBeforePartials, + "use flags must already be collected by the time collectPartials runs") +} + +// TestAllLexerFlagsAllowedByDefault guards against the two flag registries +// (lexer.AllUseFlags and the compiler's default allowedFlags) drifting apart. Every flag the +// lexer recognizes via `use ` must also be accepted by a default (no Disallow* options) +// Compile call, since a flag that lexes but never compiles is unusable and would silently +// break any feature built on top of it (as `testdecorators` did before allowedFlags() was +// switched to derive from lexer.AllUseFlags instead of a hand-maintained list). +// +// `use import` is included in this loop deliberately: a bare `use import` directive with no +// accompanying `import "..."` statement never triggers the import-resolution machinery (which +// needs a configured source FS), so it exercises only the flag-allowance path being tested +// here and needs no special-casing. +func TestAllLexerFlagsAllowedByDefault(t *testing.T) { + t.Parallel() + + // Six, not just "non-empty": the five production flags (expiration, self, typechecking, + // partial, import) plus the testing.Testing()-gated decorators test flag. A single-element + // slice would satisfy a bare NotEmpty check without actually exercising every flag. + require.Len(t, lexer.AllUseFlags, 6, "expected exactly the five production flags plus the decorators test flag") + + for _, flag := range lexer.AllUseFlags { + t.Run(flag, func(t *testing.T) { + t.Parallel() + + _, err := Compile(InputSchema{ + Source: input.Source("test"), + SchemaString: "use " + flag + "\ndefinition user {}", + }, AllowUnprefixedObjectType()) + + require.NoError(t, err) + }) + } +} + +// TestProductionUseFlagsUnchanged pins the production allowed-flag set now that allowedFlags() +// derives from lexer.AllUseFlags rather than a hardcoded list (see the fix for the flag-drift +// defect above). lexer.AllUseFlags in this test binary is the five production flags plus +// decorators.TestFlag, which lexer/flags.go registers only behind a testing.Testing() guard; +// this test asserts that subtracting exactly that one known test-only flag from +// lexer.AllUseFlags yields precisely the five flags this compiler shipped with before this +// change (referencing the same named constants DisallowExpirationFlag/DisallowImportFlag use, +// so selfFlag/typeCheckingFlag/partialFlag stay load-bearing instead of becoming dead code). +// +// If the testing.Testing() guard were ever loosened or removed, or a future contributor +// registered another test-only flag without gating it, this test - being itself a test binary +// - could not observe that regression directly; TestAllLexerFlagsAllowedByDefault's exact-count +// assertion above is the guard for that case instead. Production behavior (a non-test binary +// having lexer.AllUseFlags equal to exactly the five flags below, and rejecting `use +// testdecorators`) was confirmed empirically out-of-band; see the task report. +func TestProductionUseFlagsUnchanged(t *testing.T) { + t.Parallel() + + prod := slices.DeleteFunc(slices.Clone(lexer.AllUseFlags), + func(f string) bool { return f == decorators.TestFlag }) + + require.ElementsMatch(t, + []string{expirationFlag, selfFlag, typeCheckingFlag, partialFlag, importFlag}, prod) +} + +// TestDecoratorsTestFlagAllowedByDefault specifically pins that `use testdecorators` compiles +// under a default Compile call. Task 7's decorator tests are built almost entirely on schemas +// declaring this flag, so it must remain compilable; this is called out on its own in addition +// to TestAllLexerFlagsAllowedByDefault above so a future change that removes or renames the +// flag fails with an obviously-relevant test name. +func TestDecoratorsTestFlagAllowedByDefault(t *testing.T) { + t.Parallel() + + _, err := Compile(InputSchema{ + Source: input.Source("test"), + SchemaString: "use testdecorators\ndefinition user {}", + }, AllowUnprefixedObjectType()) + + require.NoError(t, err) +} diff --git a/pkg/schemadsl/compiler/decorators.go b/pkg/schemadsl/compiler/decorators.go new file mode 100644 index 0000000000..1fe1e278be --- /dev/null +++ b/pkg/schemadsl/compiler/decorators.go @@ -0,0 +1,105 @@ +package compiler + +import ( + "google.golang.org/protobuf/proto" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" + "github.com/authzed/spicedb/pkg/schemadsl/dslshape" +) + +// translateDecorators validates and compiles the decorators attached to the given node. +func translateDecorators(tctx *translationContext, node *dslNode, site decorators.Site) ([]*core.Decorator, error) { + decoratorNodes := node.List(dslshape.NodePredicateDecorator) + if len(decoratorNodes) == 0 { + return nil, nil + } + + seen := make(map[string]struct{}, len(decoratorNodes)) + compiled := make([]*core.Decorator, 0, len(decoratorNodes)) + + for _, decoratorNode := range decoratorNodes { + applied, err := appliedDecorator(decoratorNode) + if err != nil { + return nil, err + } + + if _, found := seen[applied.Name]; found { + return nil, decoratorNode.WithSourceErrorf(applied.Name, + "decorator `@%s` specified more than once", applied.Name) + } + seen[applied.Name] = struct{}{} + + result, err := tctx.decoratorRegistry.Validate( + applied, + site, + tctx.enabledFlags.Has, + tctx.allowedFlags.Has, + ) + if err != nil { + return nil, decoratorNode.WithSourceErrorf(applied.Name, "%s", err.Error()) + } + + compiled = append(compiled, result) + } + + return compiled, nil +} + +// appliedDecorator reads a decorator AST node into its pre-validation form. +func appliedDecorator(decoratorNode *dslNode) (decorators.Applied, error) { + name, err := decoratorNode.GetString(dslshape.NodeDecoratorPredicateName) + if err != nil { + return decorators.Applied{}, err + } + + paramNodes := decoratorNode.List(dslshape.NodeDecoratorPredicateParameters) + params := make([]decorators.AppliedParameter, 0, len(paramNodes)) + + for _, paramNode := range paramNodes { + paramName, err := paramNode.GetString(dslshape.NodeDecoratorParameterPredicateName) + if err != nil { + return decorators.Applied{}, err + } + + kind, err := paramNode.GetString(dslshape.NodeDecoratorParameterPredicateKind) + if err != nil { + return decorators.Applied{}, err + } + + value, err := paramNode.GetString(dslshape.NodeDecoratorParameterPredicateValue) + if err != nil { + return decorators.Applied{}, err + } + + params = append(params, decorators.AppliedParameter{ + Name: paramName, + Value: decorators.Value{Kind: decorators.ValueKind(kind), Raw: value}, + }) + } + + return decorators.Applied{Name: name, Parameters: params}, nil +} + +// mergeDecorators appends `incoming` to `existing`, collapsing identical duplicates and +// rejecting same-name decorators whose parameters differ. +func mergeDecorators(existing []*core.Decorator, incoming []*core.Decorator, node *dslNode) ([]*core.Decorator, error) { + for _, candidate := range incoming { + duplicate := false + for _, present := range existing { + if present.GetName() != candidate.GetName() { + continue + } + if !proto.Equal(present, candidate) { + return nil, node.WithSourceErrorf(candidate.GetName(), + "decorator `@%s` is applied with conflicting parameters", candidate.GetName()) + } + duplicate = true + break + } + if !duplicate { + existing = append(existing, candidate) + } + } + return existing, nil +} diff --git a/pkg/schemadsl/compiler/decorators_test.go b/pkg/schemadsl/compiler/decorators_test.go new file mode 100644 index 0000000000..4dbbb5b793 --- /dev/null +++ b/pkg/schemadsl/compiler/decorators_test.go @@ -0,0 +1,320 @@ +package compiler_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" + "github.com/authzed/spicedb/pkg/schemadsl/input" +) + +func compileWithTestDecorators(t *testing.T, schema string) (*compiler.CompiledSchema, error) { + t.Helper() + return compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: schema, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) +} + +func TestCompileDecoratorOnDefinition(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators + +@testall(needed: 7, label: "hi", on: true, mode: hash) +definition user {}`) + require.NoError(t, err) + + def := compiled.ObjectDefinitions[0] + require.Len(t, def.GetDecorators(), 1) + + d := def.GetDecorators()[0] + require.Equal(t, "testall", d.GetName()) + require.Equal(t, decorators.TestFlag, d.GetRequiredFlag()) + require.Len(t, d.GetParameters(), 4) + require.Equal(t, int64(7), d.GetParameters()[0].GetIntValue()) + require.Equal(t, "hi", d.GetParameters()[1].GetStringValue()) + require.True(t, d.GetParameters()[2].GetBoolValue()) + require.Equal(t, "hash", d.GetParameters()[3].GetEnumValue()) +} + +func TestCompileDecoratorOnRelationAndSubjectType(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators + +definition user {} + +definition document { + @testrel + relation viewer: @testsub user + + @testrel + permission view = viewer +}`) + require.NoError(t, err) + + doc := compiled.ObjectDefinitions[1] + viewer := doc.GetRelation()[0] + require.Equal(t, "viewer", viewer.GetName()) + require.Len(t, viewer.GetDecorators(), 1) + require.Equal(t, "testrel", viewer.GetDecorators()[0].GetName()) + + allowed := viewer.GetTypeInformation().GetAllowedDirectRelations()[0] + require.Len(t, allowed.GetDecorators(), 1) + require.Equal(t, "testsub", allowed.GetDecorators()[0].GetName()) + + view := doc.GetRelation()[1] + require.Len(t, view.GetDecorators(), 1) +} + +func TestCompileDecoratorOnCaveat(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators + +@testcaveat +caveat somecaveat(someparam int) { + someparam == 42 +}`) + require.NoError(t, err) + require.Len(t, compiled.CaveatDefinitions[0].GetDecorators(), 1) +} + +func TestCompileDecoratorOnPartialAppliesToIncluders(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators +use partial + +@testdef +partial base { + relation viewer: user +} + +definition user {} + +definition document { + ...base +}`) + require.NoError(t, err) + + doc := compiled.ObjectDefinitions[1] + require.Equal(t, "document", doc.GetName()) + require.Len(t, doc.GetDecorators(), 1) + require.Equal(t, "testdef", doc.GetDecorators()[0].GetName()) +} + +func TestCompileDecoratorErrors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + schema string + expectedErr string + }{ + { + name: "unknown decorator", + schema: "use testdecorators\n\n@nope\ndefinition user {}", + expectedErr: "unknown decorator `@nope`", + }, + { + name: "missing use flag", + schema: "@testdef\ndefinition user {}", + expectedErr: "decorator `@testdef` requires `use testdecorators`", + }, + { + name: "wrong site", + schema: "use testdecorators\n\ndefinition user {}\ndefinition document {\n\t@testdef\n\trelation viewer: user\n}", + expectedErr: "decorator `@testdef` is not permitted on a relation", + }, + { + // Pins the SiteRelation/SitePermission distinction: `@testdef` is definition-only, + // so it must be rejected on a permission just as it is on a relation, above. If the + // translateDecorators call were ever hoisted into the shared + // translateRelationOrPermission dispatcher with a single hardcoded site, one of + // these two cases would start asserting the wrong error message. + name: "wrong site permission", + schema: "use testdecorators\n\ndefinition user {}\ndefinition document {\n\trelation viewer: user\n\n\t@testdef\n\tpermission view = viewer\n}", + expectedErr: "decorator `@testdef` is not permitted on a permission", + }, + { + name: "missing required parameter", + schema: "use testdecorators\n\n@testall\ndefinition user {}", + expectedErr: "missing required parameter `needed` for decorator `@testall`", + }, + { + name: "bad enum value", + schema: "use testdecorators\n\n@testall(needed: 1, mode: nope)\ndefinition user {}", + expectedErr: "invalid value `nope` for parameter `mode`", + }, + { + name: "duplicate decorator", + schema: "use testdecorators\n\n@testdef\n@testdef\ndefinition user {}", + expectedErr: "decorator `@testdef` specified more than once", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, err := compileWithTestDecorators(t, test.schema) + require.ErrorContains(t, err, test.expectedErr) + }) + } +} + +func TestCompileDecoratorRejectedByDefaultRegistry(t *testing.T) { + t.Parallel() + + // The production registry ships empty, so any decorator is unknown. + _, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: "use testdecorators\n\n@testdef\ndefinition user {}", + }, compiler.AllowUnprefixedObjectType()) + require.ErrorContains(t, err, "unknown decorator `@testdef`") +} + +// TestCompileDecoratorNestedPartialPropagation covers behavior beyond the base brief: a +// decorator declared on a partial must reach a definition even when the partial is +// included transitively, through another partial, rather than directly. `derived` is +// declared (and thus translated) after `base`, so this exercises the ordinary, +// already-resolved lookup path in translatePartialReference/translateRelationsAndPermissions +// rather than the unresolvedPartials retry path (see the sibling out-of-order test below +// for that path). +func TestCompileDecoratorNestedPartialPropagation(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators +use partial + +@testdef +partial base { + relation viewer: user +} + +partial derived { + ...base +} + +definition user {} + +definition document { + ...derived +}`) + require.NoError(t, err) + + doc := compiled.ObjectDefinitions[1] + require.Equal(t, "document", doc.GetName()) + require.Len(t, doc.GetDecorators(), 1) + require.Equal(t, "testdef", doc.GetDecorators()[0].GetName()) +} + +// TestCompileDecoratorNestedPartialPropagationOutOfOrder is identical in effect to +// TestCompileDecoratorNestedPartialPropagation above, except `derived` is declared BEFORE +// the `base` partial it references. collectPartials translates partials in declaration +// order, so translatePartial(derived) runs first, finds `base` not yet in +// tctx.compiledPartials, and defers `derived` onto tctx.unresolvedPartials keyed by +// "base". Only once translatePartial(base) later succeeds does the deferred retry for +// `derived` run (translatePartial's "waitingPartials" loop). This test pins that the +// retried translation of `derived` still merges in `base`'s decorator, not just its +// relations. +func TestCompileDecoratorNestedPartialPropagationOutOfOrder(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators +use partial + +partial derived { + ...base +} + +@testdef +partial base { + relation viewer: user +} + +definition user {} + +definition document { + ...derived +}`) + require.NoError(t, err) + + doc := compiled.ObjectDefinitions[1] + require.Equal(t, "document", doc.GetName()) + require.Len(t, doc.GetDecorators(), 1) + require.Equal(t, "testdef", doc.GetDecorators()[0].GetName()) +} + +// TestCompileDecoratorIdenticalDuplicateAcrossPartialsCollapses covers mergeDecorators' +// non-conflicting branch: two different partials, included by the same definition, each +// apply the identical (parameterless) decorator. The result must collapse to a single +// decorator rather than erroring or duplicating. +func TestCompileDecoratorIdenticalDuplicateAcrossPartialsCollapses(t *testing.T) { + t.Parallel() + + compiled, err := compileWithTestDecorators(t, `use testdecorators +use partial + +@testdef +partial base1 { + relation viewer: user +} + +@testdef +partial base2 { + relation editor: user +} + +definition user {} + +definition document { + ...base1 + ...base2 +}`) + require.NoError(t, err) + + doc := compiled.ObjectDefinitions[1] + require.Len(t, doc.GetDecorators(), 1, + "identical decorators contributed by two different partials must collapse to one") +} + +// TestCompileDecoratorConflictingParametersAcrossPartials covers mergeDecorators' error +// branch (decorators.go), which was previously unreachable by any committed test: two +// partials apply the same decorator name with different parameters, and both are included +// by the same definition. This must be rejected, and the resulting error must carry a +// source position like every other compiler error, not just a bare message. +func TestCompileDecoratorConflictingParametersAcrossPartials(t *testing.T) { + t.Parallel() + + _, err := compileWithTestDecorators(t, `use testdecorators +use partial + +@testall(needed: 1) +partial base1 { + relation viewer: user +} + +@testall(needed: 2) +partial base2 { + relation editor: user +} + +definition user {} + +definition document { + ...base1 + ...base2 +}`) + require.Error(t, err) + + var contextErr compiler.WithContextError + require.ErrorAs(t, err, &contextErr) + require.Equal(t, + "parse error in `test`, line 18, column 2: decorator `@testall` is applied with conflicting parameters", + contextErr.Error()) +} diff --git a/pkg/schemadsl/compiler/translator.go b/pkg/schemadsl/compiler/translator.go index c34d7d9e34..1dd994aaaf 100644 --- a/pkg/schemadsl/compiler/translator.go +++ b/pkg/schemadsl/compiler/translator.go @@ -18,26 +18,38 @@ import ( "github.com/authzed/spicedb/pkg/genutil/mapz" "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/dslshape" "github.com/authzed/spicedb/pkg/schemadsl/input" "github.com/authzed/spicedb/pkg/spiceerrors" ) type translationContext struct { - objectTypePrefix *string - mapper input.PositionMapper - skipValidate bool - allowedFlags *mapz.Set[string] - enabledFlags *mapz.Set[string] - existingNames *mapz.Set[string] - caveatTypeSet *caveattypes.TypeSet + objectTypePrefix *string + mapper input.PositionMapper + skipValidate bool + allowedFlags *mapz.Set[string] + enabledFlags *mapz.Set[string] + existingNames *mapz.Set[string] + caveatTypeSet *caveattypes.TypeSet + decoratorRegistry decorators.Registry // The mapping of partial name -> relations represented by the partial compiledPartials map[string][]*core.Relation + // The mapping of partial name -> decorators declared on the partial itself + partialDecorators map[string][]*core.Decorator + // A mapping of partial name -> partial DSL nodes whose resolution depends on // the resolution of the named partial unresolvedPartials *mapz.MultiMap[string, *dslNode] + + // testBeforePartialCollection, if non-nil, is invoked by translate immediately after + // `use` flags are collected but before collectPartials runs. It exists solely so tests + // can observe tctx.enabledFlags at that exact point and pin the ordering guarantee that + // flags are collected before partials are translated. It is always nil outside of tests + // and has no effect in production. + testBeforePartialCollection func() } func (tctx *translationContext) prefixedPath(definitionName string) (string, error) { @@ -68,6 +80,17 @@ func translate(tctx *translationContext, root *dslNode) (*CompiledSchema, error) // as we do our walk names := tctx.existingNames.Copy() + // Collect `use` flags first: partials are translated in a pass of their own, below, + // and decorators inside a partial body need the schema's flags to already be known. + // The parser guarantees `use` precedes every definition, so a dedicated pass is safe. + if err := collectUseFlags(tctx, root); err != nil { + return nil, err + } + + if tctx.testBeforePartialCollection != nil { + tctx.testBeforePartialCollection() + } + // Do an initial pass to translate partials and add them to the // translation context. This ensures that they're available for // subsequent reference in definition compilation. @@ -79,10 +102,7 @@ func translate(tctx *translationContext, root *dslNode) (*CompiledSchema, error) for _, topLevelNode := range root.GetChildren() { switch topLevelNode.GetType() { case dslshape.NodeTypeUseFlag: - err := translateUseFlag(tctx, topLevelNode) - if err != nil { - return nil, err - } + // Already collected by collectUseFlags, above. continue case dslshape.NodeTypeCaveatDefinition: @@ -228,6 +248,13 @@ func translateCaveatDefinition(tctx *translationContext, defNode *dslNode) (*cor def.Metadata = addComments(def.Metadata, defNode) def.SourcePosition = getSourcePosition(defNode, tctx.mapper) + + ds, err := translateDecorators(tctx, defNode, decorators.SiteCaveat) + if err != nil { + return nil, err + } + def.Decorators = ds + return def, nil } @@ -262,7 +289,17 @@ func translateObjectDefinition(tctx *translationContext, defNode *dslNode) (*cor } errorOnMissingReference := true - relationsAndPermissions, _, err := translateRelationsAndPermissions(tctx, defNode, errorOnMissingReference) + relationsAndPermissions, inheritedDecorators, _, err := translateRelationsAndPermissions(tctx, defNode, errorOnMissingReference) + if err != nil { + return nil, err + } + + ownDecorators, err := translateDecorators(tctx, defNode, decorators.SiteDefinition) + if err != nil { + return nil, err + } + + ds, err := mergeDecorators(ownDecorators, inheritedDecorators, defNode) if err != nil { return nil, err } @@ -276,6 +313,7 @@ func translateObjectDefinition(tctx *translationContext, defNode *dslNode) (*cor ns := namespace.Namespace(nspath) ns.Metadata = addComments(ns.Metadata, defNode) ns.SourcePosition = getSourcePosition(defNode, tctx.mapper) + ns.Decorators = ds if !tctx.skipValidate { if err = protovalidate.Validate(ns); err != nil { @@ -289,6 +327,7 @@ func translateObjectDefinition(tctx *translationContext, defNode *dslNode) (*cor ns := namespace.Namespace(nspath, relationsAndPermissions...) ns.Metadata = addComments(ns.Metadata, defNode) ns.SourcePosition = getSourcePosition(defNode, tctx.mapper) + ns.Decorators = ds if !tctx.skipValidate { if err := protovalidate.Validate(ns); err != nil { @@ -303,33 +342,39 @@ func translateObjectDefinition(tctx *translationContext, defNode *dslNode) (*cor // A value of true treats that as an error state, since all partials should be resolved when translating definitions, // where the false value returns the name of the partial for collection for future processing // when translating partials. -func translateRelationsAndPermissions(tctx *translationContext, astNode *dslNode, errorOnMissingReference bool) ([]*core.Relation, string, error) { +func translateRelationsAndPermissions(tctx *translationContext, astNode *dslNode, errorOnMissingReference bool) ([]*core.Relation, []*core.Decorator, string, error) { relationsAndPermissions := []*core.Relation{} + var inheritedDecorators []*core.Decorator for _, definitionChildNode := range astNode.GetChildren() { if definitionChildNode.GetType() == dslshape.NodeTypeComment { continue } if definitionChildNode.GetType() == dslshape.NodeTypePartialReference { - partialContents, unresolvedPartial, err := translatePartialReference(tctx, definitionChildNode, errorOnMissingReference) + partialContents, partialDecorators, unresolvedPartial, err := translatePartialReference(tctx, definitionChildNode, errorOnMissingReference) if err != nil { - return nil, "", err + return nil, nil, "", err } if unresolvedPartial != "" { - return nil, unresolvedPartial, nil + return nil, nil, unresolvedPartial, nil } relationsAndPermissions = append(relationsAndPermissions, partialContents...) + + inheritedDecorators, err = mergeDecorators(inheritedDecorators, partialDecorators, definitionChildNode) + if err != nil { + return nil, nil, "", err + } continue } relationOrPermission, err := translateRelationOrPermission(tctx, definitionChildNode) if err != nil { - return nil, "", err + return nil, nil, "", err } relationsAndPermissions = append(relationsAndPermissions, relationOrPermission) } - return relationsAndPermissions, "", nil + return relationsAndPermissions, inheritedDecorators, "", nil } func getSourcePosition(dslNode *dslNode, mapper input.PositionMapper) *core.SourcePosition { @@ -430,6 +475,12 @@ func translateRelation(tctx *translationContext, relationNode *dslNode) (*core.R } } + ds, err := translateDecorators(tctx, relationNode, decorators.SiteRelation) + if err != nil { + return nil, err + } + relation.Decorators = ds + return relation, nil } @@ -479,6 +530,12 @@ func translatePermission(tctx *translationContext, permissionNode *dslNode) (*co } } + ds, err := translateDecorators(tctx, permissionNode, decorators.SitePermission) + if err != nil { + return nil, err + } + permission.Decorators = ds + return permission, nil } @@ -753,6 +810,13 @@ func translateSpecificTypeReference(tctx *translationContext, typeRefNode *dslNo } ref.SourcePosition = getSourcePosition(typeRefNode, tctx.mapper) + + ds, err := translateDecorators(tctx, typeRefNode, decorators.SiteSubjectType) + if err != nil { + return nil, err + } + ref.Decorators = ds + return ref, nil } @@ -914,6 +978,20 @@ func (itctx *importResolutionContext) translateImports(root *dslNode, locallyVis return nil } +// collectUseFlags walks the top level and records every declared `use` flag. +func collectUseFlags(tctx *translationContext, rootNode *dslNode) error { + for _, topLevelNode := range rootNode.GetChildren() { + if topLevelNode.GetType() != dslshape.NodeTypeUseFlag { + continue + } + + if err := translateUseFlag(tctx, topLevelNode); err != nil { + return err + } + } + return nil +} + func collectPartials(tctx *translationContext, rootNode *dslNode) error { for _, topLevelNode := range rootNode.GetChildren() { if topLevelNode.GetType() == dslshape.NodeTypePartial { @@ -942,7 +1020,7 @@ func translatePartial(tctx *translationContext, partialNode *dslNode) error { } // This needs to return the unresolved name. errorOnMissingReference := false - relationsAndPermissions, unresolvedPartial, err := translateRelationsAndPermissions(tctx, partialNode, errorOnMissingReference) + relationsAndPermissions, inheritedDecorators, unresolvedPartial, err := translateRelationsAndPermissions(tctx, partialNode, errorOnMissingReference) if err != nil { return err } @@ -951,8 +1029,25 @@ func translatePartial(tctx *translationContext, partialNode *dslNode) error { return nil } + // A decorator on a partial applies at SiteDefinition: it is legal here exactly when + // it would be legal on the definitions that end up including this partial. + ownDecorators, err := translateDecorators(tctx, partialNode, decorators.SiteDefinition) + if err != nil { + return err + } + + mergedDecorators, err := mergeDecorators(ownDecorators, inheritedDecorators, partialNode) + if err != nil { + return err + } + tctx.compiledPartials[partialPath] = relationsAndPermissions + if tctx.partialDecorators == nil { + tctx.partialDecorators = make(map[string][]*core.Decorator) + } + tctx.partialDecorators[partialPath] = mergedDecorators + // Since we've successfully compiled a partial, check the unresolved partials to see if any other partial was // waiting on this partial // NOTE: we're making an assumption here that a partial can't end up back in the same @@ -973,25 +1068,25 @@ func translatePartial(tctx *translationContext, partialNode *dslNode) error { // NOTE: we treat partial references in definitions and partials differently because a missing partial // reference in definition compilation is an error state, where a missing partial reference in a // partial definition is an indeterminate state. -func translatePartialReference(tctx *translationContext, partialReferenceNode *dslNode, errorOnMissingReference bool) ([]*core.Relation, string, error) { +func translatePartialReference(tctx *translationContext, partialReferenceNode *dslNode, errorOnMissingReference bool) ([]*core.Relation, []*core.Decorator, string, error) { name, err := partialReferenceNode.GetString(dslshape.NodePartialReferencePredicateName) if err != nil { - return nil, "", err + return nil, nil, "", err } path, err := tctx.prefixedPath(name) if err != nil { - return nil, "", err + return nil, nil, "", err } relationsAndPermissions, ok := tctx.compiledPartials[path] if !ok { if errorOnMissingReference { - return nil, "", partialReferenceNode.Errorf("could not find partial reference with name %s", path) + return nil, nil, "", partialReferenceNode.Errorf("could not find partial reference with name %s", path) } // If the partial isn't present and we're not throwing an error, we return the name of the missing partial // This behavior supports partial collection - return nil, path, nil + return nil, nil, path, nil } - return relationsAndPermissions, "", nil + return relationsAndPermissions, tctx.partialDecorators[path], "", nil } // Translate use node and add flag to list of enabled flags @@ -1000,12 +1095,14 @@ func translateUseFlag(tctx *translationContext, useFlagNode *dslNode) error { if err != nil { return err } + + if !tctx.allowedFlags.Has(flagName) { + return useFlagNode.WithSourceErrorf(flagName, "the `%s` flag is not allowed", flagName) + } + // NOTE: we're okay with multiple instances of a given `use` directive in // composable schemas, because each file may declare it separately // and that should be valid. - - // TODO: make this check the list of allowed flags. this will be required for - // `use import` support. tctx.enabledFlags.Insert(flagName) return nil } diff --git a/pkg/schemadsl/decorators/decorators.go b/pkg/schemadsl/decorators/decorators.go new file mode 100644 index 0000000000..0cfc31227c --- /dev/null +++ b/pkg/schemadsl/decorators/decorators.go @@ -0,0 +1,101 @@ +// Package decorators defines the registry of schema decorators: which decorators +// exist, where each may be applied, what parameters each takes, and which `use` +// feature flag enables it. +package decorators + +import "slices" + +// Site is a location in a schema at which a decorator may be applied. +type Site string + +const ( + SiteDefinition Site = "definition" + SiteRelation Site = "relation" + SitePermission Site = "permission" + SiteSubjectType Site = "subject type" + SiteCaveat Site = "caveat" +) + +// ParamType is the declared type of a decorator parameter. +type ParamType string + +const ( + ParamTypeInt ParamType = "int" + ParamTypeString ParamType = "string" + ParamTypeBool ParamType = "bool" + ParamTypeEnum ParamType = "enum" +) + +// Parameter declares a single named parameter of a decorator. +type Parameter struct { + // Name is the parameter's name, as written in `@decorator(name: value)`. + Name string + + // Type is the parameter's declared type. + Type ParamType + + // Required indicates the parameter must be supplied. + Required bool + + // EnumValues is the set of legal values; ParamTypeEnum only. + EnumValues []string +} + +// Spec declares a single decorator. +type Spec struct { + // Name is the decorator's name, without the leading `@`. + Name string + + // RequiredFlag is the `use` feature flag that enables this decorator. Several + // decorators may share one flag. + RequiredFlag string + + // Sites are the locations at which this decorator may be applied. A decorator + // listing SiteDefinition may also be applied to a `partial`, in which case it + // applies to every definition including that partial. + Sites []Site + + // Parameters are the decorator's parameters, in canonical order. + Parameters []Parameter +} + +// AllowsSite returns whether this decorator may be applied at the given site. +func (s Spec) AllowsSite(site Site) bool { + return slices.Contains(s.Sites, site) +} + +// Parameter returns the named parameter's declaration, if it exists. +func (s Spec) Parameter(name string) (Parameter, bool) { + for _, param := range s.Parameters { + if param.Name == name { + return param, true + } + } + return Parameter{}, false +} + +// ParameterNames returns the names of all declared parameters, in canonical order. +func (s Spec) ParameterNames() []string { + names := make([]string, 0, len(s.Parameters)) + for _, param := range s.Parameters { + names = append(names, param.Name) + } + return names +} + +// Registry is the set of known decorators, keyed by name. +type Registry map[string]Spec + +// Names returns all registered decorator names, sorted. +func (r Registry) Names() []string { + names := make([]string, 0, len(r)) + for name := range r { + names = append(names, name) + } + slices.Sort(names) + return names +} + +// DefaultRegistry is the registry used in production. It is intentionally empty: +// the decorator machinery ships before any decorator does. +var DefaultRegistry = Registry{} diff --git a/pkg/schemadsl/decorators/testregistry.go b/pkg/schemadsl/decorators/testregistry.go new file mode 100644 index 0000000000..2112376e69 --- /dev/null +++ b/pkg/schemadsl/decorators/testregistry.go @@ -0,0 +1,46 @@ +package decorators + +// TestFlag is the `use` feature flag that enables the decorators in TestRegistry. +// It is registered as a valid flag only in test binaries; see pkg/schemadsl/lexer/flags.go, +// which imports this constant so the name is declared exactly once. +const TestFlag = "testdecorators" + +// TestRegistry is a fixture registry used to exercise the decorator machinery. It +// deliberately covers combinations no real decorator is expected to have. +// +// It is never used in production: the compiler defaults to DefaultRegistry, and tests +// opt in via compiler.WithDecoratorRegistry. +var TestRegistry = Registry{ + "testdef": { + Name: "testdef", + RequiredFlag: TestFlag, + Sites: []Site{SiteDefinition}, + }, + "testrel": { + Name: "testrel", + RequiredFlag: TestFlag, + Sites: []Site{SiteRelation, SitePermission}, + }, + "testsub": { + Name: "testsub", + RequiredFlag: TestFlag, + Sites: []Site{SiteSubjectType}, + }, + "testcaveat": { + Name: "testcaveat", + RequiredFlag: TestFlag, + Sites: []Site{SiteCaveat}, + }, + "testall": { + Name: "testall", + RequiredFlag: TestFlag, + Sites: []Site{SiteDefinition, SiteRelation, SitePermission, SiteSubjectType, SiteCaveat}, + Parameters: []Parameter{ + {Name: "needed", Type: ParamTypeInt, Required: true}, + {Name: "count", Type: ParamTypeInt}, + {Name: "label", Type: ParamTypeString}, + {Name: "on", Type: ParamTypeBool}, + {Name: "mode", Type: ParamTypeEnum, EnumValues: []string{"hash", "range"}}, + }, + }, +} diff --git a/pkg/schemadsl/decorators/validate.go b/pkg/schemadsl/decorators/validate.go new file mode 100644 index 0000000000..85a6403aeb --- /dev/null +++ b/pkg/schemadsl/decorators/validate.go @@ -0,0 +1,198 @@ +package decorators + +import ( + "fmt" + "slices" + "strconv" + "strings" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// ValueKind is the syntactic kind of a decorator parameter value, as parsed. +// +// NOTE: there is no distinct number kind. The lexer never emits TokenTypeNumber +// (isAlphaNumeric includes digits), so `16` arrives as an identifier. The declared +// parameter type is what decides how a value is coerced. +type ValueKind string + +const ( + // ValueKindString is a quote-delimited string. + ValueKindString ValueKind = "string" + + // ValueKindIdentifier is a bare token: a number, `true`/`false`, or an enum value. + ValueKindIdentifier ValueKind = "identifier" +) + +// Value is a decorator parameter value as parsed: for a string value, the delimiting +// quote characters have already been stripped by the parser's tryConsumeStringLiteral, +// so Raw holds the string's contents, not its literal source text. +type Value struct { + Kind ValueKind + Raw string +} + +// AppliedParameter is one `name: value` argument as written in a schema. +type AppliedParameter struct { + Name string + Value Value +} + +// Applied is a decorator as written in a schema, before validation. +type Applied struct { + Name string + Parameters []AppliedParameter +} + +// Validate checks an applied decorator against the registry and, on success, returns +// its compiled proto form. +// +// flagEnabled reports whether the schema declared `use `; flagAllowed reports +// whether the deployment permits that flag. +func (r Registry) Validate( + applied Applied, + site Site, + flagEnabled func(string) bool, + flagAllowed func(string) bool, +) (*core.Decorator, error) { + spec, ok := r[applied.Name] + if !ok { + if len(r) == 0 { + return nil, fmt.Errorf("unknown decorator `@%s`", applied.Name) + } + return nil, fmt.Errorf("unknown decorator `@%s`. Options are: %s", + applied.Name, strings.Join(r.Names(), ", ")) + } + + if !flagEnabled(spec.RequiredFlag) { + return nil, fmt.Errorf("decorator `@%s` requires `use %s`", applied.Name, spec.RequiredFlag) + } + + if !flagAllowed(spec.RequiredFlag) { + return nil, fmt.Errorf("the `%s` flag is not allowed", spec.RequiredFlag) + } + + if !spec.AllowsSite(site) { + return nil, fmt.Errorf("decorator `@%s` is not permitted on a %s", applied.Name, site) + } + + seen := make(map[string]struct{}, len(applied.Parameters)) + byName := make(map[string]Value, len(applied.Parameters)) + + for _, param := range applied.Parameters { + if _, found := seen[param.Name]; found { + return nil, fmt.Errorf("parameter `%s` specified more than once for decorator `@%s`", + param.Name, applied.Name) + } + seen[param.Name] = struct{}{} + + if _, found := spec.Parameter(param.Name); !found { + return nil, fmt.Errorf("unknown parameter `%s` for decorator `@%s`. Options are: %s", + param.Name, applied.Name, strings.Join(spec.ParameterNames(), ", ")) + } + + byName[param.Name] = param.Value + } + + // Emit parameters in the spec's canonical order so that generated schemas are stable. + compiled := make([]*core.DecoratorParameter, 0, len(applied.Parameters)) + for _, declared := range spec.Parameters { + value, found := byName[declared.Name] + if !found { + if declared.Required { + return nil, fmt.Errorf("missing required parameter `%s` for decorator `@%s`", + declared.Name, applied.Name) + } + continue + } + + converted, err := coerce(applied.Name, declared, value) + if err != nil { + return nil, err + } + + compiled = append(compiled, converted) + } + + return &core.Decorator{ + Name: applied.Name, + RequiredFlag: spec.RequiredFlag, + Parameters: compiled, + }, nil +} + +func coerce(decoratorName string, declared Parameter, value Value) (*core.DecoratorParameter, error) { + switch declared.Type { + case ParamTypeInt: + if value.Kind != ValueKindIdentifier { + return nil, intError(decoratorName, declared) + } + parsed, err := strconv.ParseInt(value.Raw, 10, 64) + if err != nil { + return nil, intError(decoratorName, declared) + } + return &core.DecoratorParameter{ + Name: declared.Name, + Value: &core.DecoratorParameter_IntValue{IntValue: parsed}, + }, nil + + case ParamTypeString: + if value.Kind != ValueKindString { + return nil, fmt.Errorf("parameter `%s` of decorator `@%s` expects a quoted string", + declared.Name, decoratorName) + } + + // The schema DSL has no backslash-escape syntax anywhere: the lexer's string + // scanner reads raw characters up to the closing quote, and the parser just + // trims the quote characters off the token. That means a string value can only + // be regenerated as valid schema source if a delimiter exists that it does not + // contain (the generator picks `"` unless the value contains one, then falls + // back to `'`), and it cannot span multiple lines. Reject values that violate + // this here, at the source position, rather than let them reach the proto and + // silently corrupt the generator's output (and, transitively, the schema hash). + // Do NOT "fix" this by adding escape support here without also teaching the + // lexer (lexStringLiteral) and parser (tryConsumeStringLiteral) to understand it. + if strings.ContainsRune(value.Raw, '"') && strings.ContainsRune(value.Raw, '\'') { + return nil, fmt.Errorf("parameter `%s` of decorator `@%s` contains characters that cannot be represented in a schema string: a value may not contain both quote styles", + declared.Name, decoratorName) + } + if strings.ContainsAny(value.Raw, "\n\r") { + return nil, fmt.Errorf("parameter `%s` of decorator `@%s` may not contain a newline", + declared.Name, decoratorName) + } + + return &core.DecoratorParameter{ + Name: declared.Name, + Value: &core.DecoratorParameter_StringValue{StringValue: value.Raw}, + }, nil + + case ParamTypeBool: + if value.Kind != ValueKindIdentifier || (value.Raw != "true" && value.Raw != "false") { + return nil, fmt.Errorf("parameter `%s` of decorator `@%s` expects true or false", + declared.Name, decoratorName) + } + return &core.DecoratorParameter{ + Name: declared.Name, + Value: &core.DecoratorParameter_BoolValue{BoolValue: value.Raw == "true"}, + }, nil + + case ParamTypeEnum: + if value.Kind != ValueKindIdentifier || !slices.Contains(declared.EnumValues, value.Raw) { + return nil, fmt.Errorf("invalid value `%s` for parameter `%s` of decorator `@%s`; expected one of: %s", + value.Raw, declared.Name, decoratorName, strings.Join(declared.EnumValues, ", ")) + } + return &core.DecoratorParameter{ + Name: declared.Name, + Value: &core.DecoratorParameter_EnumValue{EnumValue: value.Raw}, + }, nil + + default: + return nil, fmt.Errorf("decorator `@%s` declares parameter `%s` with unknown type `%s`", + decoratorName, declared.Name, declared.Type) + } +} + +func intError(decoratorName string, declared Parameter) error { + return fmt.Errorf("parameter `%s` of decorator `@%s` expects an integer", + declared.Name, decoratorName) +} diff --git a/pkg/schemadsl/decorators/validate_test.go b/pkg/schemadsl/decorators/validate_test.go new file mode 100644 index 0000000000..4e213fae42 --- /dev/null +++ b/pkg/schemadsl/decorators/validate_test.go @@ -0,0 +1,248 @@ +package decorators + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func allowAll(string) bool { return true } +func denyAll(string) bool { return false } + +func ident(v string) Value { return Value{Kind: ValueKindIdentifier, Raw: v} } +func str(v string) Value { return Value{Kind: ValueKindString, Raw: v} } + +func TestValidate(t *testing.T) { + tests := []struct { + name string + applied Applied + site Site + flagEnabled func(string) bool + expectedErr string + }{ + { + name: "no parameters", + applied: Applied{Name: "testdef"}, + site: SiteDefinition, + flagEnabled: allowAll, + }, + { + name: "all parameter types", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "count", Value: ident("-16")}, + {Name: "label", Value: str("hi")}, + {Name: "on", Value: ident("true")}, + {Name: "mode", Value: ident("hash")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + }, + { + name: "unknown decorator", + applied: Applied{Name: "nope"}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "unknown decorator `@nope`", + }, + { + name: "flag not enabled", + applied: Applied{Name: "testdef"}, + site: SiteDefinition, + flagEnabled: denyAll, + expectedErr: "decorator `@testdef` requires `use " + TestFlag + "`", + }, + { + name: "illegal site", + applied: Applied{Name: "testdef"}, + site: SiteRelation, + flagEnabled: allowAll, + expectedErr: "decorator `@testdef` is not permitted on a relation", + }, + { + name: "unknown parameter", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "bogus", Value: ident("1")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "unknown parameter `bogus` for decorator `@testall`", + }, + { + name: "duplicate parameter", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "needed", Value: ident("2")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `needed` specified more than once", + }, + { + name: "missing required parameter", + applied: Applied{Name: "testall"}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "missing required parameter `needed` for decorator `@testall`", + }, + { + name: "int given a string", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: str("1")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `needed` of decorator `@testall` expects an integer", + }, + { + name: "int given a non-number", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("hash")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `needed` of decorator `@testall` expects an integer", + }, + { + name: "string given an identifier", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "label", Value: ident("hi")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `label` of decorator `@testall` expects a quoted string", + }, + { + name: "bool given a non-bool", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "on", Value: ident("yes")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `on` of decorator `@testall` expects true or false", + }, + { + name: "enum out of range", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "mode", Value: ident("nope")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "invalid value `nope` for parameter `mode` of decorator `@testall`; expected one of: hash, range", + }, + { + // The generator can only regenerate a string value using a delimiter (` or ") + // the value does not contain, since the DSL has no escape syntax. A value + // containing both is unrepresentable, so it must be rejected here rather than + // silently corrupting generated output. + name: "string containing both quote styles", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "label", Value: str(`he said "it's mine"`)}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `label` of decorator `@testall` contains characters that cannot be represented in a schema string: a value may not contain both quote styles", + }, + { + // Single/double-quoted strings in the DSL are single-line only, so a value + // containing a real newline cannot be regenerated as valid schema source. + name: "string containing a newline", + applied: Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "label", Value: str("line one\nline two")}, + }}, + site: SiteDefinition, + flagEnabled: allowAll, + expectedErr: "parameter `label` of decorator `@testall` may not contain a newline", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + result, err := TestRegistry.Validate(test.applied, test.site, test.flagEnabled, allowAll) + if test.expectedErr != "" { + require.ErrorContains(t, err, test.expectedErr) + return + } + require.NoError(t, err) + require.Equal(t, test.applied.Name, result.GetName()) + require.Equal(t, TestFlag, result.GetRequiredFlag()) + }) + } +} + +func TestValidateFlagNotAllowedByDeployment(t *testing.T) { + t.Parallel() + _, err := TestRegistry.Validate(Applied{Name: "testdef"}, SiteDefinition, allowAll, denyAll) + require.ErrorContains(t, err, "the `"+TestFlag+"` flag is not allowed") +} + +func TestValidateCoercesValues(t *testing.T) { + t.Parallel() + result, err := TestRegistry.Validate(Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("7")}, + {Name: "count", Value: ident("-16")}, + {Name: "label", Value: str("hi")}, + {Name: "on", Value: ident("true")}, + {Name: "mode", Value: ident("hash")}, + }}, SiteDefinition, allowAll, allowAll) + require.NoError(t, err) + + params := result.GetParameters() + require.Len(t, params, 5) + require.Equal(t, int64(7), params[0].GetIntValue()) + require.Equal(t, int64(-16), params[1].GetIntValue()) + require.Equal(t, "hi", params[2].GetStringValue()) + require.True(t, params[3].GetBoolValue()) + require.Equal(t, "hash", params[4].GetEnumValue()) +} + +// TestValidateEmitsSpecOrderRegardlessOfSourceOrder supplies parameters in an order that +// does NOT match the spec's canonical order (mode, label, needed instead of needed, ..., +// mode). If Validate ever emitted parameters in source order instead of spec order, this +// test would catch it: the parameter names, not just their values, are asserted per index. +func TestValidateEmitsSpecOrderRegardlessOfSourceOrder(t *testing.T) { + t.Parallel() + result, err := TestRegistry.Validate(Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "mode", Value: ident("hash")}, + {Name: "label", Value: str("hi")}, + {Name: "needed", Value: ident("1")}, + }}, SiteDefinition, allowAll, allowAll) + require.NoError(t, err) + + params := result.GetParameters() + require.Len(t, params, 3) + + require.Equal(t, "needed", params[0].GetName()) + require.Equal(t, int64(1), params[0].GetIntValue()) + + require.Equal(t, "label", params[1].GetName()) + require.Equal(t, "hi", params[1].GetStringValue()) + + require.Equal(t, "mode", params[2].GetName()) + require.Equal(t, "hash", params[2].GetEnumValue()) +} + +// TestValidateSkipsOmittedOptionalParameters pins the `continue` branch that skips an +// absent, non-required parameter: only `needed` and `mode` are supplied, so the compiled +// output must contain exactly those two, in spec order, with nothing emitted for the +// omitted `count`, `label`, and `on`. +func TestValidateSkipsOmittedOptionalParameters(t *testing.T) { + t.Parallel() + result, err := TestRegistry.Validate(Applied{Name: "testall", Parameters: []AppliedParameter{ + {Name: "needed", Value: ident("1")}, + {Name: "mode", Value: ident("hash")}, + }}, SiteDefinition, allowAll, allowAll) + require.NoError(t, err) + + params := result.GetParameters() + require.Len(t, params, 2) + require.Equal(t, "needed", params[0].GetName()) + require.Equal(t, "mode", params[1].GetName()) +} diff --git a/pkg/schemadsl/dslshape/dslshape.go b/pkg/schemadsl/dslshape/dslshape.go index 8989dfbedb..7b6a15d7ef 100644 --- a/pkg/schemadsl/dslshape/dslshape.go +++ b/pkg/schemadsl/dslshape/dslshape.go @@ -39,6 +39,9 @@ const ( NodeTypeCaveatTypeReference // A type reference for a caveat parameter. + NodeTypeDecorator // A decorator, e.g. @somename(param: value) + NodeTypeDecoratorParameter // A single parameter of a decorator + NodeTypeImport NodeTypePartial NodeTypePartialReference // A location where a partial is referenced @@ -237,4 +240,30 @@ const ( // NodeTypePartialReference // NodePartialReferencePredicateName = "partial-reference-name" + + // + // NodeTypeDecorator + // + + // A decorator applied to the decorated node. + NodePredicateDecorator = "decorator" + + // The name of the decorator, without the leading `@`. + NodeDecoratorPredicateName = "decorator-name" + + // The parameters of the decorator. + NodeDecoratorPredicateParameters = "decorator-parameters" + + // + // NodeTypeDecoratorParameter + // + + // The name of the decorator parameter. + NodeDecoratorParameterPredicateName = "decorator-parameter-name" + + // The value of the decorator parameter, as written. + NodeDecoratorParameterPredicateValue = "decorator-parameter-value" + + // The syntactic kind of the value: `string` or `identifier`. + NodeDecoratorParameterPredicateKind = "decorator-parameter-kind" ) diff --git a/pkg/schemadsl/dslshape/zz_generated.nodetype_string.go b/pkg/schemadsl/dslshape/zz_generated.nodetype_string.go index 25502ede98..54bf4b0efe 100644 --- a/pkg/schemadsl/dslshape/zz_generated.nodetype_string.go +++ b/pkg/schemadsl/dslshape/zz_generated.nodetype_string.go @@ -31,14 +31,16 @@ func _() { _ = x[NodeTypeNilExpression-20] _ = x[NodeTypeSelfExpression-21] _ = x[NodeTypeCaveatTypeReference-22] - _ = x[NodeTypeImport-23] - _ = x[NodeTypePartial-24] - _ = x[NodeTypePartialReference-25] + _ = x[NodeTypeDecorator-23] + _ = x[NodeTypeDecoratorParameter-24] + _ = x[NodeTypeImport-25] + _ = x[NodeTypePartial-26] + _ = x[NodeTypePartialReference-27] } -const _NodeType_name = "NodeTypeErrorNodeTypeFileNodeTypeCommentNodeTypeUseFlagNodeTypeDefinitionNodeTypeCaveatDefinitionNodeTypeCaveatParameterNodeTypeCaveatExpressionNodeTypeRelationNodeTypePermissionNodeTypeTypeAnnotationNodeTypeTypeReferenceNodeTypeSpecificTypeReferenceNodeTypeCaveatReferenceNodeTypeTraitReferenceNodeTypeUnionExpressionNodeTypeIntersectExpressionNodeTypeExclusionExpressionNodeTypeArrowExpressionNodeTypeIdentifierNodeTypeNilExpressionNodeTypeSelfExpressionNodeTypeCaveatTypeReferenceNodeTypeImportNodeTypePartialNodeTypePartialReference" +const _NodeType_name = "NodeTypeErrorNodeTypeFileNodeTypeCommentNodeTypeUseFlagNodeTypeDefinitionNodeTypeCaveatDefinitionNodeTypeCaveatParameterNodeTypeCaveatExpressionNodeTypeRelationNodeTypePermissionNodeTypeTypeAnnotationNodeTypeTypeReferenceNodeTypeSpecificTypeReferenceNodeTypeCaveatReferenceNodeTypeTraitReferenceNodeTypeUnionExpressionNodeTypeIntersectExpressionNodeTypeExclusionExpressionNodeTypeArrowExpressionNodeTypeIdentifierNodeTypeNilExpressionNodeTypeSelfExpressionNodeTypeCaveatTypeReferenceNodeTypeDecoratorNodeTypeDecoratorParameterNodeTypeImportNodeTypePartialNodeTypePartialReference" -var _NodeType_index = [...]uint16{0, 13, 25, 40, 55, 73, 97, 120, 144, 160, 178, 200, 221, 250, 273, 295, 318, 345, 372, 395, 413, 434, 456, 483, 497, 512, 536} +var _NodeType_index = [...]uint16{0, 13, 25, 40, 55, 73, 97, 120, 144, 160, 178, 200, 221, 250, 273, 295, 318, 345, 372, 395, 413, 434, 456, 483, 500, 526, 540, 555, 579} func (i NodeType) String() string { idx := int(i) - 0 diff --git a/pkg/schemadsl/generator/generator.go b/pkg/schemadsl/generator/generator.go index 7b74004a43..1004fb192c 100644 --- a/pkg/schemadsl/generator/generator.go +++ b/pkg/schemadsl/generator/generator.go @@ -9,6 +9,7 @@ import ( "maps" "slices" "sort" + "strconv" "strings" "go.opentelemetry.io/otel" @@ -71,13 +72,14 @@ func GenerateSchemaWithCaveatTypeSet(ctx context.Context, definitions []compiler for _, definition := range definitions { switch def := definition.(type) { case *core.CaveatDefinition: - generatedCaveat, ok, err := GenerateCaveatSource(def, caveatTypeSet) + generatedCaveat, caveatFlags, ok, err := GenerateCaveatSource(def, caveatTypeSet) if err != nil { return "", false, err } result = result && ok generated = append(generated, generatedCaveat) + flags.Extend(caveatFlags) case *core.NamespaceDefinition: generatedSchema, defFlags, ok, err := generateDefinitionSource(def, caveatTypeSet) @@ -107,15 +109,15 @@ func GenerateSchemaWithCaveatTypeSet(ctx context.Context, definitions []compiler } // GenerateCaveatSource generates a DSL view of the given caveat definition. -func GenerateCaveatSource(caveat *core.CaveatDefinition, caveatTypeSet *caveattypes.TypeSet) (string, bool, error) { +func GenerateCaveatSource(caveat *core.CaveatDefinition, caveatTypeSet *caveattypes.TypeSet) (string, []string, bool, error) { generator := NewSourceGenerator(caveatTypeSet) err := generator.emitCaveat(caveat) if err != nil { - return "", false, err + return "", nil, false, err } - return generator.buf.String(), !generator.hasIssue, nil + return generator.buf.String(), generator.flags.AsSlice(), !generator.hasIssue, nil } // GenerateSource generates a DSL view of the given namespace definition. @@ -147,8 +149,100 @@ func GenerateRelationSource(relation *core.Relation, caveatTypeSet *caveattypes. return generator.buf.String(), nil } +// emitDecorators writes the given decorators and records the `use` flags they require. +// When inline is true they are written space-separated on the current line (subject +// types); otherwise each is written on its own line (definitions, relations, caveats). +func (sg *sourceGenerator) emitDecorators(ds []*core.Decorator, inline bool) { + for _, d := range ds { + if flag := d.GetRequiredFlag(); flag != "" { + sg.flags.Add(flag) + } + + sg.append("@") + sg.append(d.GetName()) + + if len(d.GetParameters()) > 0 { + sg.append("(") + for index, param := range d.GetParameters() { + if index > 0 { + sg.append(", ") + } + sg.append(param.GetName()) + sg.append(": ") + sg.append(sg.decoratorParameterValue(param)) + } + sg.append(")") + } + + if inline { + sg.append(" ") + } else { + sg.appendLine() + } + } +} + +// decoratorParameterValue renders a single decorator parameter's value as it should +// appear in generated source. If the value cannot be represented at all (a oneof left +// unset, e.g. on a hand-built proto that bypassed decorators.Validate), it records an +// issue via appendIssue so the caller's `ok` return flips to false instead of emitting +// text that cannot be reparsed. +func (sg *sourceGenerator) decoratorParameterValue(param *core.DecoratorParameter) string { + switch value := param.GetValue().(type) { + case *core.DecoratorParameter_IntValue: + return strconv.FormatInt(value.IntValue, 10) + + case *core.DecoratorParameter_StringValue: + return sg.decoratorStringValue(param.GetName(), value.StringValue) + + case *core.DecoratorParameter_BoolValue: + return strconv.FormatBool(value.BoolValue) + + case *core.DecoratorParameter_EnumValue: + return value.EnumValue + + default: + sg.appendIssue(fmt.Sprintf("decorator parameter `%s` has no value", param.GetName())) + return "" + } +} + +// decoratorStringValue renders a decorator string parameter's value using whichever of +// the DSL's two string delimiters (`"` or `'`) the value does not contain, writing the +// value out RAW between them. +// +// The schema DSL has no backslash-escape syntax: lexStringLiteral +// (pkg/schemadsl/lexer/lex_def.go) scans raw for the closing delimiter with no escape +// awareness, and tryConsumeStringLiteral (pkg/schemadsl/parser/parser_impl.go) just +// trims the quote characters off. So a value can only round-trip if some delimiter it +// does not contain exists, and it must not contain a newline (single/double-quoted +// strings are single-line only). decorators.Validate rejects values that violate this +// before they ever reach a proto, but a hand-built proto could still hit it, so this is +// a defensive appendIssue rather than emitting unparseable text. +func (sg *sourceGenerator) decoratorStringValue(paramName string, value string) string { + hasDouble := strings.Contains(value, `"`) + hasSingle := strings.Contains(value, `'`) + + switch { + case strings.ContainsAny(value, "\n\r"): + sg.appendIssue(fmt.Sprintf("value for parameter `%s` contains a newline, which cannot be represented in schema source", paramName)) + return "" + + case hasDouble && hasSingle: + sg.appendIssue(fmt.Sprintf("value for parameter `%s` contains both quote styles and cannot be represented in schema source", paramName)) + return "" + + case hasDouble: + return `'` + value + `'` + + default: + return `"` + value + `"` + } +} + func (sg *sourceGenerator) emitCaveat(caveat *core.CaveatDefinition) error { sg.emitComments(caveat.Metadata) + sg.emitDecorators(caveat.GetDecorators(), false) sg.append("caveat ") sg.append(caveat.Name) sg.append("(") @@ -203,6 +297,7 @@ func (sg *sourceGenerator) emitCaveat(caveat *core.CaveatDefinition) error { func (sg *sourceGenerator) emitNamespace(namespace *core.NamespaceDefinition) error { sg.emitComments(namespace.Metadata) + sg.emitDecorators(namespace.GetDecorators(), false) sg.append("definition ") sg.append(namespace.Name) @@ -237,6 +332,7 @@ func (sg *sourceGenerator) emitRelation(relation *core.Relation) error { isPermission := relation.UsersetRewrite != nil && !hasThis sg.emitComments(relation.Metadata) + sg.emitDecorators(relation.GetDecorators(), false) if isPermission { sg.append("permission ") } else { @@ -270,6 +366,7 @@ func (sg *sourceGenerator) emitRelation(relation *core.Relation) error { } func (sg *sourceGenerator) emitAllowedRelation(allowedRelation *core.AllowedRelation) { + sg.emitDecorators(allowedRelation.GetDecorators(), true) sg.append(allowedRelation.Namespace) if allowedRelation.GetRelation() != "" && allowedRelation.GetRelation() != Ellipsis { sg.append("#") diff --git a/pkg/schemadsl/generator/generator_test.go b/pkg/schemadsl/generator/generator_test.go index 924fd511da..e8dbc178eb 100644 --- a/pkg/schemadsl/generator/generator_test.go +++ b/pkg/schemadsl/generator/generator_test.go @@ -11,6 +11,7 @@ import ( "github.com/authzed/spicedb/pkg/namespace" core "github.com/authzed/spicedb/pkg/proto/core/v1" "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" ) @@ -74,7 +75,7 @@ caveat somecaveat(someParam int) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { require := require.New(t) - source, ok, err := GenerateCaveatSource(test.input, caveattypes.Default.TypeSet) + source, _, ok, err := GenerateCaveatSource(test.input, caveattypes.Default.TypeSet) require.NoError(err) require.Equal(strings.TrimSpace(test.expected), source) require.Equal(test.okay, ok) @@ -465,3 +466,192 @@ definition user { }) } } + +func TestGenerateDecoratorsRoundTrip(t *testing.T) { + t.Parallel() + + schema := `use testdecorators + +@testall(needed: 7, label: "hi", on: true, mode: hash) +definition document { + @testrel + relation viewer: @testsub user + + @testrel + permission view = viewer +} + +definition user {}` + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: schema, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + generated, ok, err := GenerateSchema(t.Context(), compiled.OrderedDefinitions) + require.NoError(t, err) + require.True(t, ok) + + // The generated source must carry the decorators and the `use` line they require. + require.Contains(t, generated, "use testdecorators") + require.Contains(t, generated, "@testall(needed: 7, label: \"hi\", on: true, mode: hash)") + require.Contains(t, generated, "@testrel") + require.Contains(t, generated, "@testsub user") + + // And it must recompile to an identical schema. + recompiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: generated, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + regenerated, _, err := GenerateSchema(t.Context(), recompiled.OrderedDefinitions) + require.NoError(t, err) + require.Equal(t, generated, regenerated) +} + +func TestGenerateDecoratorsOnCaveatRoundTrip(t *testing.T) { + t.Parallel() + + schema := `use testdecorators + +@testcaveat +caveat somecaveat(someparam int) { + someparam == 42 +}` + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: schema, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + generated, _, err := GenerateSchema(t.Context(), compiled.OrderedDefinitions) + require.NoError(t, err) + require.Contains(t, generated, "use testdecorators") + require.Contains(t, generated, "@testcaveat") +} + +// decoratorStringParam finds the named parameter on the given decorator and returns its +// StringValue, failing the test if the parameter is absent or not a string. +func decoratorStringParam(t *testing.T, d *core.Decorator, name string) string { + t.Helper() + for _, p := range d.GetParameters() { + if p.GetName() == name { + return p.GetStringValue() + } + } + t.Fatalf("parameter %q not found on decorator %q", name, d.GetName()) + return "" +} + +// TestGenerateDecoratorStringParameterQuoting exercises the DSL's lack of backslash-escape +// syntax: the generator must pick whichever of `"`/`'` the value does not contain, and must +// write the value out raw (no escaping) since the lexer never interprets a backslash. Each +// case asserts a full compile -> generate -> recompile -> regenerate round trip, checking +// both that the regenerated text is byte-identical to the first generation (a fixed point, +// which is what ComputeSchemaHash relies on for stability) and that the decoded StringValue +// survives unchanged. +func TestGenerateDecoratorStringParameterQuoting(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + schema string + wantValue string + wantSubstring string + }{ + { + name: "double quote in value flips delimiter to single quote", + schema: `use testdecorators + +@testall(needed: 1, label: 'he said "hi"') +definition document {}`, + wantValue: `he said "hi"`, + wantSubstring: `label: 'he said "hi"'`, + }, + { + name: "single quote in value keeps double quote delimiter", + schema: `use testdecorators + +@testall(needed: 1, label: "it's here") +definition document {}`, + wantValue: `it's here`, + wantSubstring: `label: "it's here"`, + }, + { + name: "backslash requires no escaping", + schema: `use testdecorators + +@testall(needed: 1, label: "a\b") +definition document {}`, + wantValue: `a\b`, + wantSubstring: `label: "a\b"`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + compiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: test.schema, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + ns, ok := compiled.OrderedDefinitions[0].(*core.NamespaceDefinition) + require.True(t, ok) + require.Equal(t, test.wantValue, decoratorStringParam(t, ns.GetDecorators()[0], "label")) + + generated, ok, err := GenerateSchema(t.Context(), compiled.OrderedDefinitions) + require.NoError(t, err) + require.True(t, ok) + require.Contains(t, generated, test.wantSubstring) + + recompiled, err := compiler.Compile(compiler.InputSchema{ + Source: input.Source("test"), + SchemaString: generated, + }, compiler.AllowUnprefixedObjectType(), + compiler.WithDecoratorRegistry(decorators.TestRegistry)) + require.NoError(t, err) + + recompiledNS, ok := recompiled.OrderedDefinitions[0].(*core.NamespaceDefinition) + require.True(t, ok) + require.Equal(t, test.wantValue, decoratorStringParam(t, recompiledNS.GetDecorators()[0], "label"), + "recompiled StringValue must be byte-identical to the original") + + regenerated, _, err := GenerateSchema(t.Context(), recompiled.OrderedDefinitions) + require.NoError(t, err) + require.Equal(t, generated, regenerated) + }) + } +} + +// TestGenerateDecoratorParameterWithUnsetValueIsNotOK pins the defensive appendIssue guard +// in decoratorParameterValue's default case. decorators.Validate always sets one of the four +// oneof variants, so this path isn't reachable through the compiler, but a hand-built proto +// (or a future fifth oneof variant nobody wired up here yet) must not silently emit +// `@name(x: )`, which fails to recompile. It must flip `ok` to false instead. +func TestGenerateDecoratorParameterWithUnsetValueIsNotOK(t *testing.T) { + t.Parallel() + + ns := namespace.Namespace("document") + ns.Decorators = []*core.Decorator{ + { + Name: "testdef", + Parameters: []*core.DecoratorParameter{ + {Name: "x"}, // Value left unset. + }, + }, + } + + _, ok, err := GenerateSource(ns, caveattypes.Default.TypeSet) + require.NoError(t, err) + require.False(t, ok) +} diff --git a/pkg/schemadsl/lexer/flags.go b/pkg/schemadsl/lexer/flags.go index db1c225a09..93352814e3 100644 --- a/pkg/schemadsl/lexer/flags.go +++ b/pkg/schemadsl/lexer/flags.go @@ -3,6 +3,9 @@ package lexer import ( "maps" "slices" + "testing" + + "github.com/authzed/spicedb/pkg/schemadsl/decorators" ) const ( @@ -27,7 +30,22 @@ const ( var AllUseFlags []string +// noTransform is the transformer for feature flags that require no lexical change. +// Decorator feature flags use this: `@name` cannot collide with any identifier, so +// nothing needs to be promoted to a keyword. +func noTransform(lexeme Lexeme) (Lexeme, bool) { + return lexeme, false +} + func init() { + // The fixture decorators used to exercise the decorator machinery need their flag + // to be a valid `use` flag. Register it only in test binaries, so it never reaches + // production. testing.Testing() reads a string set by cmd/go via a linker -X flag, + // so it is already correct during package initialization. + if testing.Testing() { + Flags[decorators.TestFlag] = noTransform + } + AllUseFlags = slices.Collect(maps.Keys(Flags)) slices.Sort(AllUseFlags) } diff --git a/pkg/schemadsl/lexer/lex_def.go b/pkg/schemadsl/lexer/lex_def.go index eb98385618..aa69cd7d2a 100644 --- a/pkg/schemadsl/lexer/lex_def.go +++ b/pkg/schemadsl/lexer/lex_def.go @@ -50,6 +50,7 @@ const ( TokenTypeHash // # TokenTypeEllipsis // ... TokenTypeStar // * + TokenTypeAt // @ // Additional tokens for CEL: https://github.com/google/cel-spec/blob/master/doc/langdef.md#syntax TokenTypeQuestionMark // ? @@ -191,6 +192,9 @@ Loop: case r == '*': l.emit(TokenTypeStar) + case r == '@': + l.emit(TokenTypeAt) + case r == '.': if l.acceptString("..") { l.emit(TokenTypeEllipsis) diff --git a/pkg/schemadsl/lexer/lex_test.go b/pkg/schemadsl/lexer/lex_test.go index 8eb575b006..b3b03fa398 100644 --- a/pkg/schemadsl/lexer/lex_test.go +++ b/pkg/schemadsl/lexer/lex_test.go @@ -3,6 +3,9 @@ package lexer import ( "testing" + "github.com/stretchr/testify/require" + + "github.com/authzed/spicedb/pkg/schemadsl/decorators" "github.com/authzed/spicedb/pkg/schemadsl/input" ) @@ -41,6 +44,13 @@ var lexerTests = []lexerTest{ {"semicolon", ";", []Lexeme{{TokenTypeSemicolon, 0, ";", ""}, tEOF}}, {"star", "*", []Lexeme{{TokenTypeStar, 0, "*", ""}, tEOF}}, + {"at", "@", []Lexeme{{TokenTypeAt, 0, "@", ""}, tEOF}}, + {"at with identifier", "@circular", []Lexeme{ + {TokenTypeAt, 0, "@", ""}, + {TokenTypeIdentifier, 0, "circular", ""}, + tEOF, + }}, + {"right arrow", "->", []Lexeme{{TokenTypeRightArrow, 0, "->", ""}, tEOF}}, {"hash", "#", []Lexeme{{TokenTypeHash, 0, "#", ""}, tEOF}}, @@ -320,3 +330,9 @@ func equal(found, expected []Lexeme) bool { } return true } + +func TestTestDecoratorsFlagRegistered(t *testing.T) { + // Registered here because this is a test binary; see the init() in flags.go. + _, ok := Flags[decorators.TestFlag] + require.True(t, ok) +} diff --git a/pkg/schemadsl/lexer/tokentype_string.go b/pkg/schemadsl/lexer/tokentype_string.go index 73c993846c..a8c46ca2a4 100644 --- a/pkg/schemadsl/lexer/tokentype_string.go +++ b/pkg/schemadsl/lexer/tokentype_string.go @@ -34,27 +34,28 @@ func _() { _ = x[TokenTypeHash-23] _ = x[TokenTypeEllipsis-24] _ = x[TokenTypeStar-25] - _ = x[TokenTypeQuestionMark-26] - _ = x[TokenTypeConditionalOr-27] - _ = x[TokenTypeConditionalAnd-28] - _ = x[TokenTypeExclamationPoint-29] - _ = x[TokenTypeLeftBracket-30] - _ = x[TokenTypeRightBracket-31] - _ = x[TokenTypePeriod-32] - _ = x[TokenTypeComma-33] - _ = x[TokenTypePercent-34] - _ = x[TokenTypeLessThan-35] - _ = x[TokenTypeGreaterThan-36] - _ = x[TokenTypeLessThanOrEqual-37] - _ = x[TokenTypeGreaterThanOrEqual-38] - _ = x[TokenTypeEqualEqual-39] - _ = x[TokenTypeNotEqual-40] - _ = x[TokenTypeString-41] + _ = x[TokenTypeAt-26] + _ = x[TokenTypeQuestionMark-27] + _ = x[TokenTypeConditionalOr-28] + _ = x[TokenTypeConditionalAnd-29] + _ = x[TokenTypeExclamationPoint-30] + _ = x[TokenTypeLeftBracket-31] + _ = x[TokenTypeRightBracket-32] + _ = x[TokenTypePeriod-33] + _ = x[TokenTypeComma-34] + _ = x[TokenTypePercent-35] + _ = x[TokenTypeLessThan-36] + _ = x[TokenTypeGreaterThan-37] + _ = x[TokenTypeLessThanOrEqual-38] + _ = x[TokenTypeGreaterThanOrEqual-39] + _ = x[TokenTypeEqualEqual-40] + _ = x[TokenTypeNotEqual-41] + _ = x[TokenTypeString-42] } -const _TokenType_name = "TokenTypeErrorTokenTypeSyntheticSemicolonTokenTypeEOFTokenTypeWhitespaceTokenTypeSinglelineCommentTokenTypeMultilineCommentTokenTypeNewlineTokenTypeKeywordTokenTypeIdentifierTokenTypeNumberTokenTypeLeftBraceTokenTypeRightBraceTokenTypeLeftParenTokenTypeRightParenTokenTypePipeTokenTypePlusTokenTypeMinusTokenTypeAndTokenTypeDivTokenTypeEqualsTokenTypeColonTokenTypeSemicolonTokenTypeRightArrowTokenTypeHashTokenTypeEllipsisTokenTypeStarTokenTypeQuestionMarkTokenTypeConditionalOrTokenTypeConditionalAndTokenTypeExclamationPointTokenTypeLeftBracketTokenTypeRightBracketTokenTypePeriodTokenTypeCommaTokenTypePercentTokenTypeLessThanTokenTypeGreaterThanTokenTypeLessThanOrEqualTokenTypeGreaterThanOrEqualTokenTypeEqualEqualTokenTypeNotEqualTokenTypeString" +const _TokenType_name = "TokenTypeErrorTokenTypeSyntheticSemicolonTokenTypeEOFTokenTypeWhitespaceTokenTypeSinglelineCommentTokenTypeMultilineCommentTokenTypeNewlineTokenTypeKeywordTokenTypeIdentifierTokenTypeNumberTokenTypeLeftBraceTokenTypeRightBraceTokenTypeLeftParenTokenTypeRightParenTokenTypePipeTokenTypePlusTokenTypeMinusTokenTypeAndTokenTypeDivTokenTypeEqualsTokenTypeColonTokenTypeSemicolonTokenTypeRightArrowTokenTypeHashTokenTypeEllipsisTokenTypeStarTokenTypeAtTokenTypeQuestionMarkTokenTypeConditionalOrTokenTypeConditionalAndTokenTypeExclamationPointTokenTypeLeftBracketTokenTypeRightBracketTokenTypePeriodTokenTypeCommaTokenTypePercentTokenTypeLessThanTokenTypeGreaterThanTokenTypeLessThanOrEqualTokenTypeGreaterThanOrEqualTokenTypeEqualEqualTokenTypeNotEqualTokenTypeString" -var _TokenType_index = [...]uint16{0, 14, 41, 53, 72, 98, 123, 139, 155, 174, 189, 207, 226, 244, 263, 276, 289, 303, 315, 327, 342, 356, 374, 393, 406, 423, 436, 457, 479, 502, 527, 547, 568, 583, 597, 613, 630, 650, 674, 701, 720, 737, 752} +var _TokenType_index = [...]uint16{0, 14, 41, 53, 72, 98, 123, 139, 155, 174, 189, 207, 226, 244, 263, 276, 289, 303, 315, 327, 342, 356, 374, 393, 406, 423, 436, 447, 468, 490, 513, 538, 558, 579, 594, 608, 624, 641, 661, 685, 712, 731, 748, 763} func (i TokenType) String() string { idx := int(i) - 0 diff --git a/pkg/schemadsl/parser/decorators.go b/pkg/schemadsl/parser/decorators.go new file mode 100644 index 0000000000..55bd1a0019 --- /dev/null +++ b/pkg/schemadsl/parser/decorators.go @@ -0,0 +1,231 @@ +package parser + +import ( + "github.com/authzed/spicedb/pkg/schemadsl/dslshape" + "github.com/authzed/spicedb/pkg/schemadsl/lexer" +) + +// Syntactic kinds recorded on a decorator parameter value. These mirror +// decorators.ValueKind; the compiler coerces to the declared parameter type. +const ( + decoratorValueKindString = "string" + decoratorValueKindIdentifier = "identifier" +) + +// tryConsumeDecorators consumes a run of decorators, if any are present, returning the +// nodes for the caller to attach to whatever declaration follows, along with every +// comment found across the whole run (including any comment preceding the first +// decorator, and any comment between two stacked decorators), in source order. +// +// ```@first @second``` and ```@first\n@second``` are equivalent: a newline after a +// decorator produces a synthetic semicolon, which is absorbed here. +func (p *sourceParser) tryConsumeDecorators() ([]AstNode, []string) { + var decorators []AstNode + var comments []string + for p.isToken(lexer.TokenTypeAt) { + comments = append(comments, p.currentToken.comments...) + decorators = append(decorators, p.consumeDecorator()) + p.tryConsumeStatementTerminator() + } + return decorators, comments +} + +// consumeDecorator consumes a single decorator. +// ```@somename``` or ```@somename(param: value, other: "value")``` +func (p *sourceParser) consumeDecorator() AstNode { + decoratorNode := p.startNodeWithoutComments(dslshape.NodeTypeDecorator) + defer p.mustFinishNode() + + // @ + if _, ok := p.consume(lexer.TokenTypeAt); !ok { + return decoratorNode + } + + name, ok := p.consumeIdentifier() + if !ok { + return decoratorNode + } + + decoratorNode.MustDecorate(dslshape.NodeDecoratorPredicateName, name) + + // Parameters are optional. + // ( + if _, ok := p.tryConsume(lexer.TokenTypeLeftParen); !ok { + return decoratorNode + } + + if p.isToken(lexer.TokenTypeRightParen) { + p.emitErrorf("Decorator `@%s` has an empty parameter list; write `@%s` instead", name, name) + // Consume the stray `)` so the caller can recover and continue parsing the + // declaration that follows, rather than getting stuck on it. + p.tryConsume(lexer.TokenTypeRightParen) + return decoratorNode + } + + for { + paramNode, ok := p.consumeDecoratorParameter() + + // Connect paramNode unconditionally: even on failure, it carries the specific + // error node for whatever went wrong (e.g. a missing `:`), and that error is + // only reachable via root.FindAll(NodeTypeError) once it is linked into the + // tree. Returning before this Connect would silently drop the real diagnostic. + decoratorNode.Connect(dslshape.NodeDecoratorPredicateParameters, paramNode) + + if !ok { + // Recover by skipping ahead to the decorator's closing `)`, the same way + // the empty-parameter-list case above does. Without this, the token + // cursor is left wherever the malformed parameter choked (e.g. right + // before the `)`, or on a stray value token), the caller's switch on + // whatever construct is supposed to follow the decorator fails to match + // anything, and the decorator - along with the real error node just + // connected above - is discarded wholesale via the generic "expected + // definition/caveat/partial after decorator" path. That is the same + // orphaning mechanism fixed for decorators in commit a694626, just + // reached from inside the parameter list instead of from an empty one. + p.skipToDecoratorParametersClose() + return decoratorNode + } + + if _, ok := p.tryConsume(lexer.TokenTypeComma); !ok { + break + } + } + + // ) + p.consume(lexer.TokenTypeRightParen) + return decoratorNode +} + +// skipToDecoratorParametersClose advances past whatever malformed token(s) remain in a +// decorator's parameter list after an unrecoverable parse error, consuming up to and +// including the list's own closing `)` if one is found. This lets parsing of whatever +// follows the decorator resume normally instead of leaving the cursor stuck +// mid-decorator (see consumeDecorator's call site for why that matters). +// +// Depth is tracked (mirroring the brace-depth tracking in consumeCaveatExpression), +// starting at 1 for the decorator's own already-consumed opening `(`, so that a stray +// `(` inside a malformed value (e.g. `@name(p: (1))`) does not make the *inner* `)` +// look like the decorator's own close. Stopping at that inner `)` would leave the +// decorator's real `)` dangling as the next token and reintroduce the exact orphaning +// this function exists to avoid (the outer switch would see a stray `)` where a +// declaration should be, and discard the decorator - and its real error - all over +// again). +// +// Termination is unconditional, not dependent on well-formed input, mirroring +// consumeCaveatExpression's own hard stops: TokenTypeError and TokenTypeEOF always +// stop the scan. This matters because after a lex error the lexer's goroutine exits +// and closes its token channel; every subsequent read from a closed channel returns +// the zero Lexeme, whose Kind is TokenTypeError (iota 0) - so failing to treat that +// as a hard stop would spin forever re-observing the same synthetic token. The +// statement-terminator and brace kinds are additional hard stops because none of them +// can legitimately appear inside a decorator's parameter list, so treating them as +// "just another token to skip" could run the scan into unrelated, distant source +// instead of admitting defeat. +// +// Progress argument: each loop iteration either returns (via the labeled break) or +// falls through to an unconditional p.consumeToken(), which always advances to the +// next token in the (finite, for any input) token stream. Combined with the +// unconditional EOF/error stops above, the loop is bounded by the number of tokens +// remaining before EOF or a lex error and cannot run forever on any input. +func (p *sourceParser) skipToDecoratorParametersClose() { + depth := 1 +skipLoop: + for { + switch p.currentToken.Kind { + case lexer.TokenTypeLeftParen: + depth++ + + case lexer.TokenTypeRightParen: + depth-- + if depth == 0 { + p.consumeToken() + break skipLoop + } + + case lexer.TokenTypeError, + lexer.TokenTypeEOF, + lexer.TokenTypeSyntheticSemicolon, + lexer.TokenTypeSemicolon, + lexer.TokenTypeLeftBrace, + lexer.TokenTypeRightBrace: + break skipLoop + } + + p.consumeToken() + } +} + +// consumeDecoratorParameter consumes a single named decorator parameter. +// ```paramname: value``` +func (p *sourceParser) consumeDecoratorParameter() (AstNode, bool) { + paramNode := p.startNode(dslshape.NodeTypeDecoratorParameter) + defer p.mustFinishNode() + + name, ok := p.consumeIdentifier() + if !ok { + return paramNode, false + } + + paramNode.MustDecorate(dslshape.NodeDecoratorParameterPredicateName, name) + + // : + if _, ok := p.consume(lexer.TokenTypeColon); !ok { + return paramNode, false + } + + kind, value, ok := p.consumeDecoratorValue() + if !ok { + return paramNode, false + } + + paramNode.MustDecorate(dslshape.NodeDecoratorParameterPredicateKind, kind) + paramNode.MustDecorate(dslshape.NodeDecoratorParameterPredicateValue, value) + return paramNode, true +} + +// consumeDecoratorValue consumes a decorator parameter value, returning its syntactic +// kind and its text. +// +// NOTE: numbers arrive as identifiers, because the lexer never emits TokenTypeNumber. +// The compiler coerces based on the declared parameter type. +func (p *sourceParser) consumeDecoratorValue() (string, string, bool) { + if value, ok := p.tryConsumeStringLiteral(); ok { + return decoratorValueKindString, value, true + } + + negated := false + if _, ok := p.tryConsume(lexer.TokenTypeMinus); ok { + negated = true + } + + token, ok := p.tryConsume(lexer.TokenTypeIdentifier) + if !ok { + p.emitErrorf("Expected a decorator parameter value, found token %v", p.currentToken.Kind) + return "", "", false + } + + if negated { + return decoratorValueKindIdentifier, "-" + token.Value, true + } + + return decoratorValueKindIdentifier, token.Value, true +} + +// attachDecorators connects the given decorators to the decorated node and replays the +// comments captured across the whole decorator run onto it, so that a doc comment +// written above (or between) decorators documents the declaration rather than the +// decorator(s). +// +// If there were no decorators, this is a no-op: the declaration's own startNode call +// already claimed its comments normally, and replaying here would double-attach them. +func (p *sourceParser) attachDecorators(node AstNode, decorators []AstNode, comments []string) { + if len(decorators) == 0 { + return + } + + for _, decorator := range decorators { + node.Connect(dslshape.NodePredicateDecorator, decorator) + } + + p.decorateComments(node, comments) +} diff --git a/pkg/schemadsl/parser/decorators_test.go b/pkg/schemadsl/parser/decorators_test.go new file mode 100644 index 0000000000..bbbe2e7c09 --- /dev/null +++ b/pkg/schemadsl/parser/decorators_test.go @@ -0,0 +1,58 @@ +package parser + +import ( + "testing" + "time" + + "github.com/authzed/spicedb/pkg/schemadsl/input" +) + +// TestDecoratorMalformedParameterRecoveryTerminates guards against a regression in +// skipToDecoratorParametersClose (consumeDecorator's malformed-parameter recovery path): +// a lex error (or an unterminated string, which the lexer also reports as a lex error) +// inside a decorator's parameter list closes the lexer's token channel, after which +// every further read returns the zero Lexeme - Kind TokenTypeError (iota 0). A recovery +// loop that does not treat TokenTypeError as a hard stop will busy-loop forever +// re-observing that same synthetic token instead of returning, hanging whatever called +// Parse (and, transitively, WriteSchema/compiler.Compile - decorator syntax parses +// unconditionally, with no `use` flag required to reach it). +// +// Each case runs Parse on its own goroutine and requires it to return well within the +// test timeout, so a regression shows up as a fast, specific test failure instead of a +// CI job hanging until the suite-level timeout kills it. +func TestDecoratorMalformedParameterRecoveryTerminates(t *testing.T) { + tcs := []struct { + name string + input string + }{ + { + "lex error inside parameter list", + "use testdecorators\n\n@testall(needed: $)\ndefinition user {}", + }, + { + "unterminated string inside parameter list", + "use testdecorators\n\n@testall(needed: 1, label: \"unterminated)\ndefinition user {}", + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + done := make(chan struct{}) + go func() { + defer close(done) + Parse(createAstNode, input.Source(tc.name), tc.input) + }() + + select { + case <-done: + // Parse returned: the recovery loop terminated as required. Note we + // deliberately do not assert anything about the resulting tree here - + // this test exists solely to prove termination. + case <-time.After(5 * time.Second): + t.Fatalf("Parse(%q) did not return within 5s; the malformed-parameter "+ + "recovery loop in skipToDecoratorParametersClose likely regressed "+ + "into an infinite loop on a lex error", tc.name) + } + }) + } +} diff --git a/pkg/schemadsl/parser/parser.go b/pkg/schemadsl/parser/parser.go index cf283e7709..04ce61ca14 100644 --- a/pkg/schemadsl/parser/parser.go +++ b/pkg/schemadsl/parser/parser.go @@ -59,28 +59,44 @@ Loop: // definition foobar { ... } // caveat somecaveat (...) { ... } + decorators, decoratorComments := p.tryConsumeDecorators() + + var consumed AstNode switch { case p.isIdentifier("use"): - rootNode.Connect(dslshape.NodePredicateChild, p.consumeUseFlag(hasSeenDefinition)) + if len(decorators) > 0 { + p.emitErrorf("Decorators cannot be applied to a `use` flag") + } + consumed = p.consumeUseFlag(hasSeenDefinition) case p.isKeyword("definition"): hasSeenDefinition = true - rootNode.Connect(dslshape.NodePredicateChild, p.consumeDefinition()) + consumed = p.consumeDefinition() case p.isKeyword("caveat"): hasSeenDefinition = true - rootNode.Connect(dslshape.NodePredicateChild, p.consumeCaveat()) + consumed = p.consumeCaveat() case p.isKeyword("import"): - rootNode.Connect(dslshape.NodePredicateChild, p.consumeImport()) + if len(decorators) > 0 { + p.emitErrorf("Decorators cannot be applied to an `import`") + } + consumed = p.consumeImport() case p.isKeyword("partial"): - rootNode.Connect(dslshape.NodePredicateChild, p.consumePartial()) + consumed = p.consumePartial() default: - p.emitErrorf("Unexpected token at root level: %v", p.currentToken.Kind) + if len(decorators) > 0 { + p.emitErrorf("Expected definition, caveat or partial after decorator, found token %v", p.currentToken.Kind) + } else { + p.emitErrorf("Unexpected token at root level: %v", p.currentToken.Kind) + } break Loop } + + p.attachDecorators(consumed, decorators, decoratorComments) + rootNode.Connect(dslshape.NodePredicateChild, consumed) } return rootNode @@ -338,22 +354,38 @@ func (p *sourceParser) consumeDefinitionOrPartialImpl(node AstNode) AstNode { // Relations and permissions. for { + decorators, decoratorComments := p.tryConsumeDecorators() + // } if _, ok := p.tryConsume(lexer.TokenTypeRightBrace); ok { + if len(decorators) > 0 { + p.emitErrorf("Expected relation or permission after decorator, found `}`") + } break } // relation ... // permission ... + var consumed AstNode switch { case p.isKeyword("relation"): - node.Connect(dslshape.NodePredicateChild, p.consumeRelation()) + consumed = p.consumeRelation() case p.isKeyword("permission"): - node.Connect(dslshape.NodePredicateChild, p.consumePermission()) + consumed = p.consumePermission() case p.isToken(lexer.TokenTypeEllipsis): - node.Connect(dslshape.NodePredicateChild, p.consumePartialReference()) + if len(decorators) > 0 { + p.emitErrorf("Decorators cannot be applied to a partial reference") + } + consumed = p.consumePartialReference() + } + + if consumed != nil { + p.attachDecorators(consumed, decorators, decoratorComments) + node.Connect(dslshape.NodePredicateChild, consumed) + } else if len(decorators) > 0 { + p.emitErrorf("Expected relation or permission after decorator, found token %v", p.currentToken.Kind) } ok := p.consumeStatementTerminator() @@ -399,7 +431,12 @@ func (p *sourceParser) consumeTypeReference() AstNode { defer p.mustFinishNode() for { - refNode.Connect(dslshape.NodeTypeReferencePredicateType, p.consumeSpecificTypeWithCaveat()) + decorators, decoratorComments := p.tryConsumeDecorators() + + specificNode := p.consumeSpecificTypeWithCaveat() + p.attachDecorators(specificNode, decorators, decoratorComments) + refNode.Connect(dslshape.NodeTypeReferencePredicateType, specificNode) + if _, ok := p.tryConsume(lexer.TokenTypePipe); !ok { break } diff --git a/pkg/schemadsl/parser/parser_impl.go b/pkg/schemadsl/parser/parser_impl.go index e8272d701a..a50768f938 100644 --- a/pkg/schemadsl/parser/parser_impl.go +++ b/pkg/schemadsl/parser/parser_impl.go @@ -95,11 +95,30 @@ func (p *sourceParser) startNode(kind dslshape.NodeType) AstNode { return node } +// startNodeWithoutComments creates a new node of the given type and decorates it with the +// current token's position, but deliberately does NOT claim the token's pending comments. +// +// This exists for decorators: a doc comment preceding `@somedecorator` documents the +// declaration that follows, not the decorator, so the comments are left for the caller +// to replay onto the decorated node. +func (p *sourceParser) startNodeWithoutComments(kind dslshape.NodeType) AstNode { + node := p.createNode(kind) + p.decorateStartRune(node, p.currentToken) + p.nodes.push(node) + return node +} + +// decorateStartRune decorates the given node with the location of the given token as its +// starting rune. +func (p *sourceParser) decorateStartRune(node AstNode, token commentedLexeme) { + node.MustDecorate(dslshape.NodePredicateSource, string(p.source)) + node.MustDecorateWithInt(dslshape.NodePredicateStartRune, int(token.Position)) +} + // decorateStartRuneAndComments decorates the given node with the location of the given token as its // starting rune, as well as any comments attached to the token. func (p *sourceParser) decorateStartRuneAndComments(node AstNode, token commentedLexeme) { - node.MustDecorate(dslshape.NodePredicateSource, string(p.source)) - node.MustDecorateWithInt(dslshape.NodePredicateStartRune, int(token.Position)) + p.decorateStartRune(node, token) p.decorateComments(node, token.comments) } diff --git a/pkg/schemadsl/parser/parser_test.go b/pkg/schemadsl/parser/parser_test.go index 46c09c8bec..43b3df69dc 100644 --- a/pkg/schemadsl/parser/parser_test.go +++ b/pkg/schemadsl/parser/parser_test.go @@ -154,6 +154,15 @@ func TestParser(t *testing.T) { {"partials with malformed reference splat", "partials_with_malformed_reference_splat"}, {"partials with malformed partial block", "partials_with_malformed_partial_block"}, {"expiration before caveat test", "expirationbeforecaveat"}, + {"decorator on definition test", "decorator_definition"}, + {"decorator on relation and permission test", "decorator_member"}, + {"decorator on subject type test", "decorator_subjecttype"}, + {"decorator error test", "decorator_errors"}, + {"decorator on illegal sites test", "decorator_illegal_sites"}, + {"decorator malformed parameter missing colon test", "decorator_malformed_parameter_no_colon"}, + {"decorator malformed parameter missing colon before value test", "decorator_malformed_parameter_missing_colon_value"}, + {"decorator malformed parameter trailing comma test", "decorator_malformed_parameter_trailing_comma"}, + {"decorator malformed parameter with nested parens test", "decorator_malformed_parameter_nested_parens"}, } for _, test := range parserTests { diff --git a/pkg/schemadsl/parser/tests/decorator_definition.zed b/pkg/schemadsl/parser/tests/decorator_definition.zed new file mode 100644 index 0000000000..1fa44b51ba --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_definition.zed @@ -0,0 +1,21 @@ +use testdecorators + +// This comment documents the definition, not the decorator. +@testdef +definition user {} + +@testall(needed: 1, count: -16, label: "hi", on: true, mode: hash) +definition document {} + +@testdef @testall(needed: 2) definition folder {} + +@testcaveat +caveat somecaveat(someparam int) { + someparam == 42 +} + +// Comment above the first decorator. +@testdef +// Comment between the decorators. +@testall(needed: 3) +definition stacked {} diff --git a/pkg/schemadsl/parser/tests/decorator_definition.zed.expected b/pkg/schemadsl/parser/tests/decorator_definition.zed.expected new file mode 100644 index 0000000000..3ea463250b --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_definition.zed.expected @@ -0,0 +1,154 @@ +NodeTypeFile + end-rune = 442 + input-source = decorator on definition test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator on definition test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 107 + input-source = decorator on definition test + start-rune = 90 + child-node => + NodeTypeComment + comment-value = // This comment documents the definition, not the decorator. + decorator => + NodeTypeDecorator + decorator-name = testdef + end-rune = 88 + input-source = decorator on definition test + start-rune = 81 + NodeTypeDefinition + definition-name = document + end-rune = 198 + input-source = decorator on definition test + start-rune = 177 + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 175 + input-source = decorator on definition test + start-rune = 110 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 1 + end-rune = 127 + input-source = decorator on definition test + start-rune = 119 + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = count + decorator-parameter-value = -16 + end-rune = 139 + input-source = decorator on definition test + start-rune = 130 + NodeTypeDecoratorParameter + decorator-parameter-kind = string + decorator-parameter-name = label + decorator-parameter-value = hi + end-rune = 152 + input-source = decorator on definition test + start-rune = 142 + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = on + decorator-parameter-value = true + end-rune = 162 + input-source = decorator on definition test + start-rune = 155 + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = mode + decorator-parameter-value = hash + end-rune = 174 + input-source = decorator on definition test + start-rune = 165 + NodeTypeDefinition + definition-name = folder + end-rune = 249 + input-source = decorator on definition test + start-rune = 230 + decorator => + NodeTypeDecorator + decorator-name = testdef + end-rune = 208 + input-source = decorator on definition test + start-rune = 201 + NodeTypeDecorator + decorator-name = testall + end-rune = 228 + input-source = decorator on definition test + start-rune = 210 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 2 + end-rune = 227 + input-source = decorator on definition test + start-rune = 219 + NodeTypeCaveatDefinition + caveat-definition-name = somecaveat + end-rune = 316 + input-source = decorator on definition test + start-rune = 264 + caveat-definition-expression => + NodeTypeCaveatExpression + caveat-expression-expressionstr = someparam == 42 + + end-rune = 315 + input-source = decorator on definition test + start-rune = 300 + decorator => + NodeTypeDecorator + decorator-name = testcaveat + end-rune = 262 + input-source = decorator on definition test + start-rune = 252 + parameters => + NodeTypeCaveatParameter + caveat-parameter-name = someparam + end-rune = 294 + input-source = decorator on definition test + start-rune = 282 + caveat-parameter-type => + NodeTypeCaveatTypeReference + end-rune = 294 + input-source = decorator on definition test + start-rune = 292 + type-name = int + NodeTypeDefinition + definition-name = stacked + end-rune = 441 + input-source = decorator on definition test + start-rune = 421 + child-node => + NodeTypeComment + comment-value = // Comment above the first decorator. + NodeTypeComment + comment-value = // Comment between the decorators. + decorator => + NodeTypeDecorator + decorator-name = testdef + end-rune = 364 + input-source = decorator on definition test + start-rune = 357 + NodeTypeDecorator + decorator-name = testall + end-rune = 419 + input-source = decorator on definition test + start-rune = 401 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 3 + end-rune = 418 + input-source = decorator on definition test + start-rune = 410 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_errors.zed b/pkg/schemadsl/parser/tests/decorator_errors.zed new file mode 100644 index 0000000000..c8c2de0f98 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_errors.zed @@ -0,0 +1,4 @@ +use testdecorators + +@testdef() +definition user {} diff --git a/pkg/schemadsl/parser/tests/decorator_errors.zed.expected b/pkg/schemadsl/parser/tests/decorator_errors.zed.expected new file mode 100644 index 0000000000..892615d65d --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_errors.zed.expected @@ -0,0 +1,28 @@ +NodeTypeFile + end-rune = 49 + input-source = decorator error test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator error test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 48 + input-source = decorator error test + start-rune = 31 + decorator => + NodeTypeDecorator + decorator-name = testdef + end-rune = 29 + input-source = decorator error test + start-rune = 20 + child-node => + NodeTypeError + end-rune = 28 + error-message = Decorator `@testdef` has an empty parameter list; write `@testdef` instead + error-source = ) + input-source = decorator error test + start-rune = 29 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed b/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed new file mode 100644 index 0000000000..bb7086db49 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed @@ -0,0 +1,20 @@ +use testdecorators +use partial +use import + +@testrel +use self + +@testrel +import "other.zed" + +partial base { + relation viewer: user +} + +definition user {} + +definition document { + @testrel + ...base +} diff --git a/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed.expected b/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed.expected new file mode 100644 index 0000000000..fba4e1e416 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_illegal_sites.zed.expected @@ -0,0 +1,104 @@ +NodeTypeFile + end-rune = 194 + input-source = decorator on illegal sites test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator on illegal sites test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeUseFlag + end-rune = 29 + input-source = decorator on illegal sites test + start-rune = 19 + use-flag-name = partial + NodeTypeUseFlag + end-rune = 40 + input-source = decorator on illegal sites test + start-rune = 31 + use-flag-name = import + NodeTypeError + end-rune = 51 + error-message = Decorators cannot be applied to a `use` flag + error-source = use + input-source = decorator on illegal sites test + start-rune = 52 + NodeTypeUseFlag + end-rune = 59 + input-source = decorator on illegal sites test + start-rune = 52 + use-flag-name = self + decorator => + NodeTypeDecorator + decorator-name = testrel + end-rune = 50 + input-source = decorator on illegal sites test + start-rune = 43 + NodeTypeError + end-rune = 70 + error-message = Decorators cannot be applied to an `import` + error-source = import + input-source = decorator on illegal sites test + start-rune = 71 + NodeTypeImport + end-rune = 88 + import-path = other.zed + input-source = decorator on illegal sites test + start-rune = 71 + decorator => + NodeTypeDecorator + decorator-name = testrel + end-rune = 69 + input-source = decorator on illegal sites test + start-rune = 62 + NodeTypePartial + end-rune = 129 + input-source = decorator on illegal sites test + partial-name = base + start-rune = 91 + child-node => + NodeTypeRelation + end-rune = 127 + input-source = decorator on illegal sites test + relation-name = viewer + start-rune = 107 + allowed-types => + NodeTypeTypeReference + end-rune = 127 + input-source = decorator on illegal sites test + start-rune = 124 + type-ref-type => + NodeTypeSpecificTypeReference + end-rune = 127 + input-source = decorator on illegal sites test + start-rune = 124 + type-name = user + NodeTypeDefinition + definition-name = user + end-rune = 149 + input-source = decorator on illegal sites test + start-rune = 132 + NodeTypeDefinition + definition-name = document + end-rune = 193 + input-source = decorator on illegal sites test + start-rune = 152 + child-node => + NodeTypeError + end-rune = 183 + error-message = Decorators cannot be applied to a partial reference + error-source = ... + input-source = decorator on illegal sites test + start-rune = 185 + NodeTypePartialReference + end-rune = 191 + input-source = decorator on illegal sites test + partial-reference-name = base + start-rune = 185 + decorator => + NodeTypeDecorator + decorator-name = testrel + end-rune = 182 + input-source = decorator on illegal sites test + start-rune = 175 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed new file mode 100644 index 0000000000..f9fdc90e05 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed @@ -0,0 +1,4 @@ +use testdecorators + +@testall(needed 1) +definition user {} diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed.expected b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed.expected new file mode 100644 index 0000000000..dfaa655e8d --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_missing_colon_value.zed.expected @@ -0,0 +1,34 @@ +NodeTypeFile + end-rune = 57 + input-source = decorator malformed parameter missing colon before value test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator malformed parameter missing colon before value test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 56 + input-source = decorator malformed parameter missing colon before value test + start-rune = 39 + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 37 + input-source = decorator malformed parameter missing colon before value test + start-rune = 20 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-name = needed + end-rune = 34 + input-source = decorator malformed parameter missing colon before value test + start-rune = 29 + child-node => + NodeTypeError + end-rune = 34 + error-message = Expected one of: [TokenTypeColon], found: TokenTypeIdentifier + error-source = 1 + input-source = decorator malformed parameter missing colon before value test + start-rune = 36 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed new file mode 100644 index 0000000000..b5a2d03d68 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed @@ -0,0 +1,4 @@ +use testdecorators + +@testall(needed: (1)) +definition user {} diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed.expected b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed.expected new file mode 100644 index 0000000000..6da57dec0c --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_nested_parens.zed.expected @@ -0,0 +1,34 @@ +NodeTypeFile + end-rune = 60 + input-source = decorator malformed parameter with nested parens test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator malformed parameter with nested parens test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 59 + input-source = decorator malformed parameter with nested parens test + start-rune = 42 + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 40 + input-source = decorator malformed parameter with nested parens test + start-rune = 20 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-name = needed + end-rune = 35 + input-source = decorator malformed parameter with nested parens test + start-rune = 29 + child-node => + NodeTypeError + end-rune = 35 + error-message = Expected a decorator parameter value, found token TokenTypeLeftParen + error-source = ( + input-source = decorator malformed parameter with nested parens test + start-rune = 37 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed new file mode 100644 index 0000000000..a26b0f619e --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed @@ -0,0 +1,4 @@ +use testdecorators + +@testall(bar) +definition user {} diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed.expected b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed.expected new file mode 100644 index 0000000000..0217406c9f --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_no_colon.zed.expected @@ -0,0 +1,34 @@ +NodeTypeFile + end-rune = 52 + input-source = decorator malformed parameter missing colon test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator malformed parameter missing colon test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 51 + input-source = decorator malformed parameter missing colon test + start-rune = 34 + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 32 + input-source = decorator malformed parameter missing colon test + start-rune = 20 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-name = bar + end-rune = 31 + input-source = decorator malformed parameter missing colon test + start-rune = 29 + child-node => + NodeTypeError + end-rune = 31 + error-message = Expected one of: [TokenTypeColon], found: TokenTypeRightParen + error-source = ) + input-source = decorator malformed parameter missing colon test + start-rune = 32 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed new file mode 100644 index 0000000000..2fdb228391 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed @@ -0,0 +1,4 @@ +use testdecorators + +@testall(needed: 1,) +definition user {} diff --git a/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed.expected b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed.expected new file mode 100644 index 0000000000..399a63cb55 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_malformed_parameter_trailing_comma.zed.expected @@ -0,0 +1,40 @@ +NodeTypeFile + end-rune = 59 + input-source = decorator malformed parameter trailing comma test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator malformed parameter trailing comma test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 58 + input-source = decorator malformed parameter trailing comma test + start-rune = 41 + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 39 + input-source = decorator malformed parameter trailing comma test + start-rune = 20 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 1 + end-rune = 37 + input-source = decorator malformed parameter trailing comma test + start-rune = 29 + NodeTypeDecoratorParameter + end-rune = 38 + input-source = decorator malformed parameter trailing comma test + start-rune = 39 + child-node => + NodeTypeError + end-rune = 38 + error-message = Expected identifier, found token TokenTypeRightParen + error-source = ) + input-source = decorator malformed parameter trailing comma test + start-rune = 39 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_member.zed b/pkg/schemadsl/parser/tests/decorator_member.zed new file mode 100644 index 0000000000..66c4c257c8 --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_member.zed @@ -0,0 +1,15 @@ +use testdecorators + +definition user {} + +definition document { + // This comment documents the relation. + @testrel + relation viewer: user + + @testall(needed: 1, mode: range) + relation editor: user + + @testrel + permission view = viewer + editor +} diff --git a/pkg/schemadsl/parser/tests/decorator_member.zed.expected b/pkg/schemadsl/parser/tests/decorator_member.zed.expected new file mode 100644 index 0000000000..eb4bd1ed5e --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_member.zed.expected @@ -0,0 +1,111 @@ +NodeTypeFile + end-rune = 241 + input-source = decorator on relation and permission test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator on relation and permission test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 37 + input-source = decorator on relation and permission test + start-rune = 20 + NodeTypeDefinition + definition-name = document + end-rune = 240 + input-source = decorator on relation and permission test + start-rune = 40 + child-node => + NodeTypeRelation + end-rune = 134 + input-source = decorator on relation and permission test + relation-name = viewer + start-rune = 114 + allowed-types => + NodeTypeTypeReference + end-rune = 134 + input-source = decorator on relation and permission test + start-rune = 131 + type-ref-type => + NodeTypeSpecificTypeReference + end-rune = 134 + input-source = decorator on relation and permission test + start-rune = 131 + type-name = user + child-node => + NodeTypeComment + comment-value = // This comment documents the relation. + decorator => + NodeTypeDecorator + decorator-name = testrel + end-rune = 111 + input-source = decorator on relation and permission test + start-rune = 104 + NodeTypeRelation + end-rune = 192 + input-source = decorator on relation and permission test + relation-name = editor + start-rune = 172 + allowed-types => + NodeTypeTypeReference + end-rune = 192 + input-source = decorator on relation and permission test + start-rune = 189 + type-ref-type => + NodeTypeSpecificTypeReference + end-rune = 192 + input-source = decorator on relation and permission test + start-rune = 189 + type-name = user + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 169 + input-source = decorator on relation and permission test + start-rune = 138 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 1 + end-rune = 155 + input-source = decorator on relation and permission test + start-rune = 147 + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = mode + decorator-parameter-value = range + end-rune = 168 + input-source = decorator on relation and permission test + start-rune = 158 + NodeTypePermission + end-rune = 238 + input-source = decorator on relation and permission test + relation-name = view + start-rune = 206 + compute-expression => + NodeTypeUnionExpression + end-rune = 238 + input-source = decorator on relation and permission test + start-rune = 224 + left-expr => + NodeTypeIdentifier + end-rune = 229 + identifier-value = viewer + input-source = decorator on relation and permission test + start-rune = 224 + right-expr => + NodeTypeIdentifier + end-rune = 238 + identifier-value = editor + input-source = decorator on relation and permission test + start-rune = 233 + decorator => + NodeTypeDecorator + decorator-name = testrel + end-rune = 203 + input-source = decorator on relation and permission test + start-rune = 196 \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/decorator_subjecttype.zed b/pkg/schemadsl/parser/tests/decorator_subjecttype.zed new file mode 100644 index 0000000000..0752d33c2d --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_subjecttype.zed @@ -0,0 +1,14 @@ +use testdecorators + +definition user {} + +definition document { + relation parent: @testsub document | folder + relation viewer: user | @testall(needed: 1) user:* | @testsub user with somecaveat +} + +definition folder {} + +caveat somecaveat(someparam int) { + someparam == 42 +} diff --git a/pkg/schemadsl/parser/tests/decorator_subjecttype.zed.expected b/pkg/schemadsl/parser/tests/decorator_subjecttype.zed.expected new file mode 100644 index 0000000000..79d42d1fff --- /dev/null +++ b/pkg/schemadsl/parser/tests/decorator_subjecttype.zed.expected @@ -0,0 +1,130 @@ +NodeTypeFile + end-rune = 269 + input-source = decorator on subject type test + start-rune = 0 + child-node => + NodeTypeUseFlag + end-rune = 17 + input-source = decorator on subject type test + start-rune = 0 + use-flag-name = testdecorators + NodeTypeDefinition + definition-name = user + end-rune = 37 + input-source = decorator on subject type test + start-rune = 20 + NodeTypeDefinition + definition-name = document + end-rune = 191 + input-source = decorator on subject type test + start-rune = 40 + child-node => + NodeTypeRelation + end-rune = 105 + input-source = decorator on subject type test + relation-name = parent + start-rune = 63 + allowed-types => + NodeTypeTypeReference + end-rune = 105 + input-source = decorator on subject type test + start-rune = 80 + type-ref-type => + NodeTypeSpecificTypeReference + end-rune = 96 + input-source = decorator on subject type test + start-rune = 89 + type-name = document + decorator => + NodeTypeDecorator + decorator-name = testsub + end-rune = 87 + input-source = decorator on subject type test + start-rune = 80 + NodeTypeSpecificTypeReference + end-rune = 105 + input-source = decorator on subject type test + start-rune = 100 + type-name = folder + NodeTypeRelation + end-rune = 189 + input-source = decorator on subject type test + relation-name = viewer + start-rune = 108 + allowed-types => + NodeTypeTypeReference + end-rune = 189 + input-source = decorator on subject type test + start-rune = 125 + type-ref-type => + NodeTypeSpecificTypeReference + end-rune = 128 + input-source = decorator on subject type test + start-rune = 125 + type-name = user + NodeTypeSpecificTypeReference + end-rune = 157 + input-source = decorator on subject type test + start-rune = 152 + type-name = user + type-wildcard = true + decorator => + NodeTypeDecorator + decorator-name = testall + end-rune = 150 + input-source = decorator on subject type test + start-rune = 132 + decorator-parameters => + NodeTypeDecoratorParameter + decorator-parameter-kind = identifier + decorator-parameter-name = needed + decorator-parameter-value = 1 + end-rune = 149 + input-source = decorator on subject type test + start-rune = 141 + NodeTypeSpecificTypeReference + end-rune = 189 + input-source = decorator on subject type test + start-rune = 170 + type-name = user + caveat => + NodeTypeCaveatReference + caveat-name = somecaveat + end-rune = 189 + input-source = decorator on subject type test + start-rune = 180 + decorator => + NodeTypeDecorator + decorator-name = testsub + end-rune = 168 + input-source = decorator on subject type test + start-rune = 161 + NodeTypeDefinition + definition-name = folder + end-rune = 213 + input-source = decorator on subject type test + start-rune = 194 + NodeTypeCaveatDefinition + caveat-definition-name = somecaveat + end-rune = 268 + input-source = decorator on subject type test + start-rune = 216 + caveat-definition-expression => + NodeTypeCaveatExpression + caveat-expression-expressionstr = someparam == 42 + + end-rune = 267 + input-source = decorator on subject type test + start-rune = 252 + parameters => + NodeTypeCaveatParameter + caveat-parameter-name = someparam + end-rune = 246 + input-source = decorator on subject type test + start-rune = 234 + caveat-parameter-type => + NodeTypeCaveatTypeReference + end-rune = 246 + input-source = decorator on subject type test + start-rune = 244 + type-name = int \ No newline at end of file diff --git a/pkg/schemadsl/parser/tests/invaliduse.zed.expected b/pkg/schemadsl/parser/tests/invaliduse.zed.expected index d0b0653ea2..eeb0e9abda 100644 --- a/pkg/schemadsl/parser/tests/invaliduse.zed.expected +++ b/pkg/schemadsl/parser/tests/invaliduse.zed.expected @@ -10,7 +10,7 @@ NodeTypeFile child-node => NodeTypeError end-rune = 12 - error-message = Unknown use flag: `something`. Options are: expiration, import, partial, self, typechecking + error-message = Unknown use flag: `something`. Options are: expiration, import, partial, self, testdecorators, typechecking error-source = input-source = invalid use diff --git a/proto/internal/core/v1/core.proto b/proto/internal/core/v1/core.proto index 527c125058..8311411131 100644 --- a/proto/internal/core/v1/core.proto +++ b/proto/internal/core/v1/core.proto @@ -75,6 +75,9 @@ message CaveatDefinition { /** source_position contains the position of the caveat in the source schema, if any */ SourcePosition source_position = 5; + + /** decorators are the decorators applied to this caveat */ + repeated Decorator decorators = 6; } message CaveatTypeReference { @@ -182,6 +185,43 @@ message Metadata { ]; } +/** + * Decorator is a `@name(param: value)` annotation applied to a definition, + * relation, permission, caveat or subject type in a schema. + */ +message Decorator { + /** name is the decorator's name, without the leading `@` */ + string name = 1 [(buf.validate.field).string = { + pattern: "^[a-z][a-z0-9_]{0,62}[a-z0-9]$" + max_bytes: 64 + }]; + + /** parameters are the decorator's arguments, in source order */ + repeated DecoratorParameter parameters = 2; + + /** + * required_flag is the `use` feature flag that enables this decorator. It is stored + * so that schema generation can re-emit the necessary `use` lines without consulting + * the decorator registry. + */ + string required_flag = 3; +} + +/** DecoratorParameter is a single named argument to a Decorator. */ +message DecoratorParameter { + string name = 1 [(buf.validate.field).string = { + pattern: "^[a-z][a-z0-9_]{0,62}[a-z0-9]$" + max_bytes: 64 + }]; + + oneof value { + int64 int_value = 2; + string string_value = 3; + bool bool_value = 4; + string enum_value = 5; + } +} + /** * NamespaceDefinition represents a single definition of an object type */ @@ -200,6 +240,9 @@ message NamespaceDefinition { /** source_position contains the position of the namespace in the source schema, if any */ SourcePosition source_position = 4; + + /** decorators are the decorators applied to this definition */ + repeated Decorator decorators = 5; } /** @@ -229,6 +272,9 @@ message Relation { string aliasing_relation = 6; string canonical_cache_key = 7; + + /** decorators are the decorators applied to this relation or permission */ + repeated Decorator decorators = 8; } /** @@ -421,6 +467,9 @@ message AllowedRelation { * required_expiration defines the required expiration on this relation. */ ExpirationTrait required_expiration = 7; + + /** decorators are the decorators applied to this subject type */ + repeated Decorator decorators = 8; } /**