Skip to content

Commit cba3045

Browse files
support relation deprecations
Signed-off-by: Kartikay <[email protected]>
1 parent 2381194 commit cba3045

File tree

22 files changed

+1076
-527
lines changed

22 files changed

+1076
-527
lines changed

internal/services/shared/errors.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ type SchemaWriteDataValidationError struct {
5252
error
5353
}
5454

55+
type DeprecationError struct {
56+
error
57+
}
58+
59+
func NewDeprecationError(namespace string, relation string) DeprecationError {
60+
return DeprecationError{
61+
error: fmt.Errorf("relation %s#%s is deprecated", namespace, relation),
62+
}
63+
}
64+
5565
// MarshalZerologObject implements zerolog object marshalling.
5666
func (err SchemaWriteDataValidationError) MarshalZerologObject(e *zerolog.Event) {
5767
e.Err(err.error)
@@ -201,6 +211,9 @@ func rewriteError(ctx context.Context, err error, config *ConfigForErrors) error
201211
}
202212

203213
return status.Errorf(codes.Canceled, "%s", err)
214+
case errors.As(err, &DeprecationError{}):
215+
log.Ctx(ctx).Err(err).Msg("using deprecated relation")
216+
return status.Errorf(codes.Aborted, "%s", err)
204217
default:
205218
log.Ctx(ctx).Err(err).Msg("received unexpected error")
206219
return err

internal/services/v1/relationships.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
1717

1818
"github.com/authzed/spicedb/internal/dispatch"
19+
log "github.com/authzed/spicedb/internal/logging"
1920
"github.com/authzed/spicedb/internal/middleware"
2021
datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
2122
"github.com/authzed/spicedb/internal/middleware/handwrittenvalidation"
@@ -34,6 +35,7 @@ import (
3435
"github.com/authzed/spicedb/pkg/genutil"
3536
"github.com/authzed/spicedb/pkg/genutil/mapz"
3637
"github.com/authzed/spicedb/pkg/middleware/consistency"
38+
corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
3739
dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
3840
"github.com/authzed/spicedb/pkg/tuple"
3941
"github.com/authzed/spicedb/pkg/zedtoken"
@@ -324,6 +326,10 @@ func (ps *permissionServer) WriteRelationships(ctx context.Context, req *v1.Writ
324326
updateRelationshipSet := mapz.NewSet[string]()
325327
for _, update := range req.Updates {
326328
// TODO(jschorr): Change to struct-based keys.
329+
if err := checkForDeprecatedRelationships(ctx, update, ds); err != nil {
330+
return nil, ps.rewriteError(ctx, err)
331+
}
332+
327333
tupleStr := tuple.V1StringRelationshipWithoutCaveatOrExpiration(update.Relationship)
328334
if !updateRelationshipSet.Add(tupleStr) {
329335
return nil, ps.rewriteError(
@@ -620,3 +626,29 @@ func labelsForFilter(filter *v1.RelationshipFilter) perfinsights.APIShapeLabels
620626
perfinsights.SubjectRelationLabel: filter.OptionalSubjectFilter.OptionalRelation.Relation,
621627
}
622628
}
629+
630+
func checkForDeprecatedRelationships(ctx context.Context, update *v1.RelationshipUpdate, ds datastore.Datastore) error {
631+
resource := update.Relationship.Resource
632+
headRevision, err := ds.HeadRevision(ctx)
633+
if err != nil {
634+
return err
635+
}
636+
reader := ds.SnapshotReader(headRevision)
637+
_, relDef, err := namespace.ReadNamespaceAndRelation(ctx, resource.ObjectType, update.Relationship.Relation, reader)
638+
if err != nil {
639+
return err
640+
}
641+
642+
switch relDef.DeprecationType {
643+
case corev1.DeprecationType_DEPRECATED_TYPE_WARNING:
644+
log.Warn().
645+
Str("namespace", update.Relationship.Resource.ObjectType).
646+
Str("relation", update.Relationship.Relation).
647+
Msg("write to deprecated relation")
648+
649+
case corev1.DeprecationType_DEPRECATED_TYPE_ERROR:
650+
return shared.NewDeprecationError(update.Relationship.Resource.ObjectType, update.Relationship.Relation)
651+
}
652+
653+
return nil
654+
}

internal/services/v1/schema_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,3 +1642,75 @@ 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 deprecation type warning.
1653+
originalSchema := `
1654+
definition user {}
1655+
1656+
definition document {
1657+
@deprecated(warn)
1658+
relation somerelation: user
1659+
}`
1660+
_, err := client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
1661+
Schema: originalSchema,
1662+
})
1663+
require.NoError(t, err)
1664+
1665+
// Write the relationship referencing the relation.
1666+
toWrite := tuple.MustParse("document:somedoc#somerelation@user:tom")
1667+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1668+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1669+
toWrite,
1670+
))},
1671+
})
1672+
require.Nil(t, err)
1673+
1674+
deprecatedErrSchema := `
1675+
definition user {}
1676+
1677+
definition document {
1678+
@deprecated(error)
1679+
relation somerelation: user
1680+
}`
1681+
1682+
// Enforce deprecation over the relation in the new schema.
1683+
_, err = client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
1684+
Schema: deprecatedErrSchema,
1685+
})
1686+
require.NoError(t, err)
1687+
1688+
// Attempt to write to a deprecated relation which should fail.
1689+
toWrite = tuple.MustParse("document:somedoc#somerelation@user:jerry")
1690+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1691+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1692+
toWrite,
1693+
))},
1694+
})
1695+
require.Equal(t, "rpc error: code = Aborted desc = relation document#somerelation is deprecated", err.Error())
1696+
1697+
// Change the schema to remove the deprecation type.
1698+
newSchema := `
1699+
definition user {}
1700+
1701+
definition document {
1702+
relation somerelation: user
1703+
}`
1704+
_, err = client.WriteSchema(t.Context(), &v1.WriteSchemaRequest{
1705+
Schema: newSchema,
1706+
})
1707+
require.NoError(t, err)
1708+
1709+
// Again attempt to write to the relation, which should now succeed.
1710+
_, err = v1client.WriteRelationships(t.Context(), &v1.WriteRelationshipsRequest{
1711+
Updates: []*v1.RelationshipUpdate{tuple.MustUpdateToV1RelationshipUpdate(tuple.Create(
1712+
toWrite,
1713+
))},
1714+
})
1715+
require.NoError(t, err)
1716+
}

