-
Notifications
You must be signed in to change notification settings - Fork 245
/
schema.go
404 lines (352 loc) · 13.6 KB
/
schema.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
package shared
import (
"context"
"github.com/authzed/spicedb/internal/caveats"
"github.com/authzed/spicedb/internal/datastore/options"
log "github.com/authzed/spicedb/internal/logging"
"github.com/authzed/spicedb/internal/namespace"
"github.com/authzed/spicedb/pkg/datastore"
core "github.com/authzed/spicedb/pkg/proto/core/v1"
"github.com/authzed/spicedb/pkg/schemadsl/compiler"
"github.com/authzed/spicedb/pkg/tuple"
"github.com/authzed/spicedb/pkg/util"
)
// ValidatedSchemaChanges is a set of validated schema changes that can be applied to the datastore.
type ValidatedSchemaChanges struct {
compiled *compiler.CompiledSchema
newCaveatDefNames *util.Set[string]
newObjectDefNames *util.Set[string]
additiveOnly bool
}
// ValidateSchemaChanges validates the schema found in the compiled schema and returns a
// ValidatedSchemaChanges, if fully validated.
func ValidateSchemaChanges(ctx context.Context, compiled *compiler.CompiledSchema, additiveOnly bool) (*ValidatedSchemaChanges, error) {
// 1) Validate the caveats defined.
newCaveatDefNames := util.NewSet[string]()
for _, caveatDef := range compiled.CaveatDefinitions {
if err := namespace.ValidateCaveatDefinition(caveatDef); err != nil {
return nil, err
}
newCaveatDefNames.Add(caveatDef.Name)
}
// 2) Validate the namespaces defined.
newObjectDefNames := util.NewSet[string]()
for _, nsdef := range compiled.ObjectDefinitions {
ts, err := namespace.NewNamespaceTypeSystem(nsdef,
namespace.ResolverForPredefinedDefinitions(namespace.PredefinedElements{
Namespaces: compiled.ObjectDefinitions,
Caveats: compiled.CaveatDefinitions,
}))
if err != nil {
return nil, err
}
vts, err := ts.Validate(ctx)
if err != nil {
return nil, err
}
if err := namespace.AnnotateNamespace(vts); err != nil {
return nil, err
}
newObjectDefNames.Add(nsdef.Name)
}
return &ValidatedSchemaChanges{
compiled: compiled,
newCaveatDefNames: newCaveatDefNames,
newObjectDefNames: newObjectDefNames,
additiveOnly: additiveOnly,
}, nil
}
// AppliedSchemaChanges holds information about the applied schema changes.
type AppliedSchemaChanges struct {
// TotalOperationCount holds the total number of "dispatch" operations performed by the schema
// being applied.
TotalOperationCount uint32
// NewObjectDefNames contains the names of the newly added object definitions.
NewObjectDefNames []string
// RemovedObjectDefNames contains the names of the removed object definitions.
RemovedObjectDefNames []string
// NewCaveatDefNames contains the names of the newly added caveat definitions.
NewCaveatDefNames []string
// RemovedCaveatDefNames contains the names of the removed caveat definitions.
RemovedCaveatDefNames []string
}
// ApplySchemaChanges applies schema changes found in the validated changes struct, via the specified
// ReadWriteTransaction.
func ApplySchemaChanges(ctx context.Context, rwt datastore.ReadWriteTransaction, validated *ValidatedSchemaChanges) (*AppliedSchemaChanges, error) {
existingCaveats, err := rwt.ListAllCaveats(ctx)
if err != nil {
return nil, err
}
existingObjectDefs, err := rwt.ListAllNamespaces(ctx)
if err != nil {
return nil, err
}
return ApplySchemaChangesOverExisting(ctx, rwt, validated, datastore.DefinitionsOf(existingCaveats), datastore.DefinitionsOf(existingObjectDefs))
}
// ApplySchemaChangesOverExisting applies schema changes found in the validated changes struct, against
// existing caveat and object definitions given.
func ApplySchemaChangesOverExisting(
ctx context.Context,
rwt datastore.ReadWriteTransaction,
validated *ValidatedSchemaChanges,
existingCaveats []*core.CaveatDefinition,
existingObjectDefs []*core.NamespaceDefinition,
) (*AppliedSchemaChanges, error) {
// Build a map of existing caveats to determine those being removed, if any.
existingCaveatDefMap := make(map[string]*core.CaveatDefinition, len(existingCaveats))
existingCaveatDefNames := util.NewSet[string]()
for _, existingCaveat := range existingCaveats {
existingCaveatDefMap[existingCaveat.Name] = existingCaveat
existingCaveatDefNames.Add(existingCaveat.Name)
}
// For each caveat definition, perform a diff and ensure the changes will not result in type errors.
caveatDefsWithChanges := make([]*core.CaveatDefinition, 0, len(validated.compiled.CaveatDefinitions))
for _, caveatDef := range validated.compiled.CaveatDefinitions {
diff, err := sanityCheckCaveatChanges(ctx, rwt, caveatDef, existingCaveatDefMap)
if err != nil {
return nil, err
}
if len(diff.Deltas()) > 0 {
caveatDefsWithChanges = append(caveatDefsWithChanges, caveatDef)
}
}
removedCaveatDefNames := existingCaveatDefNames.Subtract(validated.newCaveatDefNames)
// Build a map of existing definitions to determine those being removed, if any.
existingObjectDefMap := make(map[string]*core.NamespaceDefinition, len(existingObjectDefs))
existingObjectDefNames := util.NewSet[string]()
for _, existingDef := range existingObjectDefs {
existingObjectDefMap[existingDef.Name] = existingDef
existingObjectDefNames.Add(existingDef.Name)
}
// For each definition, perform a diff and ensure the changes will not result in any
// breaking changes.
objectDefsWithChanges := make([]*core.NamespaceDefinition, 0, len(validated.compiled.ObjectDefinitions))
for _, nsdef := range validated.compiled.ObjectDefinitions {
diff, err := sanityCheckNamespaceChanges(ctx, rwt, nsdef, existingObjectDefMap)
if err != nil {
return nil, err
}
if len(diff.Deltas()) > 0 {
objectDefsWithChanges = append(objectDefsWithChanges, nsdef)
}
}
log.Ctx(ctx).
Trace().
Int("objectDefinitions", len(validated.compiled.ObjectDefinitions)).
Int("caveatDefinitions", len(validated.compiled.CaveatDefinitions)).
Int("objectDefsWithChanges", len(objectDefsWithChanges)).
Int("caveatDefsWithChanges", len(caveatDefsWithChanges)).
Msg("validated namespace definitions")
// Ensure that deleting namespaces will not result in any relationships left without associated
// schema.
removedObjectDefNames := existingObjectDefNames.Subtract(validated.newObjectDefNames)
if !validated.additiveOnly {
if err := removedObjectDefNames.ForEach(func(nsdefName string) error {
return ensureNoRelationshipsExist(ctx, rwt, nsdefName)
}); err != nil {
return nil, err
}
}
// Write the new/changes caveats.
if len(caveatDefsWithChanges) > 0 {
if err := rwt.WriteCaveats(ctx, caveatDefsWithChanges); err != nil {
return nil, err
}
}
// Write the new/changed namespaces.
if len(objectDefsWithChanges) > 0 {
if err := rwt.WriteNamespaces(ctx, objectDefsWithChanges...); err != nil {
return nil, err
}
}
if !validated.additiveOnly {
// Delete the removed namespaces.
if removedObjectDefNames.Len() > 0 {
if err := rwt.DeleteNamespaces(ctx, removedObjectDefNames.AsSlice()...); err != nil {
return nil, err
}
}
// Delete the removed caveats.
if !removedCaveatDefNames.IsEmpty() {
if err := rwt.DeleteCaveats(ctx, removedCaveatDefNames.AsSlice()); err != nil {
return nil, err
}
}
}
log.Ctx(ctx).Trace().
Interface("objectDefinitions", validated.compiled.ObjectDefinitions).
Interface("caveatDefinitions", validated.compiled.CaveatDefinitions).
Object("addedOrChangedObjectDefinitions", util.StringSet(validated.newObjectDefNames)).
Object("removedObjectDefinitions", util.StringSet(removedObjectDefNames)).
Object("addedOrChangedCaveatDefinitions", util.StringSet(validated.newCaveatDefNames)).
Object("removedCaveatDefinitions", util.StringSet(removedCaveatDefNames)).
Msg("completed schema update")
return &AppliedSchemaChanges{
TotalOperationCount: uint32(len(validated.compiled.ObjectDefinitions) + len(validated.compiled.CaveatDefinitions) + removedObjectDefNames.Len() + removedCaveatDefNames.Len()),
NewObjectDefNames: validated.newObjectDefNames.Subtract(existingObjectDefNames).AsSlice(),
RemovedObjectDefNames: removedObjectDefNames.AsSlice(),
NewCaveatDefNames: validated.newCaveatDefNames.Subtract(existingCaveatDefNames).AsSlice(),
RemovedCaveatDefNames: removedCaveatDefNames.AsSlice(),
}, nil
}
// sanityCheckCaveatChanges ensures that a caveat definition being written does not break
// the types of the parameters that may already exist on relationships.
func sanityCheckCaveatChanges(
ctx context.Context,
rwt datastore.ReadWriteTransaction,
caveatDef *core.CaveatDefinition,
existingDefs map[string]*core.CaveatDefinition,
) (*caveats.Diff, error) {
// Ensure that the updated namespace does not break the existing tuple data.
existing := existingDefs[caveatDef.Name]
diff, err := caveats.DiffCaveats(existing, caveatDef)
if err != nil {
return nil, err
}
for _, delta := range diff.Deltas() {
switch delta.Type {
case caveats.RemovedParameter:
return diff, NewSchemaWriteDataValidationError("cannot remove parameter `%s` on caveat `%s`", delta.ParameterName, caveatDef.Name)
case caveats.ParameterTypeChanged:
return diff, NewSchemaWriteDataValidationError("cannot change the type of parameter `%s` on caveat `%s`", delta.ParameterName, caveatDef.Name)
}
}
return diff, nil
}
// ensureNoRelationshipsExist ensures that no relationships exist within the namespace with the given name.
func ensureNoRelationshipsExist(ctx context.Context, rwt datastore.ReadWriteTransaction, namespaceName string) error {
qy, qyErr := rwt.QueryRelationships(
ctx,
datastore.RelationshipsFilter{ResourceType: namespaceName},
options.WithLimit(options.LimitOne),
)
if err := errorIfTupleIteratorReturnsTuples(
ctx,
qy,
qyErr,
"cannot delete object definition `%s`, as a relationship exists under it",
namespaceName,
); err != nil {
return err
}
qy, qyErr = rwt.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{
SubjectType: namespaceName,
}, options.WithReverseLimit(options.LimitOne))
err := errorIfTupleIteratorReturnsTuples(
ctx,
qy,
qyErr,
"cannot delete object definition `%s`, as a relationship references it",
namespaceName,
)
qy.Close()
if err != nil {
return err
}
return nil
}
// sanityCheckNamespaceChanges ensures that a namespace definition being written does not result
// in breaking changes, such as relationships without associated defined schema object definitions
// and relations.
func sanityCheckNamespaceChanges(
ctx context.Context,
rwt datastore.ReadWriteTransaction,
nsdef *core.NamespaceDefinition,
existingDefs map[string]*core.NamespaceDefinition,
) (*namespace.Diff, error) {
// Ensure that the updated namespace does not break the existing tuple data.
existing := existingDefs[nsdef.Name]
diff, err := namespace.DiffNamespaces(existing, nsdef)
if err != nil {
return nil, err
}
for _, delta := range diff.Deltas() {
switch delta.Type {
case namespace.RemovedRelation:
qy, qyErr := rwt.QueryRelationships(ctx, datastore.RelationshipsFilter{
ResourceType: nsdef.Name,
OptionalResourceRelation: delta.RelationName,
})
err = errorIfTupleIteratorReturnsTuples(
ctx,
qy,
qyErr,
"cannot delete relation `%s` in object definition `%s`, as a relationship exists under it", delta.RelationName, nsdef.Name)
if err != nil {
return diff, err
}
// Also check for right sides of tuples.
qy, qyErr = rwt.ReverseQueryRelationships(ctx, datastore.SubjectsFilter{
SubjectType: nsdef.Name,
RelationFilter: datastore.SubjectRelationFilter{
NonEllipsisRelation: delta.RelationName,
},
}, options.WithReverseLimit(options.LimitOne))
err = errorIfTupleIteratorReturnsTuples(
ctx,
qy,
qyErr,
"cannot delete relation `%s` in object definition `%s`, as a relationship references it", delta.RelationName, nsdef.Name)
qy.Close()
if err != nil {
return diff, err
}
case namespace.RelationAllowedTypeRemoved:
var optionalSubjectIds []string
var relationFilter datastore.SubjectRelationFilter
optionalCaveatName := ""
if delta.AllowedType.GetPublicWildcard() != nil {
optionalSubjectIds = []string{tuple.PublicWildcard}
} else {
relationFilter = datastore.SubjectRelationFilter{
NonEllipsisRelation: delta.AllowedType.GetRelation(),
}
}
if delta.AllowedType.GetRequiredCaveat() != nil {
optionalCaveatName = delta.AllowedType.GetRequiredCaveat().CaveatName
}
qyr, qyrErr := rwt.QueryRelationships(
ctx,
datastore.RelationshipsFilter{
ResourceType: nsdef.Name,
OptionalResourceRelation: delta.RelationName,
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
{
OptionalSubjectType: delta.AllowedType.Namespace,
OptionalSubjectIds: optionalSubjectIds,
RelationFilter: relationFilter,
},
},
OptionalCaveatName: optionalCaveatName,
},
options.WithLimit(options.LimitOne),
)
err = errorIfTupleIteratorReturnsTuples(
ctx,
qyr,
qyrErr,
"cannot remove allowed type `%s` from relation `%s` in object definition `%s`, as a relationship exists with it",
namespace.SourceForAllowedRelation(delta.AllowedType), delta.RelationName, nsdef.Name)
qyr.Close()
if err != nil {
return diff, err
}
}
}
return diff, nil
}
// errorIfTupleIteratorReturnsTuples takes a tuple iterator and any error that was generated
// when the original iterator was created, and returns an error if iterator contains any tuples.
func errorIfTupleIteratorReturnsTuples(ctx context.Context, qy datastore.RelationshipIterator, qyErr error, message string, args ...interface{}) error {
if qyErr != nil {
return qyErr
}
defer qy.Close()
if rt := qy.Next(); rt != nil {
if qy.Err() != nil {
return qy.Err()
}
return NewSchemaWriteDataValidationError(message, args...)
}
return nil
}