diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-24 17:58:01 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-24 17:58:01 -0600 |
| commit | 72296119fc9755774719f8f625ad03e0e0ec457a (patch) | |
| tree | ed236ddee12a20fb55b7cfecf13f62d3a000dcb5 /vendor/github.com/authzed/zed/internal/decode/decoder.go | |
| parent | a920a8cfe415858bb2777371a77018599ffed23f (diff) | |
| parent | eaa1bd3b8e12934aed06413d75e7482ac58d805a (diff) | |
Merge branch 'the-spice-must-flow' into 'main'
Add SpiceDB Authorization
See merge request gitlab-org/software-supply-chain-security/authorization/sparkled!19
Diffstat (limited to 'vendor/github.com/authzed/zed/internal/decode/decoder.go')
| -rw-r--r-- | vendor/github.com/authzed/zed/internal/decode/decoder.go | 215 |
1 files changed, 215 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/zed/internal/decode/decoder.go b/vendor/github.com/authzed/zed/internal/decode/decoder.go new file mode 100644 index 0000000..f8a02ad --- /dev/null +++ b/vendor/github.com/authzed/zed/internal/decode/decoder.go @@ -0,0 +1,215 @@ +package decode + +import ( + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "strings" + + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" + + composable "github.com/authzed/spicedb/pkg/composableschemadsl/compiler" + "github.com/authzed/spicedb/pkg/composableschemadsl/generator" + "github.com/authzed/spicedb/pkg/schemadsl/compiler" + "github.com/authzed/spicedb/pkg/schemadsl/input" + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/validationfile" + "github.com/authzed/spicedb/pkg/validationfile/blocks" +) + +var playgroundPattern = regexp.MustCompile("^.*/s/.*/schema|relationships|assertions|expected.*$") + +// SchemaRelationships holds the schema (as a string) and a list of +// relationships (as a string) in the format from the devtools download API. +type SchemaRelationships struct { + Schema string `yaml:"schema"` + Relationships string `yaml:"relationships"` +} + +// Func will decode into the supplied object. +type Func func(out interface{}) ([]byte, bool, error) + +// DecoderForURL returns the appropriate decoder for a given URL. +// Some URLs have special handling to dereference to the actual file. +func DecoderForURL(u *url.URL) (d Func, err error) { + switch s := u.Scheme; s { + case "", "file": + d = fileDecoder(u) + case "http", "https": + d = httpDecoder(u) + default: + err = fmt.Errorf("%s scheme not supported", s) + } + return +} + +func fileDecoder(u *url.URL) Func { + return func(out interface{}) ([]byte, bool, error) { + file, err := os.Open(u.Path) + if err != nil { + return nil, false, err + } + data, err := io.ReadAll(file) + if err != nil { + return nil, false, err + } + isOnlySchema, err := unmarshalAsYAMLOrSchemaWithFile(data, out, u.Path) + return data, isOnlySchema, err + } +} + +func httpDecoder(u *url.URL) Func { + rewriteURL(u) + return directHTTPDecoder(u) +} + +func rewriteURL(u *url.URL) { + // match playground urls + if playgroundPattern.MatchString(u.Path) { + u.Path = u.Path[:strings.LastIndex(u.Path, "/")] + u.Path += "/download" + return + } + + switch u.Hostname() { + case "gist.github.com": + u.Host = "gist.githubusercontent.com" + u.Path = path.Join(u.Path, "/raw") + case "pastebin.com": + if ok, _ := path.Match("/raw/*", u.Path); ok { + return + } + u.Path = path.Join("/raw/", u.Path) + } +} + +func directHTTPDecoder(u *url.URL) Func { + return func(out interface{}) ([]byte, bool, error) { + log.Debug().Stringer("url", u).Send() + r, err := http.Get(u.String()) + if err != nil { + return nil, false, err + } + defer r.Body.Close() + data, err := io.ReadAll(r.Body) + if err != nil { + return nil, false, err + } + + isOnlySchema, err := unmarshalAsYAMLOrSchema("", data, out) + return data, isOnlySchema, err + } +} + +// Uses the files passed in the args and looks for the specified schemaFile to parse the YAML. +func unmarshalAsYAMLOrSchemaWithFile(data []byte, out interface{}, filename string) (bool, error) { + if strings.Contains(string(data), "schemaFile:") && !strings.Contains(string(data), "schema:") { + if err := yaml.Unmarshal(data, out); err != nil { + return false, err + } + validationFile, ok := out.(*validationfile.ValidationFile) + if !ok { + return false, fmt.Errorf("could not cast unmarshalled file to validationfile") + } + + // Need to join the original filepath with the requested filepath + // to construct the path to the referenced schema file. + // NOTE: This does not allow for yaml files to transitively reference + // each other's schemaFile fields. + // TODO: enable this behavior + schemaPath := filepath.Join(path.Dir(filename), validationFile.SchemaFile) + + if !filepath.IsLocal(schemaPath) { + // We want to prevent access of files that are outside of the folder + // where the command was originally invoked. This should do that. + return false, fmt.Errorf("schema filepath %s must be local to where the command was invoked", schemaPath) + } + + file, err := os.Open(schemaPath) + if err != nil { + return false, err + } + data, err = io.ReadAll(file) + if err != nil { + return false, err + } + } + return unmarshalAsYAMLOrSchema(filename, data, out) +} + +func unmarshalAsYAMLOrSchema(filename string, data []byte, out interface{}) (bool, error) { + inputString := string(data) + + // Check for indications of a schema-only file. + if !strings.Contains(inputString, "schema:") && !strings.Contains(inputString, "relationships:") { + if err := compileSchemaFromData(filename, inputString, out); err != nil { + return false, err + } + return true, nil + } + + if !strings.Contains(inputString, "schema:") && !strings.Contains(inputString, "schemaFile:") { + // If there is no schema and no schemaFile and it doesn't compile then it must be yaml with missing fields + if err := compileSchemaFromData(filename, inputString, out); err != nil { + return false, errors.New("either schema or schemaFile must be present") + } + return true, nil + } + // Try to unparse as YAML for the validation file format. + if err := yaml.Unmarshal(data, out); err != nil { + return false, err + } + + return false, nil +} + +// compileSchemaFromData attempts to compile using the old DSL and the new composable DSL, +// but prefers the new DSL. +// It returns the errors returned by both compilations. +func compileSchemaFromData(filename, schemaString string, out interface{}) error { + var ( + standardCompileErr error + composableCompiled *composable.CompiledSchema + composableCompileErr error + vfile validationfile.ValidationFile + ) + + vfile = *out.(*validationfile.ValidationFile) + vfile.Schema = blocks.ParsedSchema{ + SourcePosition: spiceerrors.SourcePosition{LineNumber: 1, ColumnPosition: 1}, + } + + _, standardCompileErr = compiler.Compile(compiler.InputSchema{ + Source: input.Source("schema"), + SchemaString: schemaString, + }, compiler.AllowUnprefixedObjectType()) + + if standardCompileErr == nil { + vfile.Schema.Schema = schemaString + } + + inputSourceFolder := filepath.Dir(filename) + composableCompiled, composableCompileErr = composable.Compile(composable.InputSchema{ + SchemaString: schemaString, + }, composable.AllowUnprefixedObjectType(), composable.SourceFolder(inputSourceFolder)) + + if composableCompileErr == nil { + compiledSchemaString, _, err := generator.GenerateSchema(composableCompiled.OrderedDefinitions) + if err != nil { + return fmt.Errorf("could not generate string schema: %w", err) + } + vfile.Schema.Schema = compiledSchemaString + } + + err := errors.Join(standardCompileErr, composableCompileErr) + + *out.(*validationfile.ValidationFile) = vfile + return err +} |
