Skip to content

Commit b5137fc

Browse files
support for rel and namespace deprecation
Signed-off-by: Kartikay <[email protected]>
1 parent f05b700 commit b5137fc

39 files changed

+2180
-712
lines changed

internal/relationships/validation.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"github.com/authzed/spicedb/internal/namespace"
7+
"github.com/authzed/spicedb/internal/services/shared"
78
"github.com/authzed/spicedb/pkg/caveats"
89
caveattypes "github.com/authzed/spicedb/pkg/caveats/types"
910
"github.com/authzed/spicedb/pkg/datastore"
@@ -14,6 +15,7 @@ import (
1415
"github.com/authzed/spicedb/pkg/schema"
1516
"github.com/authzed/spicedb/pkg/spiceerrors"
1617
"github.com/authzed/spicedb/pkg/tuple"
18+
"github.com/rs/zerolog/log"
1719
)
1820

1921
// ValidateRelationshipUpdates performs validation on the given relationship updates, ensuring that
@@ -188,6 +190,11 @@ func ValidateOneRelationship(
188190
return NewCannotWriteToPermissionError(rel)
189191
}
190192

193+
// Validate if the resource, relation or subject is deprecated
194+
if err := checkForDeprecatedRelationsAndObjects(rel, namespaceMap); err != nil {
195+
return err
196+
}
197+
191198
// Validate the subject against the allowed relation(s).
192199
var caveat *core.AllowedCaveat
193200
if rel.OptionalCaveat != nil {
@@ -277,3 +284,53 @@ func hasNonEmptyCaveatContext(relationship tuple.Relationship) bool {
277284
relationship.OptionalCaveat.Context != nil &&
278285
len(relationship.OptionalCaveat.Context.GetFields()) > 0
279286
}
287+
288+
func checkForDeprecatedRelationsAndObjects(rel tuple.Relationship, nsMap map[string]*schema.Definition) error {
289+
// Validate if the resource relation is deprecated
290+
resource := nsMap[rel.Resource.ObjectType]
291+
relDef, ok := resource.GetRelation(rel.Resource.Relation)
292+
if relDef.Deprecation != nil && relDef.Deprecation.DeprecationType != core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED && ok {
293+
switch relDef.Deprecation.DeprecationType {
294+
case core.DeprecationType_DEPRECATED_TYPE_WARNING:
295+
log.Warn().
296+
Str("namespace", rel.Resource.ObjectType).
297+
Str("relation", rel.Resource.Relation).
298+
Str("comments", relDef.Deprecation.Comments).
299+
Msg("write to deprecated relation")
300+
case core.DeprecationType_DEPRECATED_TYPE_ERROR:
301+
return shared.NewDeprecationError(rel.Resource.ObjectType, rel.Resource.Relation, relDef.Deprecation.Comments)
302+
}
303+
}
304+
305+
// Validate if the resource namespace is deprecated
306+
resourceNS := resource.Namespace()
307+
if resourceNS.Deprecation != nil && resourceNS.Deprecation.DeprecationType != core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
308+
switch resourceNS.Deprecation.DeprecationType {
309+
case core.DeprecationType_DEPRECATED_TYPE_WARNING:
310+
log.Warn().
311+
Str("namespace", rel.Resource.ObjectType).
312+
Str("comments", resourceNS.Deprecation.Comments).
313+
Msg("write to deprecated object")
314+
case core.DeprecationType_DEPRECATED_TYPE_ERROR:
315+
return shared.NewDeprecationError(rel.Resource.ObjectType, "", resourceNS.Deprecation.Comments)
316+
}
317+
}
318+
319+
// Validate if the subject namespace is deprecated
320+
subject := nsMap[rel.Subject.ObjectType]
321+
subjectNS := subject.Namespace()
322+
if subjectNS.Deprecation != nil &&
323+
subjectNS.Deprecation.DeprecationType != core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
324+
switch subjectNS.Deprecation.DeprecationType {
325+
case core.DeprecationType_DEPRECATED_TYPE_WARNING:
326+
log.Warn().
327+
Str("namespace", rel.Subject.ObjectType).
328+
Str("comments", subjectNS.Deprecation.Comments).
329+
Msg("write to deprecated object")
330+
case core.DeprecationType_DEPRECATED_TYPE_ERROR:
331+
return shared.NewDeprecationError(rel.Subject.ObjectType, "", subjectNS.Deprecation.Comments)
332+
}
333+
}
334+
335+
return nil
336+
}

