Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/services/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func RegisterGrpcServices(
CaveatTypeSet: permSysConfig.CaveatTypeSet,
AdditiveOnly: schemaServiceOption == V1SchemaServiceAdditiveOnly,
ExpiringRelsEnabled: permSysConfig.ExpiringRelationshipsEnabled,
DeprecatedRelsEnabled: permSysConfig.DeprecatedRelationshipsEnabled,
PerformanceInsightMetricsEnabled: permSysConfig.PerformanceInsightMetricsEnabled,
}
v1.RegisterSchemaServiceServer(srv, v1svc.NewSchemaServer(schemaConfig))
Expand Down
18 changes: 18 additions & 0 deletions internal/services/shared/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@
error
}

type DeprecationError struct {
error
}

func NewDeprecationError(namespace string, relation string, comments string) DeprecationError {
if relation == "" {
return DeprecationError{
error: fmt.Errorf("object %s is deprecated, comments:%s", namespace, comments),
}
}
return DeprecationError{
error: fmt.Errorf("relation %s#%s is deprecated, comments:%s", namespace, relation, comments),
}

Check warning on line 67 in internal/services/shared/errors.go

View check run for this annotation

Codecov / codecov/patch

internal/services/shared/errors.go#L65-L67

Added lines #L65 - L67 were not covered by tests
}

// MarshalZerologObject implements zerolog object marshalling.
func (err SchemaWriteDataValidationError) MarshalZerologObject(e *zerolog.Event) {
e.Err(err.error)
Expand Down Expand Up @@ -201,6 +216,9 @@
}

return status.Errorf(codes.Canceled, "%s", err)
case errors.As(err, &DeprecationError{}):
log.Ctx(ctx).Err(err).Msg("using deprecated relation")
return status.Errorf(codes.Aborted, "%s", err)
default:
log.Ctx(ctx).Err(err).Msg("received unexpected error")
return err
Expand Down
79 changes: 79 additions & 0 deletions internal/services/v1/relationships.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"

"github.com/authzed/spicedb/internal/dispatch"
log "github.com/authzed/spicedb/internal/logging"
"github.com/authzed/spicedb/internal/middleware"
datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
"github.com/authzed/spicedb/internal/middleware/handwrittenvalidation"
Expand All @@ -34,6 +35,7 @@
"github.com/authzed/spicedb/pkg/genutil"
"github.com/authzed/spicedb/pkg/genutil/mapz"
"github.com/authzed/spicedb/pkg/middleware/consistency"
corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
"github.com/authzed/spicedb/pkg/tuple"
"github.com/authzed/spicedb/pkg/zedtoken"
Expand Down Expand Up @@ -103,6 +105,9 @@
// ExpiringRelationshipsEnabled defines whether or not expiring relationships are enabled.
ExpiringRelationshipsEnabled bool

// DeprecatedRelationshipsEnabled defines whether or not deprecated relationships are enabled.
DeprecatedRelationshipsEnabled bool

// CaveatTypeSet is the set of caveat types to use for caveats. If not specified,
// the default type set is used.
CaveatTypeSet *caveattypes.TypeSet
Expand Down Expand Up @@ -132,6 +137,7 @@
MaxCheckBulkConcurrency: defaultIfZero(config.MaxCheckBulkConcurrency, 50),
CaveatTypeSet: caveattypes.TypeSetOrDefault(config.CaveatTypeSet),
ExpiringRelationshipsEnabled: config.ExpiringRelationshipsEnabled,
DeprecatedRelationshipsEnabled: config.DeprecatedRelationshipsEnabled,
PerformanceInsightMetricsEnabled: config.PerformanceInsightMetricsEnabled,
}