pkg/diff/namespace/diff.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const (
6060

6161
// ChangedRelationComment indicates that the comment of the relation has changed in some way.
6262
ChangedRelationComment DeltaType = "changed-relation-comment"
63+
64+
// ChangedDeprecation indicates that the deprecation status of the relation has changed.
65+
ChangedDeprecation DeltaType = "changed-deprecation"
6366
)
6467

6568
// Diff holds the diff between two namespaces.
@@ -240,6 +243,14 @@ func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceD
240243
})
241244
}
242245

246+
// Compare deprecation status
247+
if existingRel.DeprecationType != updatedRel.DeprecationType {
248+
deltas = append(deltas, Delta{
249+
Type: ChangedDeprecation,
250+
RelationName: shared,
251+
})
252+
}
253+
243254
// Compare comments.
244255
existingComments := nspkg.GetComments(existingRel.Metadata)
245256
updatedComments := nspkg.GetComments(updatedRel.Metadata)

pkg/diff/namespace/diff_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,34 @@ func TestNamespaceDiff(t *testing.T) {
571571
),
572572
[]Delta{},
573573
},
574+
{
575+
"deprecate relation with an error type",
576+
ns.Namespace(
577+
"document",
578+
ns.MustRelation("somerel", nil, ns.AllowedDeprecatedRelation("foo", "bar", core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED)),
579+
),
580+
ns.Namespace(
581+
"document",
582+
ns.MustRelation("somerel", nil, ns.AllowedDeprecatedRelation("foo", "bar", core.DeprecationType_DEPRECATED_TYPE_ERROR)),
583+
),
584+
[]Delta{
585+
{Type: ChangedDeprecation, RelationName: "somerel"},
586+
},
587+
},
588+
{
589+
"remove deprecation",
590+
ns.Namespace(
591+
"document",
592+
ns.MustRelation("somerel", nil, ns.AllowedDeprecatedRelation("foo", "bar", core.DeprecationType_DEPRECATED_TYPE_ERROR)),
593+
),
594+
ns.Namespace(
595+
"document",
596+
ns.MustRelation("somerel", nil, ns.AllowedRelation("foo", "bar")),
597+
),
598+
[]Delta{
599+
{Type: ChangedDeprecation, RelationName: "somerel"},
600+
},
601+
},
574602
}
575603

576604
for _, tc := range testCases {

pkg/namespace/builder.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ func Relation(name string, rewrite *core.UsersetRewrite, allowedDirectRelations
4646
TypeInformation: typeInfo,
4747
}
4848

49+
if err := setRelationDeprecationType(rel, allowedDirectRelations...); err != nil {
50+
return nil, spiceerrors.MustBugf("failed to set deprecation type: %s", err.Error())
51+
}
52+
4953
switch {
5054
case rewrite != nil && len(allowedDirectRelations) == 0:
5155
if err := SetRelationKind(rel, iv1.RelationMetadata_PERMISSION); err != nil {
@@ -94,6 +98,17 @@ func AllowedRelationWithCaveat(namespaceName string, relationName string, withCa
9498
}
9599
}
96100

101+
// AllowedDeprecatedRelation creates a relation reference to an allowed relation that is deprecated.
102+
func AllowedDeprecatedRelation(namespaceName string, relationName string, deprecationType core.DeprecationType) *core.AllowedRelation {
103+
return &core.AllowedRelation{
104+
Namespace: namespaceName,
105+
RelationOrWildcard: &core.AllowedRelation_Relation{
106+
Relation: relationName,
107+
},
108+
DeprecationType: deprecationType,
109+
}
110+
}
111+
97112
// WithExpiration adds the expiration trait to the allowed relation.
98113
func WithExpiration(allowedRelation *core.AllowedRelation) *core.AllowedRelation {
99114
return &core.AllowedRelation{
@@ -243,6 +258,18 @@ func setOperation(firstChild *core.SetOperation_Child, rest []*core.SetOperation
243258
}
244259
}
245260

261+
// setRelationDeprecationType sets the deprecation type of a relation based on all of the deprecations of allowed direct relations.
262+
func setRelationDeprecationType(relation *core.Relation, allowedDirectRelations ...*core.AllowedRelation) error {
263+
if len(allowedDirectRelations) > 0 {
264+
for _, allowedRelation := range allowedDirectRelations {
265+
if allowedRelation.DeprecationType != core.DeprecationType_DEPRECATED_TYPE_UNSPECIFIED {
266+
relation.DeprecationType = allowedRelation.DeprecationType
267+
}
268+
}
269+
}
270+
return nil
271+
}
272+
246273
// Nil creates a child for a set operation that references the empty set.
247274
func Nil() *core.SetOperation_Child {
248275
return &core.SetOperation_Child{

0 commit comments

Comments
 (0)