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  }