Expand Down Expand Up @@ -324,6 +330,12 @@
updateRelationshipSet := mapz.NewSet[string]()
for _, update := range req.Updates {
// TODO(jschorr): Change to struct-based keys.
if ps.config.DeprecatedRelationshipsEnabled {
if err := checkForDeprecatedRelationsAndObjects(ctx, update, ds); err != nil {
return nil, ps.rewriteError(ctx, err)
}
}

tupleStr := tuple.V1StringRelationshipWithoutCaveatOrExpiration(update.Relationship)
if !updateRelationshipSet.Add(tupleStr) {
return nil, ps.rewriteError(
Expand Down Expand Up @@ -620,3 +632,70 @@
perfinsights.SubjectRelationLabel: filter.OptionalSubjectFilter.OptionalRelation.Relation,
}
}

func checkForDeprecatedRelationsAndObjects(ctx context.Context, update *v1.RelationshipUpdate, ds datastore.Datastore) error {
resource := update.Relationship.Resource
headRevision, err := ds.HeadRevision(ctx)
if err != nil {
return err
}

Check warning on line 641 in internal/services/v1/relationships.go

View check run for this annotation

Codecov / codecov/patch

internal/services/v1/relationships.go#L640-L641

Added lines #L640 - L641 were not covered by tests
reader := ds.SnapshotReader(headRevision)
_, relDef, err := namespace.ReadNamespaceAndRelation(ctx, resource.ObjectType, update.Relationship.Relation, reader)
if err != nil {
return err
}

if relDef.Deprecation != nil && relDef.Deprecation.DeprecationType != corev1.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
// Check if the relation is deprecated
switch relDef.Deprecation.DeprecationType {
case corev1.DeprecationType_DEPRECATED_TYPE_WARNING:
log.Warn().
Str("namespace", update.Relationship.Resource.ObjectType).
Str("relation", update.Relationship.Relation).
Str("comments", relDef.Deprecation.Comments).
Msg("write to deprecated relation")

case corev1.DeprecationType_DEPRECATED_TYPE_ERROR:
return shared.NewDeprecationError(update.Relationship.Resource.ObjectType, update.Relationship.Relation, relDef.Deprecation.Comments)

Check warning on line 659 in internal/services/v1/relationships.go

View check run for this annotation

Codecov / codecov/patch

internal/services/v1/relationships.go#L658-L659

Added lines #L658 - L659 were not covered by tests
}
}
nsdef, _, err := reader.ReadNamespaceByName(ctx, resource.ObjectType)
if err != nil {
return err
}

Check warning on line 665 in internal/services/v1/relationships.go

View check run for this annotation

Codecov / codecov/patch

internal/services/v1/relationships.go#L664-L665

Added lines #L664 - L665 were not covered by tests

// Check if the resource is deprecated
if nsdef.Deprecation != nil && nsdef.Deprecation.DeprecationType != corev1.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
// Check if the namespace is deprecated
switch nsdef.Deprecation.DeprecationType {
case corev1.DeprecationType_DEPRECATED_TYPE_WARNING:
log.Warn().
Str("namespace", nsdef.Name).
Str("comments", nsdef.Deprecation.Comments).
Msg("write to deprecated object")
case corev1.DeprecationType_DEPRECATED_TYPE_ERROR:
return shared.NewDeprecationError(resource.ObjectType, "", nsdef.Deprecation.Comments)

Check warning on line 677 in internal/services/v1/relationships.go

View check run for this annotation

Codecov / codecov/patch

internal/services/v1/relationships.go#L669-L677

Added lines #L669 - L677 were not covered by tests
}
}

objectRef := update.Relationship.Subject
nsdef, _, err = reader.ReadNamespaceByName(ctx, objectRef.Object.ObjectType)
if err != nil {
return err
}

if nsdef.Deprecation != nil && nsdef.Deprecation.DeprecationType != corev1.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
// Check if the subject is deprecated
switch nsdef.Deprecation.DeprecationType {
case corev1.DeprecationType_DEPRECATED_TYPE_WARNING:
log.Warn().
Str("namespace", nsdef.Name).
Str("comments", nsdef.Deprecation.Comments).
Msg("write to deprecated object")

Check warning on line 694 in internal/services/v1/relationships.go

View check run for this annotation

Codecov / codecov/patch

internal/services/v1/relationships.go#L690-L694

Added lines #L690 - L694 were not covered by tests
case corev1.DeprecationType_DEPRECATED_TYPE_ERROR:
return shared.NewDeprecationError(resource.ObjectType, "", nsdef.Deprecation.Comments)
}
}

return nil
}
20 changes: 14 additions & 6 deletions internal/services/v1/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ type SchemaServerConfig struct {
// ExpiringRelsEnabled indicates whether expiring relationships are enabled.
ExpiringRelsEnabled bool

// DeprecatedRelsEnabled indicates whether deprecated relations are enabled.
DeprecatedRelsEnabled bool

// PerformanceInsightMetricsEnabled indicates whether performance insight metrics are enabled.
PerformanceInsightMetricsEnabled bool
}
Expand All @@ -61,19 +64,21 @@ func NewSchemaServer(config SchemaServerConfig) v1.SchemaServiceServer {
perfinsights.StreamServerInterceptor(config.PerformanceInsightMetricsEnabled),
),
},
additiveOnly: config.AdditiveOnly,
expiringRelsEnabled: config.ExpiringRelsEnabled,
caveatTypeSet: cts,
additiveOnly: config.AdditiveOnly,
expiringRelsEnabled: config.ExpiringRelsEnabled,
deprecatedRelsEnabled: config.DeprecatedRelsEnabled,
caveatTypeSet: cts,
}
}