internal/relationships/validation_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,21 @@ func TestValidateRelationshipOperations(t *testing.T) {
306306
core.RelationTupleUpdate_CREATE,
307307
"subjects of type `user with somecaveat and expiration` are not allowed on relation `resource#viewer`",
308308
},
309+
{
310+
"deprecation namespace test",
311+
`use deprecation
312+
definition testuser {}
313+
definition user {}
314+
315+
definition document {
316+
@deprecated(error,"deprecated, migrate away")
317+
relation editor: testuser
318+
relation viewer: user
319+
}`,
320+
"document:foo#editor@testuser:tom",
321+
core.RelationTupleUpdate_CREATE,
322+
"the relation document#editor is deprecated: deprecated, migrate away",
323+
},
309324
}
310325

311326
for _, tc := range tcs {

internal/services/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func RegisterGrpcServices(
7373
CaveatTypeSet: permSysConfig.CaveatTypeSet,
7474
AdditiveOnly: schemaServiceOption == V1SchemaServiceAdditiveOnly,
7575
ExpiringRelsEnabled: permSysConfig.ExpiringRelationshipsEnabled,
76+
FeatureDeprecationEnabled: permSysConfig.DeprecatedRelationshipsAndObjectsEnabled,
7677
PerformanceInsightMetricsEnabled: permSysConfig.PerformanceInsightMetricsEnabled,
7778
}
7879
v1.RegisterSchemaServiceServer(srv, v1svc.NewSchemaServer(schemaConfig))

internal/services/shared/errors.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,43 @@ func NewSchemaWriteDataValidationError(message string, args ...any) SchemaWriteD
4747
}
4848
}
4949

