Skip to content
Open
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
48 changes: 44 additions & 4 deletions internal/relationships/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/authzed/spicedb/internal/namespace"
"github.com/authzed/spicedb/internal/services/shared"
"github.com/authzed/spicedb/pkg/caveats"
caveattypes "github.com/authzed/spicedb/pkg/caveats/types"
"github.com/authzed/spicedb/pkg/datastore"
Expand All @@ -28,8 +29,9 @@ func ValidateRelationshipUpdates(
return item.Relationship
})

ts := schema.NewTypeSystem(schema.ResolverForDatastoreReader(reader))
// Load namespaces and caveats.
referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader)
referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader, ts)
if err != nil {
return err
}
Expand All @@ -52,6 +54,17 @@ func ValidateRelationshipUpdates(
}
}

// check if the relation is deprecated for create or touch operations
relsToCheck := make([]tuple.Relationship, 0, len(updates))
for _, update := range updates {
if update.Operation == tuple.UpdateOperationTouch || update.Operation == tuple.UpdateOperationCreate {
relsToCheck = append(relsToCheck, update.Relationship)
}
}
if err := CheckDeprecationsOnRelationships(ctx, relsToCheck, ts); err != nil {
return err
}

return nil
}

Expand All @@ -65,8 +78,10 @@ func ValidateRelationshipsForCreateOrTouch(
caveatTypeSet *caveattypes.TypeSet,
rels ...tuple.Relationship,
) error {
ts := schema.NewTypeSystem(schema.ResolverForDatastoreReader(reader))

// Load namespaces and caveats.
referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader)
referencedNamespaceMap, referencedCaveatMap, err := loadNamespacesAndCaveats(ctx, rels, reader, ts)
if err != nil {
return err
}
Expand All @@ -84,10 +99,15 @@ func ValidateRelationshipsForCreateOrTouch(
}
}

// Validate if the resource, relation or subject is deprecated
if err := CheckDeprecationsOnRelationships(ctx, rels, ts); err != nil {
return err
}

return nil
}