type schemaServer struct {
v1.UnimplementedSchemaServiceServer
shared.WithServiceSpecificInterceptors

caveatTypeSet *caveattypes.TypeSet
additiveOnly bool
expiringRelsEnabled bool
caveatTypeSet *caveattypes.TypeSet
additiveOnly bool
expiringRelsEnabled bool
deprecatedRelsEnabled bool
}

func (ss *schemaServer) rewriteError(ctx context.Context, err error) error {
Expand Down Expand Up @@ -148,6 +153,9 @@ func (ss *schemaServer) WriteSchema(ctx context.Context, in *v1.WriteSchemaReque
opts = append(opts, compiler.DisallowExpirationFlag())
}

if !ss.deprecatedRelsEnabled {
opts = append(opts, compiler.DisallowDeprecationFlag())
}
opts = append(opts, compiler.CaveatTypeSet(ss.caveatTypeSet))

compiled, err := compiler.Compile(compiler.InputSchema{
Expand Down
67 changes: 67 additions & 0 deletions internal/services/v1/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1642,3 +1642,70 @@ func TestComputablePermissions(t *testing.T) {
})
}
}

func TestSchemaChangeRelationDeprecation(t *testing.T) {
conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore)
t.Cleanup(cleanup)
client := v1.NewSchemaServiceClient(conn)
v1client := v1.NewPermissionsServiceClient(conn)

// Write a basic schema with deprecations.
originalSchema := `
use deprecation
definition user {}
definition testuser {}

definition document {
relation somerelation: user
relation otherelation: testuser
}

@deprecated(warn, document#somerelation, "This relation is deprecated, please use otherelation instead.")
@deprecated(error, testuser, "super deprecated")
`
_, err := client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
Schema: originalSchema,
})
require.NoError(t, err)

// Write the relationship referencing the relation.
toWrite := tuple.MustParse("document:somedoc#somerelation@user:tom")
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
toWrite,
))},
})
require.Nil(t, err)

// Attempt to write to a deprecated object which should fail.
toWrite = tuple.MustParse("document:somedoc#otherelation@testuser:jerry")
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
toWrite,
))},
})
require.Equal(t, "rpc error: code = Aborted desc = object document is deprecated, comments:super deprecated", err.Error())

// Change the schema to remove the deprecation type.
newSchema := `
use deprecation
definition user {}
definition testuser {}

definition document {
relation somerelation: user
relation otherelation: testuser
}`
_, err = client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
Schema: newSchema,
})
require.NoError(t, err)