50+
// DeprecationError is an error returned when a schema object or relation is deprecated.
51+
type DeprecationError struct {
52+
error
53+
}
54+
55+
func (err DeprecationError) GRPCStatus() *status.Status {
56+
return spiceerrors.WithCodeAndDetails(
57+
err,
58+
codes.Aborted,
59+
spiceerrors.ForReason(
60+
v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR,
61+
map[string]string{},
62+
),
63+
)
64+
}
65+
66+
func NewDeprecationError(namespace, relation, comments string) DeprecationError {
67+
switch {
68+
case relation == "" && comments != "":
69+
return DeprecationError{
70+
error: fmt.Errorf("the object type %s is deprecated: %s", namespace, comments),
71+
}
72+
case relation == "":
73+
return DeprecationError{
74+
error: fmt.Errorf("the object type %s has been marked as deprecated", namespace),
75+
}
76+
case comments != "":
77+
return DeprecationError{
78+
error: fmt.Errorf("the relation %s#%s is deprecated: %s", namespace, relation, comments),
79+
}
80+
default:
81+
return DeprecationError{
82+
error: fmt.Errorf("the relation %s#%s has been marked as deprecated", namespace, relation),
83+
}
84+
}
85+
}
86+
5087
// SchemaWriteDataValidationError occurs when a schema cannot be applied due to leaving data unreferenced.
5188
type SchemaWriteDataValidationError struct {
5289
error

internal/services/v1/relationships.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ type PermissionsServerConfig struct {
103103
// ExpiringRelationshipsEnabled defines whether or not expiring relationships are enabled.
104104
ExpiringRelationshipsEnabled bool
105105

106+
// DeprecatedRelationshipsEnabled defines whether or not deprecated relationships are enabled.
107+
DeprecatedRelationshipsAndObjectsEnabled bool
108+
106109
// CaveatTypeSet is the set of caveat types to use for caveats. If not specified,
107110
// the default type set is used.
108111
CaveatTypeSet *caveattypes.TypeSet
@@ -117,22 +120,23 @@ func NewPermissionsServer(
117120
config PermissionsServerConfig,
118121
) v1.PermissionsServiceServer {
119122
configWithDefaults := PermissionsServerConfig{
120-
MaxPreconditionsCount: defaultIfZero(config.MaxPreconditionsCount, 1000),
121-
MaxUpdatesPerWrite: defaultIfZero(config.MaxUpdatesPerWrite, 1000),
122-
MaximumAPIDepth: defaultIfZero(config.MaximumAPIDepth, 50),
123-
StreamingAPITimeout: defaultIfZero(config.StreamingAPITimeout, 30*time.Second),
124-
MaxCaveatContextSize: defaultIfZero(config.MaxCaveatContextSize, 4096),
125-
MaxRelationshipContextSize: defaultIfZero(config.MaxRelationshipContextSize, 25_000),
126-
MaxDatastoreReadPageSize: defaultIfZero(config.MaxDatastoreReadPageSize, 1_000),
127-
MaxReadRelationshipsLimit: defaultIfZero(config.MaxReadRelationshipsLimit, 1_000),
128-
MaxDeleteRelationshipsLimit: defaultIfZero(config.MaxDeleteRelationshipsLimit, 1_000),
129-
MaxLookupResourcesLimit: defaultIfZero(config.MaxLookupResourcesLimit, 1_000),
130-
MaxBulkExportRelationshipsLimit: defaultIfZero(config.MaxBulkExportRelationshipsLimit, 100_000),
131-
DispatchChunkSize: defaultIfZero(config.DispatchChunkSize, 100),
132-
MaxCheckBulkConcurrency: defaultIfZero(config.MaxCheckBulkConcurrency, 50),
133-
CaveatTypeSet: caveattypes.TypeSetOrDefault(config.CaveatTypeSet),
134-
ExpiringRelationshipsEnabled: config.ExpiringRelationshipsEnabled,
135-
PerformanceInsightMetricsEnabled: config.PerformanceInsightMetricsEnabled,
123+
MaxPreconditionsCount: defaultIfZero(config.MaxPreconditionsCount, 1000),
124+
MaxUpdatesPerWrite: defaultIfZero(config.MaxUpdatesPerWrite, 1000),
125+
MaximumAPIDepth: defaultIfZero(config.MaximumAPIDepth, 50),
126+
StreamingAPITimeout: defaultIfZero(config.StreamingAPITimeout, 30*time.Second),
127+
MaxCaveatContextSize: defaultIfZero(config.MaxCaveatContextSize, 4096),
128+
MaxRelationshipContextSize: defaultIfZero(config.MaxRelationshipContextSize, 25_000),
129+
MaxDatastoreReadPageSize: defaultIfZero(config.MaxDatastoreReadPageSize, 1_000),
130+
MaxReadRelationshipsLimit: defaultIfZero(config.MaxReadRelationshipsLimit, 1_000),
131+
MaxDeleteRelationshipsLimit: defaultIfZero(config.MaxDeleteRelationshipsLimit, 1_000),
132+
MaxLookupResourcesLimit: defaultIfZero(config.MaxLookupResourcesLimit, 1_000),
133+
MaxBulkExportRelationshipsLimit: defaultIfZero(config.MaxBulkExportRelationshipsLimit, 100_000),
134+
DispatchChunkSize: defaultIfZero(config.DispatchChunkSize, 100),
135+
MaxCheckBulkConcurrency: defaultIfZero(config.MaxCheckBulkConcurrency, 50),
136+
CaveatTypeSet: caveattypes.TypeSetOrDefault(config.CaveatTypeSet),
137+
ExpiringRelationshipsEnabled: config.ExpiringRelationshipsEnabled,
138+
DeprecatedRelationshipsAndObjectsEnabled: config.DeprecatedRelationshipsAndObjectsEnabled,
139+
PerformanceInsightMetricsEnabled: config.PerformanceInsightMetricsEnabled,
136140
}
137141

138142
return &permissionServer{
@@ -324,6 +328,7 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ
324328
updateRelationshipSet := mapz.NewSet[string]()
325329
for _, update := range req.Updates {
326330
// TODO(jschorr): Change to struct-based keys.
331+
327332
tupleStr := tuple.V1StringRelationshipWithoutCaveatOrExpiration(update.Relationship)
328333
if !updateRelationshipSet.Add(tupleStr) {
329334
return nil, ps.rewriteError(

internal/services/v1/schema.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type SchemaServerConfig struct {
4343

4444
// PerformanceInsightMetricsEnabled indicates whether performance insight metrics are enabled.
4545
PerformanceInsightMetricsEnabled bool
46+
47+
// DeprecatedRelsEnabled indicates whether deprecated relations are enabled.
48+
FeatureDeprecationEnabled bool
4649
}
4750

4851
// NewSchemaServer creates a SchemaServiceServer instance.
@@ -61,19 +64,21 @@ func NewSchemaServer(config SchemaServerConfig) v1.SchemaServiceServer {
6164
perfinsights.StreamServerInterceptor(config.PerformanceInsightMetricsEnabled),
6265
),
6366
},
64-
additiveOnly: config.AdditiveOnly,
65-
expiringRelsEnabled: config.ExpiringRelsEnabled,
66-
caveatTypeSet: cts,
67+
additiveOnly: config.AdditiveOnly,
68+
expiringRelsEnabled: config.ExpiringRelsEnabled,
69+
featureDeprecationEnabled: config.FeatureDeprecationEnabled,
70+
caveatTypeSet: cts,
6771
}
6872
}
6973

7074
type schemaServer struct {
7175
v1.UnimplementedSchemaServiceServer
7276
shared.WithServiceSpecificInterceptors
7377

74-
caveatTypeSet *caveattypes.TypeSet
75-
additiveOnly bool
76-
expiringRelsEnabled bool
78+
caveatTypeSet *caveattypes.TypeSet
79+
additiveOnly bool
80+
expiringRelsEnabled bool
81+
featureDeprecationEnabled bool
7782
}
7883

7984
func (ss *schemaServer) rewriteError(ctx context.Context, err error) error {
@@ -147,6 +152,9 @@ func (ss *schemaServer) WriteSchema(ctx context.Context, in *v1.WriteSchemaReque
147152
if !ss.expiringRelsEnabled {
148153
opts = append(opts, compiler.DisallowExpirationFlag())
149154
}
155+
if !ss.featureDeprecationEnabled {
156+
opts = append(opts, compiler.DisallowDeprecationFlag())
157+
}
150158

151159
opts = append(opts, compiler.CaveatTypeSet(ss.caveatTypeSet))
152160

internal/services/v1/schema_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,3 +1642,71 @@ func TestComputablePermissions(t *testing.T) {
16421642
})
16431643
}
16441644
}
1645+
1646+
func TestSchemaChangeRelationDeprecation(t *testing.T) {
1647+
conn, cleanup, _, _ := testserver.NewTestServer(require.New(t), 0, memdb.DisableGC, true, tf.EmptyDatastore)
1648+
t.Cleanup(cleanup)
1649+
client := v1.NewSchemaServiceClient(conn)
1650+
v1client := v1.NewPermissionsServiceClient(conn)
1651+
1652+
// Write a basic schema with deprecations.
1653+
originalSchema := `
1654+
use deprecation
1655+
1656+
definition user {}
1657+
1658+
@deprecated(error, "super deprecated")
1659+
definition testuser {}
1660+
1661+
definition document {
1662+
relation somerelation: user
1663+
1664+
@deprecated(warn)
1665+
relation otherelation: testuser
1666+
}
1667+
`
1668+
_, err := client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
1669+
Schema: originalSchema,
1670+
})
1671+
require.NoError(t, err)
1672+
1673+
// Write the relationship referencing the relation.
1674+
toWrite := tuple.MustParse("document:somedoc#somerelation@user:tom")
1675+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1676+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1677+
toWrite,
1678+
))},
1679+
})
1680+
require.Nil(t, err)
1681+
1682+
// Attempt to write to a deprecated object which should fail.
1683+
toWrite = tuple.MustParse("document:somedoc#otherelation@testuser:jerry")
1684+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1685+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1686+
toWrite,
1687+
))},
1688+
})
1689+
require.Equal(t, "rpc error: code = Aborted desc = the object type testuser is deprecated: super deprecated", err.Error())
1690+
1691+
// Change the schema to remove the deprecation type.
1692+
newSchema := `
1693+
use deprecation
1694+
definition user {}
1695+
definition testuser {}
1696+
definition document {
1697+
relation somerelation: user
1698+
relation otherelation: testuser
1699+
}`
1700+
_, err = client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
1701+
Schema: newSchema,
1702+
})
1703+
require.NoError(t, err)
1704+
1705+
// Again attempt to write to the relation, which should now succeed.
1706+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1707+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1708+
toWrite,
1709+
))},
1710+
})
1711+
require.NoError(t, err)
1712+
}