func loadNamespacesAndCaveats(ctx context.Context, rels []tuple.Relationship, reader datastore.Reader) (map[string]*schema.Definition, map[string]*core.CaveatDefinition, error) {
func loadNamespacesAndCaveats(ctx context.Context, rels []tuple.Relationship, reader datastore.Reader, ts *schema.TypeSystem) (map[string]*schema.Definition, map[string]*core.CaveatDefinition, error) {
referencedNamespaceNames := mapz.NewSet[string]()
referencedCaveatNamesWithContext := mapz.NewSet[string]()
for _, rel := range rels {
Expand All @@ -106,7 +126,6 @@ func loadNamespacesAndCaveats(ctx context.Context, rels []tuple.Relationship, re
if err != nil {
return nil, nil, err
}
ts := schema.NewTypeSystem(schema.ResolverForDatastoreReader(reader))

referencedNamespaceMap = make(map[string]*schema.Definition, len(foundNamespaces))
for _, nsDef := range foundNamespaces {
Expand Down Expand Up @@ -277,3 +296,24 @@ func hasNonEmptyCaveatContext(relationship tuple.Relationship) bool {
relationship.OptionalCaveat.Context != nil &&
len(relationship.OptionalCaveat.Context.GetFields()) > 0
}

// CheckDeprecationsOnRelationships checks the provided relationships for any deprecations, returning an error if applicable
func CheckDeprecationsOnRelationships(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doc comment

ctx context.Context,
relationships []tuple.Relationship,
ts *schema.TypeSystem,
) error {
for _, rel := range relationships {
if err := checkForDeprecatedRelationsAndObjects(ctx, rel, ts); err != nil {
return err
}
}
return nil
}

func checkForDeprecatedRelationsAndObjects(ctx context.Context, rel tuple.Relationship, ts *schema.TypeSystem) error {
if err := ts.CheckRelationshipDeprecation(ctx, rel); err != nil {
return shared.NewDeprecationError(err)
}
return nil
}
210 changes: 209 additions & 1 deletion internal/relationships/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,214 @@ func TestValidateRelationshipOperations(t *testing.T) {
core.RelationTupleUpdate_CREATE,
"subjects of type `user with somecaveat and expiration` are not allowed on relation `resource#viewer`",
},
{
"deprecation relation test",
`use deprecation
definition testuser {}
definition user {}

definition document {
@deprecated(error,"deprecated, migrate away")
relation editor: testuser
relation viewer: user
}`,
"document:foo#editor@testuser:tom",
core.RelationTupleUpdate_CREATE,
"relation document#editor is deprecated: deprecated, migrate away",
},
{
"deprecated namespace test",
`use deprecation
@deprecated(error, "deprecated, migrate away")
definition testuser {}
definition user {}

definition document {
relation editor: testuser
}`,
"document:foo#editor@testuser:tom",
core.RelationTupleUpdate_CREATE,
"resource_type testuser is deprecated: deprecated, migrate away",
},
{
"deprecated relation subject type",
`use deprecation
definition user {}
definition testuser {}

definition platform {
relation viewer: user | @deprecated(warn, "comments") testuser
relation auditor: user | @deprecated(error, "test") testuser
}`,
"platform:foo#auditor@testuser:test",
core.RelationTupleUpdate_CREATE,
"resource_type testuser is deprecated: test",
},
{
"deprecated relation same subject type with wildcard",
`use deprecation
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add tests for deprecation of, say, a subject type without expiration but the one with expiration is still valid (and vice versa) and caveats as well

Copy link
Contributor Author

@kartikaysaxena kartikaysaxena Jul 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests of these scenarios including the log and err messages, example "resource_type user with caveat somecaveat and expiration is deprecated:comments here"

definition user {}
definition testuser {}

definition platform {
relation viewer: user | @deprecated(warn, "comments") testuser
relation auditor: testuser | @deprecated(error, "no wildcard please") testuser:*
}`,
"platform:foo#auditor@testuser:*",
core.RelationTupleUpdate_CREATE,
"wildcard allowed type testuser:* is deprecated: no wildcard please",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add the same test with writing the non-wildcard and ensure it does not return an error/warning

},
{
"deprecated relation same subject type with write on non-wildcard relation",
`use deprecation
definition user {}
definition testuser {}

definition platform {
relation viewer: user | @deprecated(warn, "comments") testuser
relation auditor: testuser | @deprecated(error, "no wildcard please") testuser:*
}`,
"platform:foo#auditor@testuser:test1",
core.RelationTupleUpdate_CREATE,
"",
},
{
"deprecated subject without expiration when expiration required",
`use expiration
use deprecation

@deprecated(error, "do not use testuser")
definition testuser {}

definition document {
relation viewer: testuser with expiration
}`,
"document:foo#viewer@testuser:tom",
core.RelationTupleUpdate_CREATE,
"subjects of type `testuser` are not allowed on relation `document#viewer`; did you mean `testuser with expiration`?",
},
{
"deprecated subject with expiration",
`use expiration
use deprecation

@deprecated(error, "do not use testuser")
definition testuser {}

definition document {
relation viewer: testuser with expiration
}`,
"document:foo#viewer@testuser:tom[expiration:2021-01-01T00:00:00Z]",
core.RelationTupleUpdate_CREATE,
"resource_type testuser is deprecated: do not use testuser",
},
{
"deprecated subject with wrong caveat",
`use deprecation

caveat somecaveat(somecondition int) {
somecondition == 42
}

caveat anothercaveat(somecondition int) {
somecondition == 42
}

@deprecated(error, "do not use testuser")
definition testuser {}

definition document {
relation viewer: testuser with somecaveat
}`,
"document:foo#viewer@testuser:tom[anothercaveat]",
core.RelationTupleUpdate_CREATE,
"subjects of type `testuser with anothercaveat` are not allowed on relation `document#viewer`",
},
{
"deprecated subject with correct caveat",
`use deprecation

caveat somecaveat(somecondition int) {
somecondition == 42
}

@deprecated(error, "do not use testuser")
definition testuser {}

definition document {
relation viewer: testuser with somecaveat
}`,
"document:foo#viewer@testuser:tom[somecaveat]",
core.RelationTupleUpdate_CREATE,
"resource_type testuser is deprecated: do not use testuser",
},
{
"deprecated subject without caveat when required",
`use deprecation

caveat somecaveat(somecondition int) {
somecondition == 42
}

@deprecated(error, "do not use testuser")
definition testuser {}

definition document {
relation viewer: testuser with somecaveat
}`,
"document:foo#viewer@testuser:tom",
core.RelationTupleUpdate_CREATE,
"subjects of type `testuser` are not allowed on relation `document#viewer` without one of the following caveats: somecaveat",
},
{
"deprecated user with caveat and expiration",
`use expiration
use deprecation

caveat somecaveat(somecondition int) {
somecondition == 42
}

definition user {}

definition document {
relation viewer: user | @deprecated(error, "don't use this") user with somecaveat and expiration
}`,
"document:foo#viewer@user:tom[somecaveat][expiration:2021-01-01T00:00:00Z]",
core.RelationTupleUpdate_CREATE,
"resource_type user with caveat `somecaveat` and expiration is deprecated: don't use this",
},
{
"deprecated user with caveat only",
`use deprecation

caveat somecaveat(somecondition int) {
somecondition == 42
}

definition user {}

definition document {
relation viewer: user | @deprecated(error, "caveated access deprecated") user with somecaveat
}`,
"document:foo#viewer@user:tom[somecaveat]",
core.RelationTupleUpdate_CREATE,
"resource_type user with caveat `somecaveat` is deprecated: caveated access deprecated",
},
{
"deprecated user with expiration only",
`use expiration
use deprecation

definition user {}

definition document {
relation viewer: user | @deprecated(error, "expiring access deprecated") user with expiration
}`,
"document:foo#viewer@user:tom[expiration:2021-01-01T00:00:00Z]",
core.RelationTupleUpdate_CREATE,
"resource_type user with expiration is deprecated: expiring access deprecated",
},
}

for _, tc := range tcs {
Expand All @@ -325,7 +533,7 @@ func TestValidateRelationshipOperations(t *testing.T) {
op = tuple.Delete
}

// Validate update.
// Validate update for delete.
err = ValidateRelationshipUpdates(t.Context(), reader, caveattypes.Default.TypeSet, []tuple.RelationshipUpdate{
op(tuple.MustParse(tc.relationship)),
})
Expand Down
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,
FeatureDeprecationEnabled: permSysConfig.DeprecatedRelationshipsAndObjectsEnabled,
PerformanceInsightMetricsEnabled: permSysConfig.PerformanceInsightMetricsEnabled,
}
v1.RegisterSchemaServiceServer(srv, v1svc.NewSchemaServer(schemaConfig))
Expand Down
24 changes: 24 additions & 0 deletions internal/services/shared/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,30 @@ func NewSchemaWriteDataValidationError(message string, args []any, metadata map[
}
}

// DeprecationError is an error returned when a schema object or relation is deprecated.
type DeprecationError struct {
error
}

func (err DeprecationError) GRPCStatus() *status.Status {
return spiceerrors.WithCodeAndDetails(
err,
codes.Aborted,
spiceerrors.ForReason(
// TODO: replace with a deprecation type error reason
v1.ErrorReason_ERROR_REASON_SCHEMA_TYPE_ERROR,
map[string]string{},
),
)
}

// NewDeprecationError wraps an error to indicate an attempted write to a deprecated object type, subject or allowed relation type.
func NewDeprecationError(err error) DeprecationError {
return DeprecationError{
error: err,
}
}

// SchemaWriteDataValidationError occurs when a schema cannot be applied due to leaving data unreferenced.
type SchemaWriteDataValidationError struct {
error
Expand Down
Loading
Loading