// Again attempt to write to the relation, which should now succeed.
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
toWrite,
))},
})
require.NoError(t, err)
}
1 change: 1 addition & 0 deletions internal/testserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func NewTestServerWithConfigAndDatastore(require *require.Assertions,
cts := caveattypes.TypeSetOrDefault(config.CaveatTypeSet)
srv, err := server.NewConfigWithOptionsAndDefaults(
server.WithEnableExperimentalRelationshipExpiration(true),
server.WithEnableExperimentalRelationshipDeprecation(true),
server.WithDatastore(ds),
server.WithDispatcher(graph.NewLocalOnlyDispatcher(cts, 10, 100)),
server.WithDispatchMaxDepth(50),
Expand Down
1 change: 1 addition & 0 deletions pkg/cmd/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ func RegisterServeFlags(cmd *cobra.Command, config *server.Config) error {
}

experimentalFlags.BoolVar(&config.EnableExperimentalRelationshipExpiration, "enable-experimental-relationship-expiration", false, "enables experimental support for relationship expiration")
experimentalFlags.BoolVar(&config.EnableExperimentalRelationshipDeprecation, "enable-experimental-relationship-deprecation", false, "enables experimental support for deprecating relations")
experimentalFlags.BoolVar(&config.EnableExperimentalWatchableSchemaCache, "enable-experimental-watchable-schema-cache", false, "enables the experimental schema cache, which uses the Watch API to keep the schema up to date")
// TODO: these two could reasonably be put in either the Dispatch group or the Experimental group. Is there a preference?
experimentalFlags.StringToStringVar(&config.DispatchSecondaryUpstreamAddrs, "experimental-dispatch-secondary-upstream-addrs", nil, "secondary upstream addresses for dispatches, each with a name")
Expand Down
32 changes: 17 additions & 15 deletions pkg/cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,22 @@ type Config struct {
ClusterDispatchCacheConfig CacheConfig `debugmap:"visible"`

// API Behavior
DisableV1SchemaAPI bool `debugmap:"visible"`
V1SchemaAdditiveOnly bool `debugmap:"visible"`
MaximumUpdatesPerWrite uint16 `debugmap:"visible"`
MaximumPreconditionCount uint16 `debugmap:"visible"`
MaxDatastoreReadPageSize uint64 `debugmap:"visible"`
StreamingAPITimeout time.Duration `debugmap:"visible"`
WatchHeartbeat time.Duration `debugmap:"visible"`
MaxReadRelationshipsLimit uint32 `debugmap:"visible"`
MaxDeleteRelationshipsLimit uint32 `debugmap:"visible"`
MaxLookupResourcesLimit uint32 `debugmap:"visible"`
MaxBulkExportRelationshipsLimit uint32 `debugmap:"visible"`
EnableExperimentalLookupResources bool `debugmap:"visible"`
EnableExperimentalRelationshipExpiration bool `debugmap:"visible"`
EnableRevisionHeartbeat bool `debugmap:"visible"`
EnablePerformanceInsightMetrics bool `debugmap:"visible"`
DisableV1SchemaAPI bool `debugmap:"visible"`
V1SchemaAdditiveOnly bool `debugmap:"visible"`
MaximumUpdatesPerWrite uint16 `debugmap:"visible"`
MaximumPreconditionCount uint16 `debugmap:"visible"`
MaxDatastoreReadPageSize uint64 `debugmap:"visible"`
StreamingAPITimeout time.Duration `debugmap:"visible"`
WatchHeartbeat time.Duration `debugmap:"visible"`
MaxReadRelationshipsLimit uint32 `debugmap:"visible"`
MaxDeleteRelationshipsLimit uint32 `debugmap:"visible"`
MaxLookupResourcesLimit uint32 `debugmap:"visible"`
MaxBulkExportRelationshipsLimit uint32 `debugmap:"visible"`
EnableExperimentalLookupResources bool `debugmap:"visible"`
EnableExperimentalRelationshipExpiration bool `debugmap:"visible"`
EnableExperimentalRelationshipDeprecation bool `debugmap:"visible"`
EnableRevisionHeartbeat bool `debugmap:"visible"`
EnablePerformanceInsightMetrics bool `debugmap:"visible"`

// Additional Services
MetricsAPI util.HTTPServerConfig `debugmap:"visible"`
Expand Down Expand Up @@ -455,6 +456,7 @@ func (c *Config) Complete(ctx context.Context) (RunnableServer, error) {
MaxBulkExportRelationshipsLimit: c.MaxBulkExportRelationshipsLimit,
DispatchChunkSize: c.DispatchChunkSize,
ExpiringRelationshipsEnabled: c.EnableExperimentalRelationshipExpiration,
DeprecatedRelationshipsEnabled: c.EnableExperimentalRelationshipDeprecation,
CaveatTypeSet: c.DatastoreConfig.CaveatTypeSet,
PerformanceInsightMetricsEnabled: c.EnablePerformanceInsightMetrics,
}
Expand Down
9 changes: 9 additions & 0 deletions pkg/cmd/server/zz_generated.options.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/cmd/testserver/testserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
MaxBulkExportRelationshipsLimit: c.MaxBulkExportRelationshipsLimit,
DispatchChunkSize: defaultMaxChunkSize,
ExpiringRelationshipsEnabled: true,
DeprecatedRelationshipsEnabled: true,

Check warning on line 90 in pkg/cmd/testserver/testserver.go

View check run for this annotation

Codecov / codecov/patch

pkg/cmd/testserver/testserver.go#L90

Added line #L90 was not covered by tests
CaveatTypeSet: cts,
},
1*time.Second,
Expand Down
2 changes: 2 additions & 0 deletions pkg/development/devcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,13 +173,15 @@ func (dc *DevContext) RunV1InMemoryService() (*grpc.ClientConn, func(), error) {
MaximumAPIDepth: 50,
MaxCaveatContextSize: 0,
ExpiringRelationshipsEnabled: true,
DeprecatedRelationshipsEnabled: true,
CaveatTypeSet: caveattypes.Default.TypeSet,
PerformanceInsightMetricsEnabled: false,
})
ss := v1svc.NewSchemaServer(v1svc.SchemaServerConfig{
CaveatTypeSet: caveattypes.Default.TypeSet,
AdditiveOnly: false,
ExpiringRelsEnabled: true,
DeprecatedRelsEnabled: true,
PerformanceInsightMetricsEnabled: false,
})

Expand Down
Loading
Loading