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 }