// Copyright 2020 The OPA Authors. All rights reserved. // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. package ast import ( "bytes" _ "embed" "encoding/json" "fmt" "io" "os" "slices" "sort" "strings" "sync" "github.com/open-policy-agent/opa/internal/semver" "github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities" caps "github.com/open-policy-agent/opa/v1/capabilities" "github.com/open-policy-agent/opa/v1/util" ) // VersonIndex contains an index from built-in function name, language feature, // and future rego keyword to version number. During the build, this is used to // create an index of the minimum version required for the built-in/feature/kw. type VersionIndex struct { Builtins map[string]semver.Version `json:"builtins"` Features map[string]semver.Version `json:"features"` Keywords map[string]semver.Version `json:"keywords"` } // NOTE(tsandall): this file is generated by internal/cmd/genversionindex/main.go // and run as part of go:generate. We generate the version index as part of the // build process because it's relatively expensive to build (it takes ~500ms on // my machine) and never changes. // //go:embed version_index.json var versionIndexBs []byte // init only on demand, as JSON unmarshalling comes with some cost, and contributes // noise to things like pprof stats var minVersionIndexOnce = sync.OnceValue(func() VersionIndex { var vi VersionIndex if err := json.Unmarshal(versionIndexBs, &vi); err != nil { panic(err) } return vi }) // In the compiler, we used this to check that we're OK working with ref heads. // If this isn't present, we'll fail. This is to ensure that older versions of // OPA can work with policies that we're compiling -- if they don't know ref // heads, they wouldn't be able to parse them. const FeatureRefHeadStringPrefixes = "rule_head_ref_string_prefixes" const FeatureRefHeads = "rule_head_refs" const FeatureRegoV1 = "rego_v1" const FeatureRegoV1Import = "rego_v1_import" const FeatureKeywordsInRefs = "keywords_in_refs" const FeatureTemplateStrings = "template_strings" // Features carries the default features supported by this version of OPA. // Use RegisterFeatures to add to them. var Features = []string{ FeatureRegoV1, FeatureKeywordsInRefs, FeatureTemplateStrings, } // RegisterFeatures lets applications wrapping OPA register features, to be // included in `ast.CapabilitiesForThisVersion()`. func RegisterFeatures(fs ...string) { for i := range fs { if slices.Contains(Features, fs[i]) { continue } Features = append(Features, fs[i]) } } // Capabilities defines a structure containing data that describes the capabilities // or features supported by a particular version of OPA. type Capabilities struct { Builtins []*Builtin `json:"builtins,omitempty"` FutureKeywords []string `json:"future_keywords,omitempty"` WasmABIVersions []WasmABIVersion `json:"wasm_abi_versions,omitempty"` // Features is a bit of a mixed bag for checking that an older version of OPA // is able to do what needs to be done. // TODO(sr): find better words ^^ Features []string `json:"features,omitempty"` // allow_net is an array of hostnames or IP addresses, that an OPA instance is // allowed to connect to. // If omitted, ANY host can be connected to. If empty, NO host can be connected to. // As of now, this only controls fetching remote refs for using JSON Schemas in // the type checker. // TODO(sr): support ports to further restrict connection peers AllowNet []string `json:"allow_net,omitempty"` } // WasmABIVersion captures the Wasm ABI version. Its `Minor` version is indicating // backwards-compatible changes. type WasmABIVersion struct { Version int `json:"version"` Minor int `json:"minor_version"` } type CapabilitiesOptions struct { regoVersion RegoVersion } func newCapabilitiesOptions(opts []CapabilitiesOption) CapabilitiesOptions { co := CapabilitiesOptions{} for _, opt := range opts { opt(&co) } return co } type CapabilitiesOption func(*CapabilitiesOptions) func CapabilitiesRegoVersion(regoVersion RegoVersion) CapabilitiesOption { return func(o *CapabilitiesOptions) { o.regoVersion = regoVersion } } // CapabilitiesForThisVersion returns the capabilities of this version of OPA. func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities { co := newCapabilitiesOptions(opts) f := &Capabilities{} for _, vers := range capabilities.ABIVersions() { f.WasmABIVersions = append(f.WasmABIVersions, WasmABIVersion{Version: vers[0], Minor: vers[1]}) } f.Builtins = make([]*Builtin, len(Builtins)) copy(f.Builtins, Builtins) slices.SortFunc(f.Builtins, func(a, b *Builtin) int { return strings.Compare(a.Name, b.Name) }) switch co.regoVersion { case RegoV0, RegoV0CompatV1: for kw := range allFutureKeywords { f.FutureKeywords = append(f.FutureKeywords, kw) } f.Features = []string{ FeatureRefHeadStringPrefixes, FeatureRefHeads, FeatureRegoV1Import, FeatureRegoV1, // Included in v0 capabilities to allow v1 bundles in --v0-compatible mode FeatureKeywordsInRefs, } default: for kw := range futureKeywords { f.FutureKeywords = append(f.FutureKeywords, kw) } f.Features = make([]string, len(Features)) copy(f.Features, Features) } sort.Strings(f.FutureKeywords) sort.Strings(f.Features) return f } // LoadCapabilitiesJSON loads a JSON serialized capabilities structure from the reader r. func LoadCapabilitiesJSON(r io.Reader) (*Capabilities, error) { d := util.NewJSONDecoder(r) var c Capabilities return &c, d.Decode(&c) } // LoadCapabilitiesVersion loads a JSON serialized capabilities structure from the specific version. func LoadCapabilitiesVersion(version string) (*Capabilities, error) { cvs, err := LoadCapabilitiesVersions() if err != nil { return nil, err } for _, cv := range cvs { if cv == version { cont, err := caps.FS.ReadFile(cv + ".json") if err != nil { return nil, err } return LoadCapabilitiesJSON(bytes.NewReader(cont)) } } return nil, fmt.Errorf("no capabilities version found %v", version) } // LoadCapabilitiesFile loads a JSON serialized capabilities structure from a file. func LoadCapabilitiesFile(file string) (*Capabilities, error) { fd, err := os.Open(file) if err != nil { return nil, err } defer fd.Close() return LoadCapabilitiesJSON(fd) } // LoadCapabilitiesVersions loads all capabilities versions func LoadCapabilitiesVersions() ([]string, error) { ents, err := caps.FS.ReadDir(".") if err != nil { return nil, err } capabilitiesVersions := make([]string, 0, len(ents)) for _, ent := range ents { capabilitiesVersions = append(capabilitiesVersions, strings.Replace(ent.Name(), ".json", "", 1)) } slices.SortStableFunc(capabilitiesVersions, semver.Compare) return capabilitiesVersions, nil } // MinimumCompatibleVersion returns the minimum compatible OPA version based on // the built-ins, features, and keywords in c. func (c *Capabilities) MinimumCompatibleVersion() (string, bool) { // this is the oldest OPA release that includes capabilities maxVersion := semver.MustParse("0.17.0") minVersionIndex := minVersionIndexOnce() for _, bi := range c.Builtins { v, ok := minVersionIndex.Builtins[bi.Name] if !ok { return "", false } if v.Compare(maxVersion) > 0 { maxVersion = v } } for _, kw := range c.FutureKeywords { v, ok := minVersionIndex.Keywords[kw] if !ok { return "", false } if v.Compare(maxVersion) > 0 { maxVersion = v } } for _, feat := range c.Features { v, ok := minVersionIndex.Features[feat] if !ok { return "", false } if v.Compare(maxVersion) > 0 { maxVersion = v } } return maxVersion.String(), true } func (c *Capabilities) ContainsFeature(feature string) bool { return slices.Contains(c.Features, feature) } func (c *Capabilities) ContainsBuiltin(name string) bool { return slices.ContainsFunc(c.Builtins, func(builtin *Builtin) bool { return builtin.Name == name }) } // addBuiltinSorted inserts a built-in into c in sorted order. An existing built-in with the same name // will be overwritten. func (c *Capabilities) addBuiltinSorted(bi *Builtin) { i := sort.Search(len(c.Builtins), func(x int) bool { return c.Builtins[x].Name >= bi.Name }) if i < len(c.Builtins) && bi.Name == c.Builtins[i].Name { c.Builtins[i] = bi return } c.Builtins = append(c.Builtins, nil) copy(c.Builtins[i+1:], c.Builtins[i:]) c.Builtins[i] = bi }