internal/testserver/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ func NewTestServerWithConfigAndDatastore(require *require.Assertions,
7676
cts := caveattypes.TypeSetOrDefault(config.CaveatTypeSet)
7777
srv, err := server.NewConfigWithOptionsAndDefaults(
7878
server.WithEnableExperimentalRelationshipExpiration(true),
79+
server.WithEnableExperimentalRelationshipDeprecation(true),
7980
server.WithDatastore(ds),
8081
server.WithDispatcher(graph.NewLocalOnlyDispatcher(cts, 10, 100)),
8182
server.WithDispatchMaxDepth(50),

pkg/cmd/serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ func RegisterServeFlags(cmd *cobra.Command, config *server.Config) error {
169169
}
170170

171171
experimentalFlags.BoolVar(&config.EnableExperimentalRelationshipExpiration, "enable-experimental-relationship-expiration", false, "enables experimental support for relationship expiration")
172+
experimentalFlags.BoolVar(&config.EnableExperimentalRelationshipDeprecation, "enable-experimental-deprecation", false, "enables experimental support for deprecating relations and objects")
172173
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")
173174
// TODO: these two could reasonably be put in either the Dispatch group or the Experimental group. Is there a preference?
174175
experimentalFlags.StringToStringVar(&config.DispatchSecondaryUpstreamAddrs, "experimental-dispatch-secondary-upstream-addrs", nil, "secondary upstream addresses for dispatches, each with a name")

0 commit comments

Comments
 (0)