github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/diff/namespace/diff.go (about)

     1  package namespace
     2  
     3  import (
     4  	"github.com/google/go-cmp/cmp"
     5  	"golang.org/x/exp/slices"
     6  	"google.golang.org/protobuf/testing/protocmp"
     7  
     8  	nsinternal "github.com/authzed/spicedb/internal/namespace"
     9  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    10  	nspkg "github.com/authzed/spicedb/pkg/namespace"
    11  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    12  	iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1"
    13  	"github.com/authzed/spicedb/pkg/typesystem"
    14  )
    15  
    16  // DeltaType defines the type of namespace deltas.
    17  type DeltaType string
    18  
    19  const (
    20  	// NamespaceAdded indicates that the namespace was newly added/created.
    21  	NamespaceAdded DeltaType = "namespace-added"
    22  
    23  	// NamespaceRemoved indicates that the namespace was removed.
    24  	NamespaceRemoved DeltaType = "namespace-removed"
    25  
    26  	// NamespaceCommentsChanged indicates that the comment(s) on the namespace were changed.
    27  	NamespaceCommentsChanged DeltaType = "namespace-comments-changed"
    28  
    29  	// AddedRelation indicates that the relation was added to the namespace.
    30  	AddedRelation DeltaType = "added-relation"
    31  
    32  	// RemovedRelation indicates that the relation was removed from the namespace.
    33  	RemovedRelation DeltaType = "removed-relation"
    34  
    35  	// AddedPermission indicates that the permission was added to the namespace.
    36  	AddedPermission DeltaType = "added-permission"
    37  
    38  	// RemovedPermission indicates that the permission was removed from the namespace.
    39  	RemovedPermission DeltaType = "removed-permission"
    40  
    41  	// ChangedPermissionImpl indicates that the implementation of the permission has changed in some
    42  	// way.
    43  	ChangedPermissionImpl DeltaType = "changed-permission-implementation"
    44  
    45  	// ChangedPermissionComment indicates that the comment of the permission has changed in some way.
    46  	ChangedPermissionComment DeltaType = "changed-permission-comment"
    47  
    48  	// LegacyChangedRelationImpl indicates that the implementation of the relation has changed in some
    49  	// way. This is for legacy checks and should not apply to any modern namespaces created
    50  	// via schema.
    51  	LegacyChangedRelationImpl DeltaType = "legacy-changed-relation-implementation"
    52  
    53  	// RelationAllowedTypeAdded indicates that an allowed relation type has been added to
    54  	// the relation.
    55  	RelationAllowedTypeAdded DeltaType = "relation-allowed-type-added"
    56  
    57  	// RelationAllowedTypeRemoved indicates that an allowed relation type has been removed from
    58  	// the relation.
    59  	RelationAllowedTypeRemoved DeltaType = "relation-allowed-type-removed"
    60  
    61  	// ChangedRelationComment indicates that the comment of the relation has changed in some way.
    62  	ChangedRelationComment DeltaType = "changed-relation-comment"
    63  )
    64  
    65  // Diff holds the diff between two namespaces.
    66  type Diff struct {
    67  	existing *core.NamespaceDefinition
    68  	updated  *core.NamespaceDefinition
    69  	deltas   []Delta
    70  }
    71  
    72  // Deltas returns the deltas between the two namespaces.
    73  func (nd Diff) Deltas() []Delta {
    74  	return nd.deltas
    75  }
    76  
    77  // Delta holds a single change of a namespace.
    78  type Delta struct {
    79  	// Type is the type of this delta.
    80  	Type DeltaType
    81  
    82  	// RelationName is the name of the relation to which this delta applies, if any.
    83  	RelationName string
    84  
    85  	// AllowedType is the allowed relation type added or removed, if any.
    86  	AllowedType *core.AllowedRelation
    87  }
    88  
    89  // DiffNamespaces performs a diff between two namespace definitions. One or both of the definitions
    90  // can be `nil`, which will be treated as an add/remove as applicable.
    91  func DiffNamespaces(existing *core.NamespaceDefinition, updated *core.NamespaceDefinition) (*Diff, error) {
    92  	// Check for the namespaces themselves.
    93  	if existing == nil && updated == nil {
    94  		return &Diff{existing, updated, []Delta{}}, nil
    95  	}
    96  
    97  	if existing != nil && updated == nil {
    98  		return &Diff{
    99  			existing: existing,
   100  			updated:  updated,
   101  			deltas: []Delta{
   102  				{
   103  					Type: NamespaceRemoved,
   104  				},
   105  			},
   106  		}, nil
   107  	}
   108  
   109  	if existing == nil && updated != nil {
   110  		return &Diff{
   111  			existing: existing,
   112  			updated:  updated,
   113  			deltas: []Delta{
   114  				{
   115  					Type: NamespaceAdded,
   116  				},
   117  			},
   118  		}, nil
   119  	}
   120  
   121  	deltas := []Delta{}
   122  
   123  	// Check the namespace's comments.
   124  	existingComments := nspkg.GetComments(existing.Metadata)
   125  	updatedComments := nspkg.GetComments(updated.Metadata)
   126  	if !slices.Equal(existingComments, updatedComments) {
   127  		deltas = append(deltas, Delta{
   128  			Type: NamespaceCommentsChanged,
   129  		})
   130  	}
   131  
   132  	// Collect up relations and check.
   133  	existingRels := map[string]*core.Relation{}
   134  	existingRelNames := mapz.NewSet[string]()
   135  
   136  	existingPerms := map[string]*core.Relation{}
   137  	existingPermNames := mapz.NewSet[string]()
   138  
   139  	updatedRels := map[string]*core.Relation{}
   140  	updatedRelNames := mapz.NewSet[string]()
   141  
   142  	updatedPerms := map[string]*core.Relation{}
   143  	updatedPermNames := mapz.NewSet[string]()
   144  
   145  	for _, relation := range existing.Relation {
   146  		_, ok := existingRels[relation.Name]
   147  		if ok {
   148  			return nil, nsinternal.NewDuplicateRelationError(existing.Name, relation.Name)
   149  		}
   150  
   151  		if isPermission(relation) {
   152  			existingPerms[relation.Name] = relation
   153  			existingPermNames.Add(relation.Name)
   154  		} else {
   155  			existingRels[relation.Name] = relation
   156  			existingRelNames.Add(relation.Name)
   157  		}
   158  	}
   159  
   160  	for _, relation := range updated.Relation {
   161  		_, ok := updatedRels[relation.Name]
   162  		if ok {
   163  			return nil, nsinternal.NewDuplicateRelationError(updated.Name, relation.Name)
   164  		}
   165  
   166  		if isPermission(relation) {
   167  			updatedPerms[relation.Name] = relation
   168  			updatedPermNames.Add(relation.Name)
   169  		} else {
   170  			updatedRels[relation.Name] = relation
   171  			updatedRelNames.Add(relation.Name)
   172  		}
   173  	}
   174  
   175  	_ = existingRelNames.Subtract(updatedRelNames).ForEach(func(removed string) error {
   176  		deltas = append(deltas, Delta{
   177  			Type:         RemovedRelation,
   178  			RelationName: removed,
   179  		})
   180  		return nil
   181  	})
   182  
   183  	_ = updatedRelNames.Subtract(existingRelNames).ForEach(func(added string) error {
   184  		deltas = append(deltas, Delta{
   185  			Type:         AddedRelation,
   186  			RelationName: added,
   187  		})
   188  		return nil
   189  	})
   190  
   191  	_ = existingPermNames.Subtract(updatedPermNames).ForEach(func(removed string) error {
   192  		deltas = append(deltas, Delta{
   193  			Type:         RemovedPermission,
   194  			RelationName: removed,
   195  		})
   196  		return nil
   197  	})
   198  
   199  	_ = updatedPermNames.Subtract(existingPermNames).ForEach(func(added string) error {
   200  		deltas = append(deltas, Delta{
   201  			Type:         AddedPermission,
   202  			RelationName: added,
   203  		})
   204  		return nil
   205  	})
   206  
   207  	_ = existingPermNames.Intersect(updatedPermNames).ForEach(func(shared string) error {
   208  		existingPerm := existingPerms[shared]
   209  		updatedPerm := updatedPerms[shared]
   210  
   211  		// Compare implementations.
   212  		if areDifferentExpressions(existingPerm.UsersetRewrite, updatedPerm.UsersetRewrite) {
   213  			deltas = append(deltas, Delta{
   214  				Type:         ChangedPermissionImpl,
   215  				RelationName: shared,
   216  			})
   217  		}
   218  
   219  		// Compare comments.
   220  		existingComments := nspkg.GetComments(existingPerm.Metadata)
   221  		updatedComments := nspkg.GetComments(updatedPerm.Metadata)
   222  		if !slices.Equal(existingComments, updatedComments) {
   223  			deltas = append(deltas, Delta{
   224  				Type:         ChangedPermissionComment,
   225  				RelationName: shared,
   226  			})
   227  		}
   228  		return nil
   229  	})
   230  
   231  	_ = existingRelNames.Intersect(updatedRelNames).ForEach(func(shared string) error {
   232  		existingRel := existingRels[shared]
   233  		updatedRel := updatedRels[shared]
   234  
   235  		// Compare implementations (legacy).
   236  		if areDifferentExpressions(existingRel.UsersetRewrite, updatedRel.UsersetRewrite) {
   237  			deltas = append(deltas, Delta{
   238  				Type:         LegacyChangedRelationImpl,
   239  				RelationName: shared,
   240  			})
   241  		}
   242  
   243  		// Compare comments.
   244  		existingComments := nspkg.GetComments(existingRel.Metadata)
   245  		updatedComments := nspkg.GetComments(updatedRel.Metadata)
   246  		if !slices.Equal(existingComments, updatedComments) {
   247  			deltas = append(deltas, Delta{
   248  				Type:         ChangedRelationComment,
   249  				RelationName: shared,
   250  			})
   251  		}
   252  
   253  		// Compare type information.
   254  		existingTypeInfo := existingRel.TypeInformation
   255  		if existingTypeInfo == nil {
   256  			existingTypeInfo = &core.TypeInformation{}
   257  		}
   258  
   259  		updatedTypeInfo := updatedRel.TypeInformation
   260  		if updatedTypeInfo == nil {
   261  			updatedTypeInfo = &core.TypeInformation{}
   262  		}
   263  
   264  		existingAllowedRels := mapz.NewSet[string]()
   265  		updatedAllowedRels := mapz.NewSet[string]()
   266  		allowedRelsBySource := map[string]*core.AllowedRelation{}
   267  
   268  		for _, existingAllowed := range existingTypeInfo.AllowedDirectRelations {
   269  			source := typesystem.SourceForAllowedRelation(existingAllowed)
   270  			allowedRelsBySource[source] = existingAllowed
   271  			existingAllowedRels.Add(source)
   272  		}
   273  
   274  		for _, updatedAllowed := range updatedTypeInfo.AllowedDirectRelations {
   275  			source := typesystem.SourceForAllowedRelation(updatedAllowed)
   276  			allowedRelsBySource[source] = updatedAllowed
   277  			updatedAllowedRels.Add(source)
   278  		}
   279  
   280  		_ = existingAllowedRels.Subtract(updatedAllowedRels).ForEach(func(removed string) error {
   281  			deltas = append(deltas, Delta{
   282  				Type:         RelationAllowedTypeRemoved,
   283  				RelationName: shared,
   284  				AllowedType:  allowedRelsBySource[removed],
   285  			})
   286  			return nil
   287  		})
   288  
   289  		_ = updatedAllowedRels.Subtract(existingAllowedRels).ForEach(func(added string) error {
   290  			deltas = append(deltas, Delta{
   291  				Type:         RelationAllowedTypeAdded,
   292  				RelationName: shared,
   293  				AllowedType:  allowedRelsBySource[added],
   294  			})
   295  			return nil
   296  		})
   297  
   298  		return nil
   299  	})
   300  
   301  	return &Diff{
   302  		existing: existing,
   303  		updated:  updated,
   304  		deltas:   deltas,
   305  	}, nil
   306  }
   307  
   308  func isPermission(relation *core.Relation) bool {
   309  	return nspkg.GetRelationKind(relation) == iv1.RelationMetadata_PERMISSION
   310  }
   311  
   312  func areDifferentExpressions(existing *core.UsersetRewrite, updated *core.UsersetRewrite) bool {
   313  	// Return whether the rewrites are different, ignoring the SourcePosition message type.
   314  	delta := cmp.Diff(
   315  		existing,
   316  		updated,
   317  		protocmp.Transform(),
   318  		protocmp.IgnoreMessages(&core.SourcePosition{}),
   319  	)
   320  	return delta != ""
   321  }