github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/validationfile/loader.go (about) 1 package validationfile 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 8 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 9 10 log "github.com/authzed/spicedb/internal/logging" 11 dsctx "github.com/authzed/spicedb/internal/middleware/datastore" 12 "github.com/authzed/spicedb/internal/namespace" 13 "github.com/authzed/spicedb/internal/relationships" 14 "github.com/authzed/spicedb/pkg/datastore" 15 "github.com/authzed/spicedb/pkg/genutil/slicez" 16 core "github.com/authzed/spicedb/pkg/proto/core/v1" 17 "github.com/authzed/spicedb/pkg/tuple" 18 "github.com/authzed/spicedb/pkg/typesystem" 19 ) 20 21 // PopulatedValidationFile contains the fully parsed information from a validation file. 22 type PopulatedValidationFile struct { 23 // Schema is the entered schema text, if any. 24 Schema string 25 26 // NamespaceDefinitions are the namespaces defined in the validation file, in either 27 // direct or compiled from schema form. 28 NamespaceDefinitions []*core.NamespaceDefinition 29 30 // CaveatDefinitions are the caveats defined in the validation file, in either 31 // direct or compiled from schema form. 32 CaveatDefinitions []*core.CaveatDefinition 33 34 // Tuples are the relation tuples defined in the validation file, either directly 35 // or in the relationships block. 36 Tuples []*core.RelationTuple 37 38 // ParsedFiles are the underlying parsed validation files. 39 ParsedFiles []ValidationFile 40 } 41 42 // PopulateFromFiles populates the given datastore with the namespaces and tuples found in 43 // the validation file(s) specified. 44 func PopulateFromFiles(ctx context.Context, ds datastore.Datastore, filePaths []string) (*PopulatedValidationFile, datastore.Revision, error) { 45 contents := map[string][]byte{} 46 47 for _, filePath := range filePaths { 48 fileContents, err := os.ReadFile(filePath) 49 if err != nil { 50 return nil, datastore.NoRevision, err 51 } 52 53 contents[filePath] = fileContents 54 } 55 56 return PopulateFromFilesContents(ctx, ds, contents) 57 } 58 59 // PopulateFromFilesContents populates the given datastore with the namespaces and tuples found in 60 // the validation file(s) contents specified. 61 func PopulateFromFilesContents(ctx context.Context, ds datastore.Datastore, filesContents map[string][]byte) (*PopulatedValidationFile, datastore.Revision, error) { 62 var schema string 63 var objectDefs []*core.NamespaceDefinition 64 var caveatDefs []*core.CaveatDefinition 65 var tuples []*core.RelationTuple 66 var updates []*core.RelationTupleUpdate 67 68 var revision datastore.Revision 69 70 files := make([]ValidationFile, 0, len(filesContents)) 71 72 // Parse each file into definitions and relationship updates. 73 for filePath, fileContents := range filesContents { 74 // Decode the validation file. 75 parsed, err := DecodeValidationFile(fileContents) 76 if err != nil { 77 return nil, datastore.NoRevision, fmt.Errorf("error when parsing config file %s: %w", filePath, err) 78 } 79 80 files = append(files, *parsed) 81 82 // Disallow legacy sections. 83 if len(parsed.NamespaceConfigs) > 0 { 84 return nil, revision, fmt.Errorf("definitions must be specified in `schema`") 85 } 86 87 if len(parsed.ValidationTuples) > 0 { 88 return nil, revision, fmt.Errorf("relationships must be specified in `relationships`") 89 } 90 91 // Add schema definitions. 92 if parsed.Schema.CompiledSchema != nil { 93 defs := parsed.Schema.CompiledSchema.ObjectDefinitions 94 if len(defs) > 0 { 95 schema += parsed.Schema.Schema + "\n\n" 96 } 97 98 log.Ctx(ctx).Info().Str("filePath", filePath).Int("schemaDefinitionCount", len(parsed.Schema.CompiledSchema.OrderedDefinitions)).Msg("adding schema definitions") 99 objectDefs = append(objectDefs, defs...) 100 caveatDefs = append(caveatDefs, parsed.Schema.CompiledSchema.CaveatDefinitions...) 101 } 102 103 // Parse relationships for updates. 104 for _, rel := range parsed.Relationships.Relationships { 105 tpl := tuple.MustFromRelationship[*v1.ObjectReference, *v1.SubjectReference, *v1.ContextualizedCaveat](rel) 106 updates = append(updates, tuple.Touch(tpl)) 107 tuples = append(tuples, tpl) 108 } 109 } 110 111 // Load the definitions and relationships into the datastore. 112 revision, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { 113 // Write the caveat definitions. 114 err := rwt.WriteCaveats(ctx, caveatDefs) 115 if err != nil { 116 return err 117 } 118 119 // Validate and write the object definitions. 120 for _, objectDef := range objectDefs { 121 ts, err := typesystem.NewNamespaceTypeSystem(objectDef, 122 typesystem.ResolverForDatastoreReader(rwt).WithPredefinedElements(typesystem.PredefinedElements{ 123 Namespaces: objectDefs, 124 })) 125 if err != nil { 126 return err 127 } 128 129 ctx := dsctx.ContextWithDatastore(ctx, ds) 130 vts, terr := ts.Validate(ctx) 131 if terr != nil { 132 return terr 133 } 134 135 aerr := namespace.AnnotateNamespace(vts) 136 if aerr != nil { 137 return aerr 138 } 139 140 if err := rwt.WriteNamespaces(ctx, objectDef); err != nil { 141 return fmt.Errorf("error when loading object definition %s: %w", objectDef.Name, err) 142 } 143 } 144 145 return err 146 }) 147 148 slicez.ForEachChunk(updates, 500, func(chunked []*core.RelationTupleUpdate) { 149 if err != nil { 150 return 151 } 152 153 chunkedTuples := make([]*core.RelationTuple, 0, len(chunked)) 154 for _, update := range chunked { 155 chunkedTuples = append(chunkedTuples, update.Tuple) 156 } 157 revision, err = ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error { 158 err = relationships.ValidateRelationshipsForCreateOrTouch(ctx, rwt, chunkedTuples) 159 if err != nil { 160 return err 161 } 162 163 return rwt.WriteRelationships(ctx, chunked) 164 }) 165 }) 166 167 if err != nil { 168 return nil, nil, err 169 } 170 171 return &PopulatedValidationFile{schema, objectDefs, caveatDefs, tuples, files}, revision, err 172 }