build(deps): bump github.com/open-policy-agent/opa from 0.51.0 to 0.59.0
Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 0.51.0 to 0.59.0. - [Release notes](https://github.com/open-policy-agent/opa/releases) - [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-policy-agent/opa/compare/v0.51.0...v0.59.0) --- updated-dependencies: - dependency-name: github.com/open-policy-agent/opa dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
committed by
Ralf Haferkamp
parent
a6a6c22c14
commit
1f069c7c00
+15
-11
@@ -11,6 +11,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/internal/deepcopy"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
@@ -39,7 +40,7 @@ type (
|
||||
|
||||
comments []*Comment
|
||||
node Node
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// SchemaAnnotation contains a schema declaration for the document identified by the path.
|
||||
@@ -76,7 +77,7 @@ type (
|
||||
Annotations *Annotations `json:"annotations,omitempty"`
|
||||
Location *Location `json:"location,omitempty"` // The location of the node the annotations are applied to
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
|
||||
node Node // The node the annotations are applied to
|
||||
}
|
||||
@@ -174,14 +175,17 @@ func (a *Annotations) GetTargetPath() Ref {
|
||||
case *Package:
|
||||
return n.Path
|
||||
case *Rule:
|
||||
return n.Path()
|
||||
return n.Ref().GroundPrefix()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Annotations) setJSONOptions(opts JSONOptions) {
|
||||
func (a *Annotations) setJSONOptions(opts astJSON.Options) {
|
||||
a.jsonOptions = opts
|
||||
if a.Location != nil {
|
||||
a.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
@@ -348,12 +352,12 @@ func compareRelatedResources(a, b []*RelatedResourceAnnotation) int {
|
||||
}
|
||||
|
||||
func compareSchemas(a, b []*SchemaAnnotation) int {
|
||||
max := len(a)
|
||||
if len(b) < max {
|
||||
max = len(b)
|
||||
maxLen := len(a)
|
||||
if len(b) < maxLen {
|
||||
maxLen = len(b)
|
||||
}
|
||||
|
||||
for i := 0; i < max; i++ {
|
||||
for i := 0; i < maxLen; i++ {
|
||||
if cmp := a[i].Compare(b[i]); cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
@@ -715,7 +719,7 @@ func (as *AnnotationSet) add(a *Annotations) *Error {
|
||||
}
|
||||
case annotationScopeDocument:
|
||||
if rule, ok := a.node.(*Rule); ok {
|
||||
path := rule.Path()
|
||||
path := rule.Ref().GroundPrefix()
|
||||
x := as.byPath.get(path)
|
||||
if x != nil {
|
||||
return errAnnotationRedeclared(a, x.Value.Location)
|
||||
@@ -811,7 +815,7 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
|
||||
// Make sure there is always a leading entry representing the passed rule, even if it has no annotations
|
||||
refs = append(refs, &AnnotationsRef{
|
||||
Location: rule.Location,
|
||||
Path: rule.Path(),
|
||||
Path: rule.Ref().GroundPrefix(),
|
||||
node: rule,
|
||||
})
|
||||
}
|
||||
@@ -823,7 +827,7 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
|
||||
})
|
||||
}
|
||||
|
||||
docAnnots := as.GetDocumentScope(rule.Path())
|
||||
docAnnots := as.GetDocumentScope(rule.Ref().GroundPrefix())
|
||||
if docAnnots != nil {
|
||||
refs = append(refs, NewAnnotationsRef(docAnnots))
|
||||
}
|
||||
|
||||
+105
-4
@@ -133,9 +133,11 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
TrimSpace,
|
||||
Sprintf,
|
||||
StringReverse,
|
||||
RenderTemplate,
|
||||
|
||||
// Numbers
|
||||
NumbersRange,
|
||||
NumbersRangeStep,
|
||||
RandIntn,
|
||||
|
||||
// Encoding
|
||||
@@ -210,10 +212,13 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
CryptoSha256,
|
||||
CryptoX509ParseCertificateRequest,
|
||||
CryptoX509ParseRSAPrivateKey,
|
||||
CryptoX509ParseKeyPair,
|
||||
CryptoParsePrivateKeys,
|
||||
CryptoHmacMd5,
|
||||
CryptoHmacSha1,
|
||||
CryptoHmacSha256,
|
||||
CryptoHmacSha512,
|
||||
CryptoHmacEqual,
|
||||
|
||||
// Graphs
|
||||
WalkBuiltin,
|
||||
@@ -282,6 +287,7 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
|
||||
// UUIDs
|
||||
UUIDRFC4122,
|
||||
UUIDParse,
|
||||
|
||||
// SemVers
|
||||
SemVerIsValid,
|
||||
@@ -1312,6 +1318,20 @@ var StringReverse = &Builtin{
|
||||
Categories: stringsCat,
|
||||
}
|
||||
|
||||
var RenderTemplate = &Builtin{
|
||||
Name: "strings.render_template",
|
||||
Description: `Renders a templated string with given template variables injected. For a given templated string and key/value mapping, values will be injected into the template where they are referenced by key.
|
||||
For examples of templating syntax, see https://pkg.go.dev/text/template`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("value", types.S).Description("a templated string"),
|
||||
types.Named("vars", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("a mapping of template variable keys to values"),
|
||||
),
|
||||
types.Named("result", types.S).Description("rendered template with template variables injected"),
|
||||
),
|
||||
Categories: stringsCat,
|
||||
}
|
||||
|
||||
/**
|
||||
* Numbers
|
||||
*/
|
||||
@@ -1344,6 +1364,23 @@ var NumbersRange = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var NumbersRangeStep = &Builtin{
|
||||
Name: "numbers.range_step",
|
||||
Description: `Returns an array of numbers in the given (inclusive) range incremented by a positive step.
|
||||
If "a==b", then "range == [a]"; if "a > b", then "range" is in descending order.
|
||||
If the provided "step" is less then 1, an error will be thrown.
|
||||
If "b" is not in the range of the provided "step", "b" won't be included in the result.
|
||||
`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("a", types.N),
|
||||
types.Named("b", types.N),
|
||||
types.Named("step", types.N),
|
||||
),
|
||||
types.Named("range", types.NewArray(nil, types.N)).Description("the range between `a` and `b` in `step` increments"),
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* Units
|
||||
*/
|
||||
@@ -1397,6 +1434,19 @@ var UUIDRFC4122 = &Builtin{
|
||||
Nondeterministic: true,
|
||||
}
|
||||
|
||||
var UUIDParse = &Builtin{
|
||||
Name: "uuid.parse",
|
||||
Description: "Parses the string value as an UUID and returns an object with the well-defined fields of the UUID if valid.",
|
||||
Categories: nil,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("uuid", types.S),
|
||||
),
|
||||
types.Named("result", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("Properties of UUID if valid (version, variant, etc). Undefined otherwise."),
|
||||
),
|
||||
Relation: false,
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON
|
||||
*/
|
||||
@@ -2158,7 +2208,7 @@ var Format = &Builtin{
|
||||
types.N,
|
||||
types.NewArray([]types.Type{types.N, types.S}, nil),
|
||||
types.NewArray([]types.Type{types.N, types.S, types.S}, nil),
|
||||
)).Description("a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string (see golang supported time formats)"),
|
||||
)).Description("a number representing the nanoseconds since the epoch (UTC); or a two-element array of the nanoseconds, and a timezone string; or a three-element array of ns, timezone string and a layout string or golang defined formatting constant (see golang supported time formats)"),
|
||||
),
|
||||
types.Named("formatted timestamp", types.S).Description("the formatted timestamp represented for the nanoseconds since the epoch in the supplied timezone (or UTC)"),
|
||||
),
|
||||
@@ -2209,7 +2259,7 @@ var Weekday = &Builtin{
|
||||
|
||||
var AddDate = &Builtin{
|
||||
Name: "time.add_date",
|
||||
Description: "Returns the nanoseconds since epoch after adding years, months and days to nanoseconds. `undefined` if the result would be outside the valid time range that can fit within an `int64`.",
|
||||
Description: "Returns the nanoseconds since epoch after adding years, months and days to nanoseconds. Month & day values outside their usual ranges after the operation and will be normalized - for example, October 32 would become November 1. `undefined` if the result would be outside the valid time range that can fit within an `int64`.",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("ns", types.N).Description("nanoseconds since the epoch"),
|
||||
@@ -2288,6 +2338,17 @@ var CryptoX509ParseCertificateRequest = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoX509ParseKeyPair = &Builtin{
|
||||
Name: "crypto.x509.parse_keypair",
|
||||
Description: "Returns a valid key pair",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("cert", types.S).Description("string containing PEM or base64 encoded DER certificates"),
|
||||
types.Named("pem", types.S).Description("string containing PEM or base64 encoded DER keys"),
|
||||
),
|
||||
types.Named("output", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("if key pair is valid, returns the tls.certificate(https://pkg.go.dev/crypto/tls#Certificate) as an object. If the key pair is invalid, nil and an error are returned."),
|
||||
),
|
||||
}
|
||||
var CryptoX509ParseRSAPrivateKey = &Builtin{
|
||||
Name: "crypto.x509.parse_rsa_private_key",
|
||||
Description: "Returns a JWK for signing a JWT from the given PEM-encoded RSA private key.",
|
||||
@@ -2299,6 +2360,19 @@ var CryptoX509ParseRSAPrivateKey = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoParsePrivateKeys = &Builtin{
|
||||
Name: "crypto.parse_private_keys",
|
||||
Description: `Returns zero or more private keys from the given encoded string containing DER certificate data.
|
||||
|
||||
If the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.`,
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("keys", types.S).Description("PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded."),
|
||||
),
|
||||
types.Named("output", types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)))).Description("parsed private keys represented as objects"),
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoMd5 = &Builtin{
|
||||
Name: "crypto.md5",
|
||||
Description: "Returns a string representing the input string hashed with the MD5 function",
|
||||
@@ -2380,6 +2454,18 @@ var CryptoHmacSha512 = &Builtin{
|
||||
),
|
||||
}
|
||||
|
||||
var CryptoHmacEqual = &Builtin{
|
||||
Name: "crypto.hmac.equal",
|
||||
Description: "Returns a boolean representing the result of comparing two MACs for equality without leaking timing information.",
|
||||
Decl: types.NewFunction(
|
||||
types.Args(
|
||||
types.Named("mac1", types.S).Description("mac1 to compare"),
|
||||
types.Named("mac2", types.S).Description("mac2 to compare"),
|
||||
),
|
||||
types.Named("result", types.B).Description("`true` if the MACs are equals, `false` otherwise"),
|
||||
),
|
||||
}
|
||||
|
||||
/**
|
||||
* Graphs.
|
||||
*/
|
||||
@@ -2399,7 +2485,7 @@ var WalkBuiltin = &Builtin{
|
||||
types.A,
|
||||
},
|
||||
nil,
|
||||
)).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`"),
|
||||
)).Description("pairs of `path` and `value`: `path` is an array representing the pointer to `value` in `x`. If `path` is assigned a wildcard (`_`), the `walk` function will skip path creation entirely for faster evaluation."),
|
||||
),
|
||||
Categories: graphs,
|
||||
}
|
||||
@@ -3155,6 +3241,21 @@ func category(cs ...string) []string {
|
||||
return cs
|
||||
}
|
||||
|
||||
// Minimal returns a shallow copy of b with the descriptions and categories and
|
||||
// named arguments stripped out.
|
||||
func (b *Builtin) Minimal() *Builtin {
|
||||
cpy := *b
|
||||
fargs := b.Decl.FuncArgs()
|
||||
if fargs.Variadic != nil {
|
||||
cpy.Decl = types.NewVariadicFunction(fargs.Args, fargs.Variadic, b.Decl.Result())
|
||||
} else {
|
||||
cpy.Decl = types.NewFunction(fargs.Args, b.Decl.Result())
|
||||
}
|
||||
cpy.Categories = nil
|
||||
cpy.Description = ""
|
||||
return &cpy
|
||||
}
|
||||
|
||||
// IsDeprecated returns true if the Builtin function is deprecated and will be removed in a future release.
|
||||
func (b *Builtin) IsDeprecated() bool {
|
||||
return b.deprecated
|
||||
@@ -3201,7 +3302,7 @@ func (b *Builtin) Ref() Ref {
|
||||
// IsTargetPos returns true if a variable in the i-th position will be bound by
|
||||
// evaluating the call expression.
|
||||
func (b *Builtin) IsTargetPos(i int) bool {
|
||||
return len(b.Decl.Args()) == i
|
||||
return len(b.Decl.FuncArgs().Args) == i
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
||||
+105
-4
@@ -6,6 +6,8 @@ package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -13,27 +15,56 @@ import (
|
||||
"strings"
|
||||
|
||||
caps "github.com/open-policy-agent/opa/capabilities"
|
||||
"github.com/open-policy-agent/opa/internal/semver"
|
||||
"github.com/open-policy-agent/opa/internal/wasm/sdk/opa/capabilities"
|
||||
"github.com/open-policy-agent/opa/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
|
||||
|
||||
var minVersionIndex = func() VersionIndex {
|
||||
var vi VersionIndex
|
||||
err := json.Unmarshal(versionIndexBs, &vi)
|
||||
if 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 FeatureRegoV1Import = "rego_v1_import"
|
||||
|
||||
// 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"`
|
||||
FutureKeywords []string `json:"future_keywords"`
|
||||
WasmABIVersions []WasmABIVersion `json:"wasm_abi_versions"`
|
||||
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"`
|
||||
Features []string `json:"features,omitempty"`
|
||||
|
||||
// allow_net is an array of hostnames or IP addresses, that an OPA instance is
|
||||
// allowed to connect to.
|
||||
@@ -73,6 +104,8 @@ func CapabilitiesForThisVersion() *Capabilities {
|
||||
|
||||
f.Features = []string{
|
||||
FeatureRefHeadStringPrefixes,
|
||||
FeatureRefHeads,
|
||||
FeatureRegoV1Import,
|
||||
}
|
||||
|
||||
return f
|
||||
@@ -129,3 +162,71 @@ func LoadCapabilitiesVersions() ([]string, error) {
|
||||
}
|
||||
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) {
|
||||
|
||||
var maxVersion semver.Version
|
||||
|
||||
// this is the oldest OPA release that includes capabilities
|
||||
if err := maxVersion.Set("0.17.0"); err != nil {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
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 {
|
||||
for _, f := range c.Features {
|
||||
if f == feature {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
+79
-38
@@ -24,6 +24,8 @@ type exprChecker func(*TypeEnv, *Expr) *Error
|
||||
// accumulated on the typeChecker so that a single run can report multiple
|
||||
// issues.
|
||||
type typeChecker struct {
|
||||
builtins map[string]*Builtin
|
||||
required *Capabilities
|
||||
errs Errors
|
||||
exprCheckers map[string]exprChecker
|
||||
varRewriter varRewriter
|
||||
@@ -60,6 +62,16 @@ func (tc *typeChecker) copy() *typeChecker {
|
||||
WithInputType(tc.input)
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithRequiredCapabilities(c *Capabilities) *typeChecker {
|
||||
tc.required = c
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithBuiltins(builtins map[string]*Builtin) *typeChecker {
|
||||
tc.builtins = builtins
|
||||
return tc
|
||||
}
|
||||
|
||||
func (tc *typeChecker) WithSchemaSet(ss *SchemaSet) *typeChecker {
|
||||
tc.ss = ss
|
||||
return tc
|
||||
@@ -177,27 +189,26 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
|
||||
|
||||
env = env.wrap()
|
||||
|
||||
if schemaAnnots := getRuleAnnotation(as, rule); schemaAnnots != nil {
|
||||
for _, schemaAnnot := range schemaAnnots {
|
||||
ref, refType, err := processAnnotation(tc.ss, schemaAnnot, rule, tc.allowNet)
|
||||
schemaAnnots := getRuleAnnotation(as, rule)
|
||||
for _, schemaAnnot := range schemaAnnots {
|
||||
ref, refType, err := processAnnotation(tc.ss, schemaAnnot, rule, tc.allowNet)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
if ref == nil && refType == nil {
|
||||
continue
|
||||
}
|
||||
prefixRef, t := getPrefix(env, ref)
|
||||
if t == nil || len(prefixRef) == len(ref) {
|
||||
env.tree.Put(ref, refType)
|
||||
} else {
|
||||
newType, err := override(ref[len(prefixRef):], t, refType, rule)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
if ref == nil && refType == nil {
|
||||
continue
|
||||
}
|
||||
prefixRef, t := getPrefix(env, ref)
|
||||
if t == nil || len(prefixRef) == len(ref) {
|
||||
env.tree.Put(ref, refType)
|
||||
} else {
|
||||
newType, err := override(ref[len(prefixRef):], t, refType, rule)
|
||||
if err != nil {
|
||||
tc.err([]*Error{err})
|
||||
continue
|
||||
}
|
||||
env.tree.Put(prefixRef, newType)
|
||||
}
|
||||
env.tree.Put(prefixRef, newType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,47 +243,66 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
|
||||
|
||||
f := types.NewFunction(args, cpy.Get(rule.Head.Value))
|
||||
|
||||
// Union with existing.
|
||||
exist := env.tree.Get(path)
|
||||
tpe = types.Or(exist, f)
|
||||
|
||||
tpe = f
|
||||
} else {
|
||||
switch rule.Head.RuleKind() {
|
||||
case SingleValue:
|
||||
typeV := cpy.Get(rule.Head.Value)
|
||||
if last := path[len(path)-1]; !last.IsGround() {
|
||||
|
||||
// e.g. store object[string: whatever] at data.p.q.r, not data.p.q.r[x]
|
||||
if !path.IsGround() {
|
||||
// e.g. store object[string: whatever] at data.p.q.r, not data.p.q.r[x] or data.p.q.r[x].y[z]
|
||||
objPath := path.DynamicSuffix()
|
||||
path = path.GroundPrefix()
|
||||
|
||||
typeK := cpy.Get(last)
|
||||
if typeK != nil && typeV != nil {
|
||||
exist := env.tree.Get(path)
|
||||
typeV = types.Or(types.Values(exist), typeV)
|
||||
typeK = types.Or(types.Keys(exist), typeK)
|
||||
tpe = types.NewObject(nil, types.NewDynamicProperty(typeK, typeV))
|
||||
var err error
|
||||
tpe, err = nestedObject(cpy, objPath, typeV)
|
||||
if err != nil {
|
||||
tc.err([]*Error{NewError(TypeErr, rule.Head.Location, err.Error())})
|
||||
tpe = nil
|
||||
}
|
||||
} else {
|
||||
if typeV != nil {
|
||||
exist := env.tree.Get(path)
|
||||
tpe = types.Or(typeV, exist)
|
||||
tpe = typeV
|
||||
}
|
||||
}
|
||||
case MultiValue:
|
||||
typeK := cpy.Get(rule.Head.Key)
|
||||
if typeK != nil {
|
||||
exist := env.tree.Get(path)
|
||||
typeK = types.Or(types.Keys(exist), typeK)
|
||||
tpe = types.NewSet(typeK)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tpe != nil {
|
||||
env.tree.Put(path, tpe)
|
||||
env.tree.Insert(path, tpe, env)
|
||||
}
|
||||
}
|
||||
|
||||
// nestedObject creates a nested structure of object types, where each term on path corresponds to a level in the
|
||||
// nesting. Each term in the path only contributes to the dynamic portion of its corresponding object.
|
||||
func nestedObject(env *TypeEnv, path Ref, tpe types.Type) (types.Type, error) {
|
||||
if len(path) == 0 {
|
||||
return tpe, nil
|
||||
}
|
||||
|
||||
k := path[0]
|
||||
typeV, err := nestedObject(env, path[1:], tpe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typeV == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var dynamicProperty *types.DynamicProperty
|
||||
typeK := env.Get(k)
|
||||
if typeK == nil {
|
||||
return nil, nil
|
||||
}
|
||||
dynamicProperty = types.NewDynamicProperty(typeK, typeV)
|
||||
|
||||
return types.NewObject(nil, dynamicProperty), nil
|
||||
}
|
||||
|
||||
func (tc *typeChecker) checkExpr(env *TypeEnv, expr *Expr) *Error {
|
||||
if err := tc.checkExprWith(env, expr, 0); err != nil {
|
||||
return err
|
||||
@@ -281,7 +311,18 @@ func (tc *typeChecker) checkExpr(env *TypeEnv, expr *Expr) *Error {
|
||||
return nil
|
||||
}
|
||||
|
||||
checker := tc.exprCheckers[expr.Operator().String()]
|
||||
operator := expr.Operator().String()
|
||||
|
||||
// If the type checker wasn't provided with a required capabilities
|
||||
// structure then just skip. In some cases, type checking might be run
|
||||
// without the need to record what builtins are required.
|
||||
if tc.required != nil {
|
||||
if bi, ok := tc.builtins[operator]; ok {
|
||||
tc.required.addBuiltinSorted(bi)
|
||||
}
|
||||
}
|
||||
|
||||
checker := tc.exprCheckers[operator]
|
||||
if checker != nil {
|
||||
return checker(env, expr)
|
||||
}
|
||||
@@ -389,7 +430,7 @@ func (tc *typeChecker) checkExprWith(env *TypeEnv, expr *Expr, i int) *Error {
|
||||
switch v := valueType.(type) {
|
||||
case *types.Function: // ...by function
|
||||
if !unifies(targetType, valueType) {
|
||||
return newArgError(expr.With[i].Loc(), target.Value.(Ref), "arity mismatch", v.Args(), t.NamedFuncArgs())
|
||||
return newArgError(expr.With[i].Loc(), target.Value.(Ref), "arity mismatch", v.FuncArgs().Args, t.NamedFuncArgs())
|
||||
}
|
||||
default: // ... by value, nothing to check
|
||||
}
|
||||
@@ -1200,7 +1241,7 @@ func getRuleAnnotation(as *AnnotationSet, rule *Rule) (result []*SchemaAnnotatio
|
||||
result = append(result, x.Schemas...)
|
||||
}
|
||||
|
||||
if x := as.GetDocumentScope(rule.Path()); x != nil {
|
||||
if x := as.GetDocumentScope(rule.Ref().GroundPrefix()); x != nil {
|
||||
result = append(result, x.Schemas...)
|
||||
}
|
||||
|
||||
|
||||
+415
-199
File diff suppressed because it is too large
Load Diff
+187
@@ -6,6 +6,7 @@ package ast
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/types"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
@@ -171,6 +172,11 @@ func (env *TypeEnv) getRefRec(node *typeTreeNode, ref, tail Ref) types.Type {
|
||||
}
|
||||
|
||||
if node.Leaf() {
|
||||
if node.children.Len() > 0 {
|
||||
if child := node.Child(tail[0].Value); child != nil {
|
||||
return env.getRefRec(child, ref, tail[1:])
|
||||
}
|
||||
}
|
||||
return selectRef(node.Value(), tail)
|
||||
}
|
||||
|
||||
@@ -304,6 +310,187 @@ func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
|
||||
curr.value = tpe
|
||||
}
|
||||
|
||||
// Insert inserts tpe at path in the tree, but also merges the value into any types.Object present along that path.
|
||||
// If a types.Object is inserted, any leafs already present further down the tree are merged into the inserted object.
|
||||
// path must be ground.
|
||||
func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
|
||||
curr := n
|
||||
for i, term := range path {
|
||||
c, ok := curr.children.Get(term.Value)
|
||||
|
||||
var child *typeTreeNode
|
||||
if !ok {
|
||||
child = newTypeTree()
|
||||
child.key = term.Value
|
||||
curr.children.Put(child.key, child)
|
||||
} else {
|
||||
child = c.(*typeTreeNode)
|
||||
|
||||
if child.value != nil && i+1 < len(path) {
|
||||
// If child has an object value, merge the new value into it.
|
||||
if o, ok := child.value.(*types.Object); ok {
|
||||
var err error
|
||||
child.value, err = insertIntoObject(o, path[i+1:], tpe, env)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curr = child
|
||||
}
|
||||
|
||||
curr.value = mergeTypes(curr.value, tpe)
|
||||
|
||||
if _, ok := tpe.(*types.Object); ok && curr.children.Len() > 0 {
|
||||
// merge all leafs into the inserted object
|
||||
leafs := curr.Leafs()
|
||||
for p, t := range leafs {
|
||||
var err error
|
||||
curr.value, err = insertIntoObject(curr.value.(*types.Object), *p, t, env)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mergeTypes merges the types of 'a' and 'b'. If both are sets, their 'of' types are joined with an types.Or.
|
||||
// If both are objects, the key types of their dynamic properties are joined with types.Or:s, and their value types
|
||||
// are recursively merged (using mergeTypes).
|
||||
// If 'a' and 'b' are both objects, and at least one of them have static properties, they are joined
|
||||
// with an types.Or, instead of being merged.
|
||||
// If 'a' is an Any containing an Object, and 'b' is an Object (or vice versa); AND both objects have no
|
||||
// static properties, they are merged.
|
||||
// If 'a' and 'b' are different types, they are joined with an types.Or.
|
||||
func mergeTypes(a, b types.Type) types.Type {
|
||||
if a == nil {
|
||||
return b
|
||||
}
|
||||
|
||||
if b == nil {
|
||||
return a
|
||||
}
|
||||
|
||||
switch a := a.(type) {
|
||||
case *types.Object:
|
||||
if bObj, ok := b.(*types.Object); ok && len(a.StaticProperties()) == 0 && len(bObj.StaticProperties()) == 0 {
|
||||
if len(a.StaticProperties()) > 0 || len(bObj.StaticProperties()) > 0 {
|
||||
return types.Or(a, bObj)
|
||||
}
|
||||
|
||||
aDynProps := a.DynamicProperties()
|
||||
bDynProps := bObj.DynamicProperties()
|
||||
dynProps := types.NewDynamicProperty(
|
||||
types.Or(aDynProps.Key, bDynProps.Key),
|
||||
mergeTypes(aDynProps.Value, bDynProps.Value))
|
||||
return types.NewObject(nil, dynProps)
|
||||
} else if bAny, ok := b.(types.Any); ok && len(a.StaticProperties()) == 0 {
|
||||
// If a is an object type with no static components ...
|
||||
for _, t := range bAny {
|
||||
if tObj, ok := t.(*types.Object); ok && len(tObj.StaticProperties()) == 0 {
|
||||
// ... and b is a types.Any containing an object with no static components, we merge them.
|
||||
aDynProps := a.DynamicProperties()
|
||||
tDynProps := tObj.DynamicProperties()
|
||||
tDynProps.Key = types.Or(tDynProps.Key, aDynProps.Key)
|
||||
tDynProps.Value = types.Or(tDynProps.Value, aDynProps.Value)
|
||||
return bAny
|
||||
}
|
||||
}
|
||||
}
|
||||
case *types.Set:
|
||||
if bSet, ok := b.(*types.Set); ok {
|
||||
return types.NewSet(types.Or(a.Of(), bSet.Of()))
|
||||
}
|
||||
case types.Any:
|
||||
if _, ok := b.(types.Any); !ok {
|
||||
return mergeTypes(b, a)
|
||||
}
|
||||
}
|
||||
|
||||
return types.Or(a, b)
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) String() string {
|
||||
b := strings.Builder{}
|
||||
|
||||
if k := n.key; k != nil {
|
||||
b.WriteString(k.String())
|
||||
} else {
|
||||
b.WriteString("-")
|
||||
}
|
||||
|
||||
if v := n.value; v != nil {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(v.String())
|
||||
}
|
||||
|
||||
n.children.Iter(func(_, v util.T) bool {
|
||||
if child, ok := v.(*typeTreeNode); ok {
|
||||
b.WriteString("\n\t+ ")
|
||||
s := child.String()
|
||||
s = strings.ReplaceAll(s, "\n", "\n\t")
|
||||
b.WriteString(s)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func insertIntoObject(o *types.Object, path Ref, tpe types.Type, env *TypeEnv) (*types.Object, error) {
|
||||
if len(path) == 0 {
|
||||
return o, nil
|
||||
}
|
||||
|
||||
key := env.Get(path[0].Value)
|
||||
|
||||
if len(path) == 1 {
|
||||
var dynamicProps *types.DynamicProperty
|
||||
if dp := o.DynamicProperties(); dp != nil {
|
||||
dynamicProps = types.NewDynamicProperty(types.Or(o.DynamicProperties().Key, key), types.Or(o.DynamicProperties().Value, tpe))
|
||||
} else {
|
||||
dynamicProps = types.NewDynamicProperty(key, tpe)
|
||||
}
|
||||
return types.NewObject(o.StaticProperties(), dynamicProps), nil
|
||||
}
|
||||
|
||||
child, err := insertIntoObject(types.NewObject(nil, nil), path[1:], tpe, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dynamicProps *types.DynamicProperty
|
||||
if dp := o.DynamicProperties(); dp != nil {
|
||||
dynamicProps = types.NewDynamicProperty(types.Or(o.DynamicProperties().Key, key), types.Or(o.DynamicProperties().Value, child))
|
||||
} else {
|
||||
dynamicProps = types.NewDynamicProperty(key, child)
|
||||
}
|
||||
return types.NewObject(o.StaticProperties(), dynamicProps), nil
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) Leafs() map[*Ref]types.Type {
|
||||
leafs := map[*Ref]types.Type{}
|
||||
n.children.Iter(func(k, v util.T) bool {
|
||||
collectLeafs(v.(*typeTreeNode), nil, leafs)
|
||||
return false
|
||||
})
|
||||
return leafs
|
||||
}
|
||||
|
||||
func collectLeafs(n *typeTreeNode, path Ref, leafs map[*Ref]types.Type) {
|
||||
nPath := append(path, NewTerm(n.key))
|
||||
if n.Leaf() {
|
||||
leafs[&nPath] = n.Value()
|
||||
return
|
||||
}
|
||||
n.children.Iter(func(k, v util.T) bool {
|
||||
collectLeafs(v.(*typeTreeNode), nPath, leafs)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (n *typeTreeNode) Value() types.Type {
|
||||
return n.value
|
||||
}
|
||||
|
||||
+11
-8
@@ -173,6 +173,7 @@ func (i *baseDocEqIndex) AllRules(resolver ValueResolver) (*IndexResult, error)
|
||||
|
||||
result := NewIndexResult(i.kind)
|
||||
result.Default = i.defaultRule
|
||||
result.OnlyGroundRefs = i.onlyGroundRefs
|
||||
result.Rules = make([]*Rule, 0, len(tr.ordering))
|
||||
|
||||
for _, pos := range tr.ordering {
|
||||
@@ -482,8 +483,10 @@ func (node *trieNode) String() string {
|
||||
if len(node.mappers) > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d mapper(s)", len(node.mappers)))
|
||||
}
|
||||
if l := node.values.Len(); l > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d value(s)", l))
|
||||
if node.values != nil {
|
||||
if l := node.values.Len(); l > 0 {
|
||||
flags = append(flags, fmt.Sprintf("%d value(s)", l))
|
||||
}
|
||||
}
|
||||
return strings.Join(flags, " ")
|
||||
}
|
||||
@@ -697,12 +700,6 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
|
||||
return node.Traverse(resolver, tr)
|
||||
}
|
||||
|
||||
head := arr.Elem(0).Value
|
||||
|
||||
if !IsScalar(head) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if node.any != nil {
|
||||
err := node.any.traverseArray(resolver, tr, arr.Slice(1, -1))
|
||||
if err != nil {
|
||||
@@ -710,6 +707,12 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
|
||||
}
|
||||
}
|
||||
|
||||
head := arr.Elem(0).Value
|
||||
|
||||
if !IsScalar(head) {
|
||||
return nil
|
||||
}
|
||||
|
||||
child, ok := node.scalars.Get(head)
|
||||
if !ok {
|
||||
return nil
|
||||
|
||||
+26
-8
@@ -18,14 +18,15 @@ const bom = 0xFEFF
|
||||
// Scanner is used to tokenize an input stream of
|
||||
// Rego source code.
|
||||
type Scanner struct {
|
||||
offset int
|
||||
row int
|
||||
col int
|
||||
bs []byte
|
||||
curr rune
|
||||
width int
|
||||
errors []Error
|
||||
keywords map[string]tokens.Token
|
||||
offset int
|
||||
row int
|
||||
col int
|
||||
bs []byte
|
||||
curr rune
|
||||
width int
|
||||
errors []Error
|
||||
keywords map[string]tokens.Token
|
||||
regoV1Compatible bool
|
||||
}
|
||||
|
||||
// Error represents a scanner error.
|
||||
@@ -102,6 +103,23 @@ func (s *Scanner) AddKeyword(kw string, tok tokens.Token) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scanner) HasKeyword(keywords map[string]tokens.Token) bool {
|
||||
for kw := range s.keywords {
|
||||
if _, ok := keywords[kw]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scanner) SetRegoV1Compatible() {
|
||||
s.regoV1Compatible = true
|
||||
}
|
||||
|
||||
func (s *Scanner) RegoV1Compatible() bool {
|
||||
return s.regoV1Compatible
|
||||
}
|
||||
|
||||
// WithKeywords returns a new copy of the Scanner struct `s`, with the set
|
||||
// of known keywords being that of `s` with `kws` added.
|
||||
func (s *Scanner) WithKeywords(kws map[string]tokens.Token) *Scanner {
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ var strings = [...]string{
|
||||
EOF: "eof",
|
||||
Whitespace: "whitespace",
|
||||
Comment: "comment",
|
||||
Ident: "ident",
|
||||
Ident: "identifier",
|
||||
Package: "package",
|
||||
Import: "import",
|
||||
As: "as",
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package json
|
||||
|
||||
// Options defines the options for JSON operations,
|
||||
// currently only marshaling can be configured
|
||||
type Options struct {
|
||||
MarshalOptions MarshalOptions
|
||||
}
|
||||
|
||||
// MarshalOptions defines the options for JSON marshaling,
|
||||
// currently only toggling the marshaling of location information is supported
|
||||
type MarshalOptions struct {
|
||||
// IncludeLocation toggles the marshaling of location information
|
||||
IncludeLocation NodeToggle
|
||||
// IncludeLocationText additionally/optionally includes the text of the location
|
||||
IncludeLocationText bool
|
||||
// ExcludeLocationFile additionally/optionally excludes the file of the location
|
||||
// Note that this is inverted (i.e. not "include" as the default needs to remain false)
|
||||
ExcludeLocationFile bool
|
||||
}
|
||||
|
||||
// NodeToggle is a generic struct to allow the toggling of
|
||||
// settings for different ast node types
|
||||
type NodeToggle struct {
|
||||
Term bool
|
||||
Package bool
|
||||
Comment bool
|
||||
Import bool
|
||||
Rule bool
|
||||
Head bool
|
||||
Expr bool
|
||||
SomeDecl bool
|
||||
Every bool
|
||||
With bool
|
||||
Annotations bool
|
||||
AnnotationsRef bool
|
||||
}
|
||||
+43
@@ -3,8 +3,11 @@ package location
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// Location records a position in source code
|
||||
@@ -14,6 +17,9 @@ type Location struct {
|
||||
Row int `json:"row"` // The line in the source.
|
||||
Col int `json:"col"` // The column in the row.
|
||||
Offset int `json:"-"` // The byte offset for the location in the source.
|
||||
|
||||
// JSONOptions specifies options for marshaling and unmarshalling of locations
|
||||
JSONOptions astJSON.Options
|
||||
}
|
||||
|
||||
// NewLocation returns a new Location object.
|
||||
@@ -87,3 +93,40 @@ func (loc *Location) Compare(other *Location) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (loc *Location) MarshalJSON() ([]byte, error) {
|
||||
// structs are used here to preserve the field ordering of the original Location struct
|
||||
if loc.JSONOptions.MarshalOptions.ExcludeLocationFile {
|
||||
data := struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
}
|
||||
|
||||
if loc.JSONOptions.MarshalOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
File string `json:"file"`
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
File: loc.File,
|
||||
}
|
||||
|
||||
if loc.JSONOptions.MarshalOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// customJSON is an interface that can be implemented by AST nodes that
|
||||
// allows the parser to set options for JSON operations on that node.
|
||||
type customJSON interface {
|
||||
setJSONOptions(JSONOptions)
|
||||
setJSONOptions(astJSON.Options)
|
||||
}
|
||||
|
||||
+133
-72
@@ -21,9 +21,12 @@ import (
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/scanner"
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/ast/location"
|
||||
)
|
||||
|
||||
var RegoV1CompatibleRef = Ref{VarTerm("rego"), StringTerm("v1")}
|
||||
|
||||
// Note: This state is kept isolated from the parser so that we
|
||||
// can do efficient shallow copies of these values when doing a
|
||||
// save() and restore().
|
||||
@@ -84,7 +87,7 @@ func (c parsedTermCache) String() string {
|
||||
s.WriteRune('{')
|
||||
var e *parsedTermCacheItem
|
||||
for e = c.m; e != nil; e = e.next {
|
||||
fmt.Fprintf(&s, "%v", e)
|
||||
s.WriteString(fmt.Sprintf("%v", e))
|
||||
}
|
||||
s.WriteRune('}')
|
||||
return s.String()
|
||||
@@ -101,37 +104,9 @@ type ParserOptions struct {
|
||||
AllFutureKeywords bool
|
||||
FutureKeywords []string
|
||||
SkipRules bool
|
||||
JSONOptions *JSONOptions
|
||||
JSONOptions *astJSON.Options
|
||||
unreleasedKeywords bool // TODO(sr): cleanup
|
||||
}
|
||||
|
||||
// JSONOptions defines the options for JSON operations,
|
||||
// currently only marshaling can be configured
|
||||
type JSONOptions struct {
|
||||
MarshalOptions JSONMarshalOptions
|
||||
}
|
||||
|
||||
// JSONMarshalOptions defines the options for JSON marshaling,
|
||||
// currently only toggling the marshaling of location information is supported
|
||||
type JSONMarshalOptions struct {
|
||||
IncludeLocation NodeToggle
|
||||
}
|
||||
|
||||
// NodeToggle is a generic struct to allow the toggling of
|
||||
// settings for different ast node types
|
||||
type NodeToggle struct {
|
||||
Term bool
|
||||
Package bool
|
||||
Comment bool
|
||||
Import bool
|
||||
Rule bool
|
||||
Head bool
|
||||
Expr bool
|
||||
SomeDecl bool
|
||||
Every bool
|
||||
With bool
|
||||
Annotations bool
|
||||
AnnotationsRef bool
|
||||
RegoV1Compatible bool
|
||||
}
|
||||
|
||||
// NewParser creates and initializes a Parser.
|
||||
@@ -207,9 +182,9 @@ func (p *Parser) WithSkipRules(skip bool) *Parser {
|
||||
return p
|
||||
}
|
||||
|
||||
// WithJSONOptions sets the JSONOptions which will be set on nodes to configure
|
||||
// WithJSONOptions sets the Options which will be set on nodes to configure
|
||||
// their JSON marshaling behavior.
|
||||
func (p *Parser) WithJSONOptions(jsonOptions *JSONOptions) *Parser {
|
||||
func (p *Parser) WithJSONOptions(jsonOptions *astJSON.Options) *Parser {
|
||||
p.po.JSONOptions = jsonOptions
|
||||
return p
|
||||
}
|
||||
@@ -356,9 +331,14 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
|
||||
s = p.save()
|
||||
|
||||
if imp := p.parseImport(); imp != nil {
|
||||
if RegoRootDocument.Equal(imp.Path.Value.(Ref)[0]) {
|
||||
p.regoV1Import(imp)
|
||||
}
|
||||
|
||||
if FutureRootDocument.Equal(imp.Path.Value.(Ref)[0]) {
|
||||
p.futureImport(imp, allowedFutureKeywords)
|
||||
}
|
||||
|
||||
stmts = append(stmts, imp)
|
||||
continue
|
||||
} else if len(p.s.errors) > 0 {
|
||||
@@ -561,9 +541,9 @@ func (p *Parser) parseImport() *Import {
|
||||
|
||||
path := imp.Path.Value.(Ref)
|
||||
|
||||
if !RootDocumentNames.Contains(path[0]) && !FutureRootDocument.Equal(path[0]) {
|
||||
if !RootDocumentNames.Contains(path[0]) && !FutureRootDocument.Equal(path[0]) && !RegoRootDocument.Equal(path[0]) {
|
||||
p.errorf(imp.Path.Location, "unexpected import path, must begin with one of: %v, got: %v",
|
||||
RootDocumentNames.Union(NewSet(FutureRootDocument)),
|
||||
RootDocumentNames.Union(NewSet(FutureRootDocument, RegoRootDocument)),
|
||||
path[0])
|
||||
return nil
|
||||
}
|
||||
@@ -609,26 +589,32 @@ func (p *Parser) parseRules() []*Rule {
|
||||
return nil
|
||||
}
|
||||
|
||||
if usesContains {
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.Contains)
|
||||
}
|
||||
|
||||
if rule.Default {
|
||||
if !p.validateDefaultRuleValue(&rule) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(rule.Head.Args) > 0 {
|
||||
if !p.validateDefaultRuleArgs(&rule) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location))
|
||||
return []*Rule{&rule}
|
||||
}
|
||||
|
||||
if usesContains && !rule.Head.Reference.IsGround() {
|
||||
p.error(p.s.Loc(), "multi-value rules need ground refs")
|
||||
return nil
|
||||
}
|
||||
|
||||
// back-compat with `p[x] { ... }``
|
||||
hasIf := p.s.tok == tokens.If
|
||||
|
||||
// p[x] if ... becomes a single-value rule p[x]
|
||||
if hasIf && !usesContains && len(rule.Head.Ref()) == 2 {
|
||||
if rule.Head.Value == nil {
|
||||
rule.Head.generatedValue = true
|
||||
rule.Head.Value = BooleanTerm(true).SetLocation(rule.Head.Location)
|
||||
} else {
|
||||
// p[x] = y if becomes a single-value rule p[x] with value y, but needs name for compat
|
||||
@@ -657,6 +643,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
|
||||
switch {
|
||||
case hasIf:
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.If)
|
||||
p.scan()
|
||||
s := p.save()
|
||||
if expr := p.parseLiteral(); expr != nil {
|
||||
@@ -688,6 +675,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
|
||||
case usesContains:
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location))
|
||||
rule.generatedBody = true
|
||||
return []*Rule{&rule}
|
||||
|
||||
default:
|
||||
@@ -695,7 +683,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.Else {
|
||||
if r := rule.Head.Ref(); len(r) > 1 && !r[len(r)-1].Value.IsGround() {
|
||||
if r := rule.Head.Ref(); len(r) > 1 && !r.IsGround() {
|
||||
p.error(p.s.Loc(), "else keyword cannot be used on rules with variables in head")
|
||||
return nil
|
||||
}
|
||||
@@ -737,6 +725,7 @@ func (p *Parser) parseRules() []*Rule {
|
||||
// rule's head AST but have their location
|
||||
// set to the rule body.
|
||||
next.Head = rule.Head.Copy()
|
||||
next.Head.keywords = rule.Head.keywords
|
||||
for i := range next.Head.Args {
|
||||
if v, ok := next.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
|
||||
next.Head.Args[i].Value = Var(p.genwildcard())
|
||||
@@ -756,6 +745,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
rule.SetLoc(p.s.Loc())
|
||||
|
||||
rule.Head = head.Copy()
|
||||
rule.Head.generatedValue = false
|
||||
for i := range rule.Head.Args {
|
||||
if v, ok := rule.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
|
||||
rule.Head.Args[i].Value = Var(p.genwildcard())
|
||||
@@ -771,6 +761,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
|
||||
switch p.s.tok {
|
||||
case tokens.LBrace, tokens.If: // no value, but a body follows directly
|
||||
rule.Head.generatedValue = true
|
||||
rule.Head.Value = BooleanTerm(true)
|
||||
case tokens.Assign, tokens.Unify:
|
||||
rule.Head.Assign = tokens.Assign == p.s.tok
|
||||
@@ -786,42 +777,37 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
}
|
||||
|
||||
hasIf := p.s.tok == tokens.If
|
||||
hasLBrace := p.s.tok == tokens.LBrace
|
||||
|
||||
if hasIf {
|
||||
p.scan()
|
||||
s := p.save()
|
||||
if expr := p.parseLiteral(); expr != nil {
|
||||
// NOTE(sr): set literals are never false or undefined, so parsing this as
|
||||
// p if false else if { true }
|
||||
// ^^^^^^^^ set of one element, `true`
|
||||
// isn't valid.
|
||||
isSetLiteral := false
|
||||
if t, ok := expr.Terms.(*Term); ok {
|
||||
_, isSetLiteral = t.Value.(Set)
|
||||
}
|
||||
// expr.Term is []*Term or Every
|
||||
if !isSetLiteral {
|
||||
rule.Body.Append(expr)
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
return &rule
|
||||
}
|
||||
}
|
||||
p.restore(s)
|
||||
}
|
||||
|
||||
if p.s.tok != tokens.LBrace {
|
||||
if !hasIf && !hasLBrace {
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true)))
|
||||
rule.generatedBody = true
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
return &rule
|
||||
}
|
||||
|
||||
p.scan()
|
||||
|
||||
if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil {
|
||||
return nil
|
||||
if hasIf {
|
||||
rule.Head.keywords = append(rule.Head.keywords, tokens.If)
|
||||
p.scan()
|
||||
}
|
||||
|
||||
p.scan()
|
||||
if p.s.tok == tokens.LBrace {
|
||||
p.scan()
|
||||
if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil {
|
||||
return nil
|
||||
}
|
||||
p.scan()
|
||||
} else if p.s.tok != tokens.EOF {
|
||||
expr := p.parseLiteral()
|
||||
if expr == nil {
|
||||
return nil
|
||||
}
|
||||
rule.Body.Append(expr)
|
||||
setLocRecursive(rule.Body, rule.Location)
|
||||
} else {
|
||||
p.illegal("rule body expected")
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.Else {
|
||||
if rule.Else = p.parseElse(head); rule.Else == nil {
|
||||
@@ -832,7 +818,6 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
}
|
||||
|
||||
func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
head := &Head{}
|
||||
loc := p.s.Loc()
|
||||
defer func() {
|
||||
@@ -855,7 +840,9 @@ func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
switch x := ref.Value.(type) {
|
||||
case Var:
|
||||
head = NewHead(x)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(x, ref.Location, p.po.JSONOptions)
|
||||
case Ref:
|
||||
head = RefHead(x)
|
||||
case Call:
|
||||
@@ -922,6 +909,7 @@ func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
if head.Value == nil && head.Key == nil {
|
||||
if len(head.Ref()) != 2 || len(head.Args) > 0 {
|
||||
head.generatedValue = true
|
||||
head.Value = BooleanTerm(true).SetLocation(head.Location)
|
||||
}
|
||||
}
|
||||
@@ -2001,7 +1989,7 @@ func (p *Parser) error(loc *location.Location, reason string) {
|
||||
|
||||
func (p *Parser) errorf(loc *location.Location, f string, a ...interface{}) {
|
||||
msg := strings.Builder{}
|
||||
fmt.Fprintf(&msg, f, a...)
|
||||
msg.WriteString(fmt.Sprintf(f, a...))
|
||||
|
||||
switch len(p.s.hints) {
|
||||
case 0: // nothing to do
|
||||
@@ -2176,6 +2164,38 @@ func (p *Parser) validateDefaultRuleValue(rule *Rule) bool {
|
||||
return valid
|
||||
}
|
||||
|
||||
func (p *Parser) validateDefaultRuleArgs(rule *Rule) bool {
|
||||
|
||||
valid := true
|
||||
vars := NewVarSet()
|
||||
|
||||
vis := NewGenericVisitor(func(x interface{}) bool {
|
||||
switch x := x.(type) {
|
||||
case Var:
|
||||
if vars.Contains(x) {
|
||||
p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot be repeated %v)", x))
|
||||
valid = false
|
||||
return true
|
||||
}
|
||||
vars.Add(x)
|
||||
|
||||
case *Term:
|
||||
switch v := x.Value.(type) {
|
||||
case Var: // do nothing
|
||||
default:
|
||||
p.error(rule.Loc(), fmt.Sprintf("illegal default rule (arguments cannot contain %v)", TypeName(v)))
|
||||
valid = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
vis.Walk(rule.Head.Args)
|
||||
return valid
|
||||
}
|
||||
|
||||
// We explicitly use yaml unmarshalling, to accommodate for the '_' in 'related_resources',
|
||||
// which isn't handled properly by json for some reason.
|
||||
type rawAnnotation struct {
|
||||
@@ -2508,6 +2528,11 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
return
|
||||
}
|
||||
|
||||
if p.s.s.RegoV1Compatible() {
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
kwds := make([]string, 0, len(allowedFutureKeywords))
|
||||
for k := range allowedFutureKeywords {
|
||||
kwds = append(kwds, k)
|
||||
@@ -2535,3 +2560,39 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
|
||||
p.s.s.AddKeyword(kw, allowedFutureKeywords[kw])
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) regoV1Import(imp *Import) {
|
||||
if !p.po.Capabilities.ContainsFeature(FeatureRegoV1Import) {
|
||||
p.errorf(imp.Path.Location, "invalid import, `%s` is not supported by current capabilities", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
path := imp.Path.Value.(Ref)
|
||||
|
||||
if len(path) == 1 || !path[1].Equal(RegoV1CompatibleRef[1]) || len(path) > 2 {
|
||||
p.errorf(imp.Path.Location, "invalid import, must be `%s`", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
if imp.Alias != "" {
|
||||
p.errorf(imp.Path.Location, "`rego` imports cannot be aliased")
|
||||
return
|
||||
}
|
||||
|
||||
// import all future keywords with the rego.v1 import
|
||||
kwds := make([]string, 0, len(futureKeywords))
|
||||
for k := range futureKeywords {
|
||||
kwds = append(kwds, k)
|
||||
}
|
||||
|
||||
if p.s.s.HasKeyword(futureKeywords) && !p.s.s.RegoV1Compatible() {
|
||||
// We have imported future keywords, but they didn't come from another `rego.v1` import.
|
||||
p.errorf(imp.Path.Location, "the `%s` import implies `future.keywords`, these are therefore mutually exclusive", RegoV1CompatibleRef)
|
||||
return
|
||||
}
|
||||
|
||||
p.s.s.SetRegoV1Compatible()
|
||||
for _, kw := range kwds {
|
||||
p.s.s.AddKeyword(kw, futureKeywords[kw])
|
||||
}
|
||||
}
|
||||
|
||||
+112
-34
@@ -16,6 +16,9 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
)
|
||||
|
||||
// MustParseBody returns a parsed body.
|
||||
@@ -244,7 +247,9 @@ func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, erro
|
||||
var head *Head
|
||||
|
||||
if v, ok := lhs.Value.(Var); ok {
|
||||
head = NewHead(v)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(v, lhs.Location, &lhs.jsonOptions)
|
||||
} else if r, ok := lhs.Value.(Ref); ok { // groundness ?
|
||||
if _, ok := r[0].Value.(Var); !ok {
|
||||
return nil, fmt.Errorf("invalid rule head: %v", r)
|
||||
@@ -258,14 +263,17 @@ func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, erro
|
||||
}
|
||||
head.Value = rhs
|
||||
head.Location = lhs.Location
|
||||
head.setJSONOptions(lhs.jsonOptions)
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
return &Rule{
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: lhs.jsonOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -279,15 +287,20 @@ func ParseCompleteDocRuleWithDotsFromTerm(module *Module, term *Term) (*Rule, er
|
||||
return nil, fmt.Errorf("invalid rule head: %v", ref)
|
||||
}
|
||||
head := RefHead(ref, BooleanTerm(true).SetLocation(term.Location))
|
||||
head.generatedValue = true
|
||||
head.Location = term.Location
|
||||
head.jsonOptions = term.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location))
|
||||
setJSONOptions(body, &term.jsonOptions)
|
||||
|
||||
return &Rule{
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location),
|
||||
),
|
||||
Module: module,
|
||||
Body: body,
|
||||
Module: module,
|
||||
|
||||
jsonOptions: term.jsonOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -309,14 +322,17 @@ func ParsePartialObjectDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule,
|
||||
head.Key = ref[1]
|
||||
}
|
||||
head.Location = rhs.Location
|
||||
head.jsonOptions = rhs.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: rhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: rhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: rhs.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -340,18 +356,23 @@ func ParsePartialSetDocRuleFromTerm(module *Module, term *Term) (*Rule, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%vs cannot be used for rule head", TypeName(term.Value))
|
||||
}
|
||||
head = NewHead(v)
|
||||
// Modify the code to add the location to the head ref
|
||||
// and set the head ref's jsonOptions.
|
||||
head = VarHead(v, ref[0].Location, &ref[0].jsonOptions)
|
||||
head.Key = ref[1]
|
||||
}
|
||||
head.Location = term.Location
|
||||
head.jsonOptions = term.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location))
|
||||
setJSONOptions(body, &term.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: NewBody(
|
||||
NewExpr(BooleanTerm(true).SetLocation(term.Location)).SetLocation(term.Location),
|
||||
),
|
||||
Module: module,
|
||||
Location: term.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: term.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -377,12 +398,17 @@ func ParseRuleFromCallEqExpr(module *Module, lhs, rhs *Term) (*Rule, error) {
|
||||
head := RefHead(ref, rhs)
|
||||
head.Location = lhs.Location
|
||||
head.Args = Args(call[1:])
|
||||
head.jsonOptions = lhs.jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location))
|
||||
setJSONOptions(body, &rhs.jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: NewBody(NewExpr(BooleanTerm(true).SetLocation(rhs.Location)).SetLocation(rhs.Location)),
|
||||
Module: module,
|
||||
Location: lhs.Location,
|
||||
Head: head,
|
||||
Body: body,
|
||||
Module: module,
|
||||
jsonOptions: lhs.jsonOptions,
|
||||
}
|
||||
|
||||
return rule, nil
|
||||
@@ -404,12 +430,17 @@ func ParseRuleFromCallExpr(module *Module, terms []*Term) (*Rule, error) {
|
||||
head := RefHead(ref, BooleanTerm(true).SetLocation(loc))
|
||||
head.Location = loc
|
||||
head.Args = terms[1:]
|
||||
head.jsonOptions = terms[0].jsonOptions
|
||||
|
||||
body := NewBody(NewExpr(BooleanTerm(true).SetLocation(loc)).SetLocation(loc))
|
||||
setJSONOptions(body, &terms[0].jsonOptions)
|
||||
|
||||
rule := &Rule{
|
||||
Location: loc,
|
||||
Head: head,
|
||||
Module: module,
|
||||
Body: NewBody(NewExpr(BooleanTerm(true).SetLocation(loc)).SetLocation(loc)),
|
||||
Location: loc,
|
||||
Head: head,
|
||||
Module: module,
|
||||
Body: body,
|
||||
jsonOptions: terms[0].jsonOptions,
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
@@ -446,7 +477,7 @@ func ParseModuleWithOpts(filename, input string, popts ParserOptions) (*Module,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseModule(filename, stmts, comments)
|
||||
return parseModule(filename, stmts, comments, popts.RegoV1Compatible)
|
||||
}
|
||||
|
||||
// ParseBody returns exactly one body.
|
||||
@@ -606,7 +637,7 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
|
||||
return stmts, comments, nil
|
||||
}
|
||||
|
||||
func parseModule(filename string, stmts []Statement, comments []*Comment) (*Module, error) {
|
||||
func parseModule(filename string, stmts []Statement, comments []*Comment, regoV1Compatible bool) (*Module, error) {
|
||||
|
||||
if len(stmts) == 0 {
|
||||
return nil, NewError(ParseErr, &Location{File: filename}, "empty module")
|
||||
@@ -627,11 +658,15 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
|
||||
// The comments slice only holds comments that were not their own statements.
|
||||
mod.Comments = append(mod.Comments, comments...)
|
||||
mod.regoV1Compatible = regoV1Compatible
|
||||
|
||||
for i, stmt := range stmts[1:] {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Import:
|
||||
mod.Imports = append(mod.Imports, stmt)
|
||||
if Compare(stmt.Path.Value, RegoV1CompatibleRef) == 0 {
|
||||
mod.regoV1Compatible = true
|
||||
}
|
||||
case *Rule:
|
||||
setRuleModule(stmt, mod)
|
||||
mod.Rules = append(mod.Rules, stmt)
|
||||
@@ -641,6 +676,7 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
errs = append(errs, NewError(ParseErr, stmt[0].Location, err.Error()))
|
||||
continue
|
||||
}
|
||||
rule.generatedBody = true
|
||||
mod.Rules = append(mod.Rules, rule)
|
||||
|
||||
// NOTE(tsandall): the statement should now be interpreted as a
|
||||
@@ -658,6 +694,29 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
}
|
||||
}
|
||||
|
||||
if mod.regoV1Compatible {
|
||||
for _, rule := range mod.Rules {
|
||||
for r := rule; r != nil; r = r.Else {
|
||||
var t string
|
||||
if r.isFunction() {
|
||||
t = "function"
|
||||
} else {
|
||||
t = "rule"
|
||||
}
|
||||
|
||||
if r.generatedBody && r.Head.generatedValue {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "%s must have value assignment and/or body declaration", t))
|
||||
}
|
||||
if r.Body != nil && !r.generatedBody && !ruleDeclarationHasKeyword(r, tokens.If) && !r.Default {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "`if` keyword is required before %s body", t))
|
||||
}
|
||||
if r.Head.RuleKind() == MultiValue && !ruleDeclarationHasKeyword(r, tokens.Contains) {
|
||||
errs = append(errs, NewError(ParseErr, r.Location, "`contains` keyword is required for partial set rules"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
@@ -671,6 +730,15 @@ func parseModule(filename string, stmts []Statement, comments []*Comment) (*Modu
|
||||
return mod, nil
|
||||
}
|
||||
|
||||
func ruleDeclarationHasKeyword(rule *Rule, keyword tokens.Token) bool {
|
||||
for _, kw := range rule.Head.keywords {
|
||||
if kw == keyword {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newScopeAttachmentErr(a *Annotations, want string) *Error {
|
||||
var have string
|
||||
if a.node != nil {
|
||||
@@ -686,6 +754,16 @@ func setRuleModule(rule *Rule, module *Module) {
|
||||
}
|
||||
}
|
||||
|
||||
func setJSONOptions(x interface{}, jsonOptions *astJSON.Options) {
|
||||
vis := NewGenericVisitor(func(x interface{}) bool {
|
||||
if x, ok := x.(customJSON); ok {
|
||||
x.setJSONOptions(*jsonOptions)
|
||||
}
|
||||
return false
|
||||
})
|
||||
vis.Walk(x)
|
||||
}
|
||||
|
||||
// ParserErrorDetail holds additional details for parser errors.
|
||||
type ParserErrorDetail struct {
|
||||
Line string `json:"line"`
|
||||
|
||||
+83
-24
@@ -12,6 +12,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast/internal/tokens"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
@@ -42,6 +44,10 @@ var FunctionArgRootDocument = VarTerm("args")
|
||||
// features.
|
||||
var FutureRootDocument = VarTerm("future")
|
||||
|
||||
// RegoRootDocument names the document containing new, to-become-default,
|
||||
// features in a future versioned release.
|
||||
var RegoRootDocument = VarTerm("rego")
|
||||
|
||||
// RootDocumentNames contains the names of top-level documents that can be
|
||||
// referred to in modules and queries.
|
||||
//
|
||||
@@ -139,12 +145,13 @@ type (
|
||||
// within a namespace (defined by the package) and optional
|
||||
// dependencies on external documents (defined by imports).
|
||||
Module struct {
|
||||
Package *Package `json:"package"`
|
||||
Imports []*Import `json:"imports,omitempty"`
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Rules []*Rule `json:"rules,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
stmts []Statement
|
||||
Package *Package `json:"package"`
|
||||
Imports []*Import `json:"imports,omitempty"`
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Rules []*Rule `json:"rules,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
stmts []Statement
|
||||
regoV1Compatible bool
|
||||
}
|
||||
|
||||
// Comment contains the raw text from the comment in the definition.
|
||||
@@ -153,7 +160,7 @@ type (
|
||||
Text []byte
|
||||
Location *Location
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Package represents the namespace of the documents produced
|
||||
@@ -162,7 +169,7 @@ type (
|
||||
Path Ref `json:"path"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Import represents a dependency on a document outside of the policy
|
||||
@@ -172,7 +179,7 @@ type (
|
||||
Alias Var `json:"alias,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Rule represents a rule as defined in the language. Rules define the
|
||||
@@ -190,7 +197,8 @@ type (
|
||||
// on the rule (e.g., printing, comparison, visiting, etc.)
|
||||
Module *Module `json:"-"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
generatedBody bool
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Head represents the head of a rule.
|
||||
@@ -203,7 +211,9 @@ type (
|
||||
Assign bool `json:"assign,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
keywords []tokens.Token
|
||||
generatedValue bool
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// Args represents zero or more arguments to a rule.
|
||||
@@ -222,7 +232,7 @@ type (
|
||||
Negated bool `json:"negated,omitempty"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// SomeDecl represents a variable declaration statement. The symbols are variables.
|
||||
@@ -230,7 +240,7 @@ type (
|
||||
Symbols []*Term `json:"symbols"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
Every struct {
|
||||
@@ -240,7 +250,7 @@ type (
|
||||
Body Body `json:"body"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// With represents a modifier on an expression.
|
||||
@@ -249,7 +259,7 @@ type (
|
||||
Value *Term `json:"value"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
)
|
||||
|
||||
@@ -428,10 +438,13 @@ func (c *Comment) Equal(other *Comment) bool {
|
||||
return c.Location.Equal(other.Location) && bytes.Equal(c.Text, other.Text)
|
||||
}
|
||||
|
||||
func (c *Comment) setJSONOptions(opts JSONOptions) {
|
||||
func (c *Comment) setJSONOptions(opts astJSON.Options) {
|
||||
// Note: this is not used for location since Comments use default JSON marshaling
|
||||
// behavior with struct field names in JSON.
|
||||
c.jsonOptions = opts
|
||||
if c.Location != nil {
|
||||
c.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
// Compare returns an integer indicating whether pkg is less than, equal to,
|
||||
@@ -478,8 +491,11 @@ func (pkg *Package) String() string {
|
||||
return fmt.Sprintf("package %v", path)
|
||||
}
|
||||
|
||||
func (pkg *Package) setJSONOptions(opts JSONOptions) {
|
||||
func (pkg *Package) setJSONOptions(opts astJSON.Options) {
|
||||
pkg.jsonOptions = opts
|
||||
if pkg.Location != nil {
|
||||
pkg.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSON() ([]byte, error) {
|
||||
@@ -588,8 +604,11 @@ func (imp *Import) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (imp *Import) setJSONOptions(opts JSONOptions) {
|
||||
func (imp *Import) setJSONOptions(opts astJSON.Options) {
|
||||
imp.jsonOptions = opts
|
||||
if imp.Location != nil {
|
||||
imp.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (imp *Import) MarshalJSON() ([]byte, error) {
|
||||
@@ -699,8 +718,15 @@ func (rule *Rule) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (rule *Rule) setJSONOptions(opts JSONOptions) {
|
||||
func (rule *Rule) isFunction() bool {
|
||||
return len(rule.Head.Args) > 0
|
||||
}
|
||||
|
||||
func (rule *Rule) setJSONOptions(opts astJSON.Options) {
|
||||
rule.jsonOptions = opts
|
||||
if rule.Location != nil {
|
||||
rule.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (rule *Rule) MarshalJSON() ([]byte, error) {
|
||||
@@ -769,6 +795,17 @@ func NewHead(name Var, args ...*Term) *Head {
|
||||
return head
|
||||
}
|
||||
|
||||
// VarHead creates a head object, initializes its Name, Location, and Options,
|
||||
// and returns the new head.
|
||||
func VarHead(name Var, location *Location, jsonOpts *astJSON.Options) *Head {
|
||||
h := NewHead(name)
|
||||
h.Reference[0].Location = location
|
||||
if jsonOpts != nil {
|
||||
h.Reference[0].setJSONOptions(*jsonOpts)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// RefHead returns a new Head object with the passed Ref. If args are provided,
|
||||
// the first will be used for the value.
|
||||
func RefHead(ref Ref, args ...*Term) *Head {
|
||||
@@ -881,6 +918,7 @@ func (head *Head) Copy() *Head {
|
||||
cpy.Args = head.Args.Copy()
|
||||
cpy.Key = head.Key.Copy()
|
||||
cpy.Value = head.Value.Copy()
|
||||
cpy.keywords = nil
|
||||
return &cpy
|
||||
}
|
||||
|
||||
@@ -915,8 +953,11 @@ func (head *Head) String() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (head *Head) setJSONOptions(opts JSONOptions) {
|
||||
func (head *Head) setJSONOptions(opts astJSON.Options) {
|
||||
head.jsonOptions = opts
|
||||
if head.Location != nil {
|
||||
head.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (head *Head) MarshalJSON() ([]byte, error) {
|
||||
@@ -975,6 +1016,12 @@ func (head *Head) SetLoc(loc *Location) {
|
||||
head.Location = loc
|
||||
}
|
||||
|
||||
func (head *Head) HasDynamicRef() bool {
|
||||
pos := head.Reference.Dynamic()
|
||||
// Ref is dynamic if it has one non-constant term that isn't the first or last term or if it's a partial set rule.
|
||||
return pos > 0 && (pos < len(head.Reference)-1 || head.RuleKind() == MultiValue)
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of a.
|
||||
func (a Args) Copy() Args {
|
||||
cpy := Args{}
|
||||
@@ -1459,8 +1506,11 @@ func (expr *Expr) String() string {
|
||||
return strings.Join(buf, " ")
|
||||
}
|
||||
|
||||
func (expr *Expr) setJSONOptions(opts JSONOptions) {
|
||||
func (expr *Expr) setJSONOptions(opts astJSON.Options) {
|
||||
expr.jsonOptions = opts
|
||||
if expr.Location != nil {
|
||||
expr.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (expr *Expr) MarshalJSON() ([]byte, error) {
|
||||
@@ -1555,8 +1605,11 @@ func (d *SomeDecl) Hash() int {
|
||||
return termSliceHash(d.Symbols)
|
||||
}
|
||||
|
||||
func (d *SomeDecl) setJSONOptions(opts JSONOptions) {
|
||||
func (d *SomeDecl) setJSONOptions(opts astJSON.Options) {
|
||||
d.jsonOptions = opts
|
||||
if d.Location != nil {
|
||||
d.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
|
||||
@@ -1629,8 +1682,11 @@ func (q *Every) KeyValueVars() VarSet {
|
||||
return vis.vars
|
||||
}
|
||||
|
||||
func (q *Every) setJSONOptions(opts JSONOptions) {
|
||||
func (q *Every) setJSONOptions(opts astJSON.Options) {
|
||||
q.jsonOptions = opts
|
||||
if q.Location != nil {
|
||||
q.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSON() ([]byte, error) {
|
||||
@@ -1708,8 +1764,11 @@ func (w *With) SetLoc(loc *Location) {
|
||||
w.Location = loc
|
||||
}
|
||||
|
||||
func (w *With) setJSONOptions(opts JSONOptions) {
|
||||
func (w *With) setJSONOptions(opts astJSON.Options) {
|
||||
w.jsonOptions = opts
|
||||
if w.Location != nil {
|
||||
w.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSON() ([]byte, error) {
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package ast
|
||||
|
||||
func checkDuplicateImports(modules []*Module) (errors Errors) {
|
||||
for _, module := range modules {
|
||||
processedImports := map[Var]*Import{}
|
||||
|
||||
for _, imp := range module.Imports {
|
||||
name := imp.Name()
|
||||
|
||||
if processed, conflict := processedImports[name]; conflict {
|
||||
errors = append(errors, NewError(CompileErr, imp.Location, "import must not shadow %v", processed))
|
||||
} else {
|
||||
processedImports[name] = imp
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func checkRootDocumentOverrides(node interface{}) Errors {
|
||||
errors := Errors{}
|
||||
|
||||
WalkRules(node, func(rule *Rule) bool {
|
||||
var name string
|
||||
if len(rule.Head.Reference) > 0 {
|
||||
name = rule.Head.Reference[0].Value.(Var).String()
|
||||
} else {
|
||||
name = rule.Head.Name.String()
|
||||
}
|
||||
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
|
||||
errors = append(errors, NewError(CompileErr, rule.Location, "rules must not shadow %v (use a different rule name)", name))
|
||||
}
|
||||
|
||||
for _, arg := range rule.Head.Args {
|
||||
if _, ok := arg.Value.(Ref); ok {
|
||||
if RootDocumentRefs.Contains(arg) {
|
||||
errors = append(errors, NewError(CompileErr, arg.Location, "args must not shadow %v (use a different variable name)", arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
WalkExprs(node, func(expr *Expr) bool {
|
||||
if expr.IsAssignment() {
|
||||
name := expr.Operand(0).String()
|
||||
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
|
||||
errors = append(errors, NewError(CompileErr, expr.Location, "variables must not shadow %v (use a different variable name)", name))
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
func walkCalls(node interface{}, f func(interface{}) bool) {
|
||||
vis := &GenericVisitor{func(x interface{}) bool {
|
||||
switch x := x.(type) {
|
||||
case Call:
|
||||
return f(x)
|
||||
case *Expr:
|
||||
if x.IsCall() {
|
||||
return f(x)
|
||||
}
|
||||
case *Head:
|
||||
// GenericVisitor doesn't walk the rule head ref
|
||||
walkCalls(x.Reference, f)
|
||||
}
|
||||
return false
|
||||
}}
|
||||
vis.Walk(node)
|
||||
}
|
||||
|
||||
func checkDeprecatedBuiltins(deprecatedBuiltinsMap map[string]struct{}, node interface{}) Errors {
|
||||
errs := make(Errors, 0)
|
||||
|
||||
walkCalls(node, func(x interface{}) bool {
|
||||
var operator string
|
||||
var loc *Location
|
||||
|
||||
switch x := x.(type) {
|
||||
case *Expr:
|
||||
operator = x.Operator().String()
|
||||
loc = x.Loc()
|
||||
case Call:
|
||||
terms := []*Term(x)
|
||||
if len(terms) > 0 {
|
||||
operator = terms[0].Value.String()
|
||||
loc = terms[0].Loc()
|
||||
}
|
||||
}
|
||||
|
||||
if operator != "" {
|
||||
if _, ok := deprecatedBuiltinsMap[operator]; ok {
|
||||
errs = append(errs, NewError(TypeErr, loc, "deprecated built-in function calls in expression: %v", operator))
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func checkDeprecatedBuiltinsForCurrentVersion(node interface{}) Errors {
|
||||
deprecatedBuiltins := make(map[string]struct{})
|
||||
capabilities := CapabilitiesForThisVersion()
|
||||
for _, bi := range capabilities.Builtins {
|
||||
if bi.IsDeprecated() {
|
||||
deprecatedBuiltins[bi.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return checkDeprecatedBuiltins(deprecatedBuiltins, node)
|
||||
}
|
||||
|
||||
// CheckRegoV1 checks the given module for errors that are specific to Rego v1
|
||||
func CheckRegoV1(module *Module) Errors {
|
||||
var errors Errors
|
||||
errors = append(errors, checkDuplicateImports([]*Module{module})...)
|
||||
errors = append(errors, checkRootDocumentOverrides(module)...)
|
||||
errors = append(errors, checkDeprecatedBuiltinsForCurrentVersion(module)...)
|
||||
return errors
|
||||
}
|
||||
+65
-9
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/OneOfOne/xxhash"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/ast/location"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
@@ -294,7 +295,7 @@ type Term struct {
|
||||
Value Value `json:"value"` // the value of the Term as represented in Go
|
||||
Location *Location `json:"location,omitempty"` // the location of the Term in the source
|
||||
|
||||
jsonOptions JSONOptions
|
||||
jsonOptions astJSON.Options
|
||||
}
|
||||
|
||||
// NewTerm returns a new Term object.
|
||||
@@ -419,8 +420,11 @@ func (term *Term) IsGround() bool {
|
||||
return term.Value.IsGround()
|
||||
}
|
||||
|
||||
func (term *Term) setJSONOptions(opts JSONOptions) {
|
||||
func (term *Term) setJSONOptions(opts astJSON.Options) {
|
||||
term.jsonOptions = opts
|
||||
if term.Location != nil {
|
||||
term.Location.JSONOptions = opts
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the term.
|
||||
@@ -888,8 +892,8 @@ func PtrRef(head *Term, s string) (Ref, error) {
|
||||
return Ref{head}, nil
|
||||
}
|
||||
parts := strings.Split(s, "/")
|
||||
if max := math.MaxInt32; len(parts) >= max {
|
||||
return nil, fmt.Errorf("path too long: %s, %d > %d (max)", s, len(parts), max)
|
||||
if maxLen := math.MaxInt32; len(parts) >= maxLen {
|
||||
return nil, fmt.Errorf("path too long: %s, %d > %d (max)", s, len(parts), maxLen)
|
||||
}
|
||||
ref := make(Ref, uint(len(parts))+1)
|
||||
ref[0] = head
|
||||
@@ -1028,6 +1032,20 @@ func (ref Ref) ConstantPrefix() Ref {
|
||||
return ref[:i]
|
||||
}
|
||||
|
||||
func (ref Ref) StringPrefix() Ref {
|
||||
r := ref.Copy()
|
||||
|
||||
for i := 1; i < len(ref); i++ {
|
||||
switch r[i].Value.(type) {
|
||||
case String: // pass
|
||||
default: // cut off
|
||||
return r[:i]
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// GroundPrefix returns the ground portion of the ref starting from the head. By
|
||||
// definition, the head of the reference is always ground.
|
||||
func (ref Ref) GroundPrefix() Ref {
|
||||
@@ -1043,6 +1061,14 @@ func (ref Ref) GroundPrefix() Ref {
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (ref Ref) DynamicSuffix() Ref {
|
||||
i := ref.Dynamic()
|
||||
if i < 0 {
|
||||
return nil
|
||||
}
|
||||
return ref[i:]
|
||||
}
|
||||
|
||||
// IsGround returns true if all of the parts of the Ref are ground.
|
||||
func (ref Ref) IsGround() bool {
|
||||
if len(ref) == 0 {
|
||||
@@ -1078,6 +1104,10 @@ func (ref Ref) Ptr() (string, error) {
|
||||
|
||||
var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
|
||||
|
||||
func IsVarCompatibleString(s string) bool {
|
||||
return varRegexp.MatchString(s)
|
||||
}
|
||||
|
||||
func (ref Ref) String() string {
|
||||
if len(ref) == 0 {
|
||||
return ""
|
||||
@@ -1840,22 +1870,34 @@ func ObjectTerm(o ...[2]*Term) *Term {
|
||||
}
|
||||
|
||||
func LazyObject(blob map[string]interface{}) Object {
|
||||
return &lazyObj{native: blob}
|
||||
return &lazyObj{native: blob, cache: map[string]Value{}}
|
||||
}
|
||||
|
||||
type lazyObj struct {
|
||||
strict Object
|
||||
cache map[string]Value
|
||||
native map[string]interface{}
|
||||
}
|
||||
|
||||
func (l *lazyObj) force() Object {
|
||||
if l.strict == nil {
|
||||
l.strict = MustInterfaceToValue(l.native).(Object)
|
||||
// NOTE(jf): a possible performance improvement here would be to check how many
|
||||
// entries have been realized to AST in the cache, and if some threshold compared to the
|
||||
// total number of keys is exceeded, realize the remaining entries and set l.strict to l.cache.
|
||||
l.cache = map[string]Value{} // We don't need the cache anymore; drop it to free up memory.
|
||||
}
|
||||
return l.strict
|
||||
}
|
||||
|
||||
func (l *lazyObj) Compare(other Value) int {
|
||||
o1 := sortOrder(l)
|
||||
o2 := sortOrder(other)
|
||||
if o1 < o2 {
|
||||
return -1
|
||||
} else if o2 < o1 {
|
||||
return 1
|
||||
}
|
||||
return l.force().Compare(other)
|
||||
}
|
||||
|
||||
@@ -1924,13 +1966,20 @@ func (l *lazyObj) Get(k *Term) *Term {
|
||||
return l.strict.Get(k)
|
||||
}
|
||||
if s, ok := k.Value.(String); ok {
|
||||
if v, ok := l.cache[string(s)]; ok {
|
||||
return NewTerm(v)
|
||||
}
|
||||
|
||||
if val, ok := l.native[string(s)]; ok {
|
||||
var converted Value
|
||||
switch val := val.(type) {
|
||||
case map[string]interface{}:
|
||||
return NewTerm(&lazyObj{native: val})
|
||||
converted = LazyObject(val)
|
||||
default:
|
||||
return NewTerm(MustInterfaceToValue(val))
|
||||
converted = MustInterfaceToValue(val)
|
||||
}
|
||||
l.cache[string(s)] = converted
|
||||
return NewTerm(converted)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -1985,13 +2034,20 @@ func (l *lazyObj) Find(path Ref) (Value, error) {
|
||||
return l, nil
|
||||
}
|
||||
if p0, ok := path[0].Value.(String); ok {
|
||||
if v, ok := l.cache[string(p0)]; ok {
|
||||
return v.Find(path[1:])
|
||||
}
|
||||
|
||||
if v, ok := l.native[string(p0)]; ok {
|
||||
var converted Value
|
||||
switch v := v.(type) {
|
||||
case map[string]interface{}:
|
||||
return (&lazyObj{native: v}).Find(path[1:])
|
||||
converted = LazyObject(v)
|
||||
default:
|
||||
return MustInterfaceToValue(v).Find(path[1:])
|
||||
converted = MustInterfaceToValue(v)
|
||||
}
|
||||
l.cache[string(p0)] = converted
|
||||
return converted.Find(path[1:])
|
||||
}
|
||||
}
|
||||
return nil, errFindNotFound
|
||||
|
||||
+1429
File diff suppressed because it is too large
Load Diff
+27
-3
@@ -20,6 +20,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/format"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/internal/merge"
|
||||
@@ -391,12 +392,14 @@ type Reader struct {
|
||||
verificationConfig *VerificationConfig
|
||||
skipVerify bool
|
||||
processAnnotations bool
|
||||
jsonOptions *astJSON.Options
|
||||
capabilities *ast.Capabilities
|
||||
files map[string]FileInfo // files in the bundle signature payload
|
||||
sizeLimitBytes int64
|
||||
etag string
|
||||
lazyLoadingMode bool
|
||||
name string
|
||||
persist bool
|
||||
}
|
||||
|
||||
// NewReader is deprecated. Use NewCustomReader instead.
|
||||
@@ -460,6 +463,12 @@ func (r *Reader) WithCapabilities(caps *ast.Capabilities) *Reader {
|
||||
return r
|
||||
}
|
||||
|
||||
// WithJSONOptions sets the JSONOptions to use when parsing policy files
|
||||
func (r *Reader) WithJSONOptions(opts *astJSON.Options) *Reader {
|
||||
r.jsonOptions = opts
|
||||
return r
|
||||
}
|
||||
|
||||
// WithSizeLimitBytes sets the size limit to apply to files in the bundle. If files are larger
|
||||
// than this, an error will be returned by the reader.
|
||||
func (r *Reader) WithSizeLimitBytes(n int64) *Reader {
|
||||
@@ -488,10 +497,17 @@ func (r *Reader) WithLazyLoadingMode(yes bool) *Reader {
|
||||
return r
|
||||
}
|
||||
|
||||
// WithBundlePersistence specifies if the downloaded bundle will eventually be persisted to disk.
|
||||
func (r *Reader) WithBundlePersistence(persist bool) *Reader {
|
||||
r.persist = persist
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Reader) ParserOptions() ast.ParserOptions {
|
||||
return ast.ParserOptions{
|
||||
ProcessAnnotation: r.processAnnotations,
|
||||
Capabilities: r.capabilities,
|
||||
JSONOptions: r.jsonOptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,7 +611,7 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
var value interface{}
|
||||
|
||||
r.metrics.Timer(metrics.RegoDataParse).Start()
|
||||
err := util.NewJSONDecoder(&buf).Decode(&value)
|
||||
err := util.UnmarshalJSON(buf.Bytes(), &value)
|
||||
r.metrics.Timer(metrics.RegoDataParse).Stop()
|
||||
|
||||
if err != nil {
|
||||
@@ -645,6 +661,10 @@ func (r *Reader) Read() (Bundle, error) {
|
||||
if len(bundle.WasmModules) != 0 {
|
||||
return bundle, fmt.Errorf("delta bundle expected to contain only patch file but wasm files found")
|
||||
}
|
||||
|
||||
if r.persist {
|
||||
return bundle, fmt.Errorf("'persist' property is true in config. persisting delta bundle to disk is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// check if the bundle signatures specify any files that weren't found in the bundle
|
||||
@@ -1082,10 +1102,14 @@ func (b Bundle) Equal(other Bundle) bool {
|
||||
return false
|
||||
}
|
||||
for i := range b.Modules {
|
||||
if b.Modules[i].URL != other.Modules[i].URL {
|
||||
// To support bundles built from rootless filesystems we ignore a "/" prefix
|
||||
// for URLs and Paths, such that "/file" and "file" are equivalent
|
||||
if strings.TrimPrefix(b.Modules[i].URL, string(filepath.Separator)) !=
|
||||
strings.TrimPrefix(other.Modules[i].URL, string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
if b.Modules[i].Path != other.Modules[i].Path {
|
||||
if strings.TrimPrefix(b.Modules[i].Path, string(filepath.Separator)) !=
|
||||
strings.TrimPrefix(other.Modules[i].Path, string(filepath.Separator)) {
|
||||
return false
|
||||
}
|
||||
if !b.Modules[i].Parsed.Equal(other.Modules[i].Parsed) {
|
||||
|
||||
+80
-35
@@ -7,7 +7,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -108,6 +107,14 @@ func (d *Descriptor) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
type PathFormat int64
|
||||
|
||||
const (
|
||||
Chrooted PathFormat = iota
|
||||
SlashRooted
|
||||
Passthrough
|
||||
)
|
||||
|
||||
// DirectoryLoader defines an interface which can be used to load
|
||||
// files from a directory by iterating over each one in the tree.
|
||||
type DirectoryLoader interface {
|
||||
@@ -115,23 +122,22 @@ type DirectoryLoader interface {
|
||||
// descriptor should *always* be closed when no longer needed.
|
||||
NextFile() (*Descriptor, error)
|
||||
WithFilter(filter filter.LoaderFilter) DirectoryLoader
|
||||
WithPathFormat(PathFormat) DirectoryLoader
|
||||
}
|
||||
|
||||
type dirLoader struct {
|
||||
root string
|
||||
files []string
|
||||
idx int
|
||||
filter filter.LoaderFilter
|
||||
root string
|
||||
files []string
|
||||
idx int
|
||||
filter filter.LoaderFilter
|
||||
pathFormat PathFormat
|
||||
}
|
||||
|
||||
// NewDirectoryLoader returns a basic DirectoryLoader implementation
|
||||
// that will load files from a given root directory path.
|
||||
func NewDirectoryLoader(root string) DirectoryLoader {
|
||||
|
||||
// Normalize root directory, ex "./src/bundle" -> "src/bundle"
|
||||
// We don't need an absolute path, but this makes the joined/trimmed
|
||||
// paths more uniform.
|
||||
func normalizeRootDirectory(root string) string {
|
||||
if len(root) > 1 {
|
||||
// Normalize relative directories, ex "./src/bundle" -> "src/bundle"
|
||||
// We don't need an absolute path, but this makes the joined/trimmed
|
||||
// paths more uniform.
|
||||
if root[0] == '.' && root[1] == filepath.Separator {
|
||||
if len(root) == 2 {
|
||||
root = root[:1] // "./" -> "."
|
||||
@@ -140,9 +146,15 @@ func NewDirectoryLoader(root string) DirectoryLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
// NewDirectoryLoader returns a basic DirectoryLoader implementation
|
||||
// that will load files from a given root directory path.
|
||||
func NewDirectoryLoader(root string) DirectoryLoader {
|
||||
d := dirLoader{
|
||||
root: root,
|
||||
root: normalizeRootDirectory(root),
|
||||
pathFormat: Chrooted,
|
||||
}
|
||||
return &d
|
||||
}
|
||||
@@ -153,6 +165,36 @@ func (d *dirLoader) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
|
||||
return d
|
||||
}
|
||||
|
||||
// WithPathFormat specifies how a path is formatted in a Descriptor
|
||||
func (d *dirLoader) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
|
||||
d.pathFormat = pathFormat
|
||||
return d
|
||||
}
|
||||
|
||||
func formatPath(fileName string, root string, pathFormat PathFormat) string {
|
||||
switch pathFormat {
|
||||
case SlashRooted:
|
||||
if !strings.HasPrefix(fileName, string(filepath.Separator)) {
|
||||
return string(filepath.Separator) + fileName
|
||||
}
|
||||
return fileName
|
||||
case Chrooted:
|
||||
// Trim off the root directory and return path as if chrooted
|
||||
result := strings.TrimPrefix(fileName, filepath.FromSlash(root))
|
||||
if root == "." && filepath.Base(fileName) == ManifestExt {
|
||||
result = fileName
|
||||
}
|
||||
if !strings.HasPrefix(result, string(filepath.Separator)) {
|
||||
result = string(filepath.Separator) + result
|
||||
}
|
||||
return result
|
||||
case Passthrough:
|
||||
fallthrough
|
||||
default:
|
||||
return fileName
|
||||
}
|
||||
}
|
||||
|
||||
// NextFile iterates to the next file in the directory tree
|
||||
// and returns a file Descriptor for the file.
|
||||
func (d *dirLoader) NextFile() (*Descriptor, error) {
|
||||
@@ -187,28 +229,20 @@ func (d *dirLoader) NextFile() (*Descriptor, error) {
|
||||
d.idx++
|
||||
fh := newLazyFile(fileName)
|
||||
|
||||
// Trim off the root directory and return path as if chrooted
|
||||
cleanedPath := strings.TrimPrefix(fileName, filepath.FromSlash(d.root))
|
||||
if d.root == "." && filepath.Base(fileName) == ManifestExt {
|
||||
cleanedPath = fileName
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(cleanedPath, string(os.PathSeparator)) {
|
||||
cleanedPath = string(os.PathSeparator) + cleanedPath
|
||||
}
|
||||
|
||||
f := newDescriptor(path.Join(d.root, cleanedPath), cleanedPath, fh).withCloser(fh)
|
||||
cleanedPath := formatPath(fileName, d.root, d.pathFormat)
|
||||
f := newDescriptor(filepath.Join(d.root, cleanedPath), cleanedPath, fh).withCloser(fh)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
type tarballLoader struct {
|
||||
baseURL string
|
||||
r io.Reader
|
||||
tr *tar.Reader
|
||||
files []file
|
||||
idx int
|
||||
filter filter.LoaderFilter
|
||||
skipDir map[string]struct{}
|
||||
baseURL string
|
||||
r io.Reader
|
||||
tr *tar.Reader
|
||||
files []file
|
||||
idx int
|
||||
filter filter.LoaderFilter
|
||||
skipDir map[string]struct{}
|
||||
pathFormat PathFormat
|
||||
}
|
||||
|
||||
type file struct {
|
||||
@@ -221,7 +255,8 @@ type file struct {
|
||||
// NewTarballLoader is deprecated. Use NewTarballLoaderWithBaseURL instead.
|
||||
func NewTarballLoader(r io.Reader) DirectoryLoader {
|
||||
l := tarballLoader{
|
||||
r: r,
|
||||
r: r,
|
||||
pathFormat: Passthrough,
|
||||
}
|
||||
return &l
|
||||
}
|
||||
@@ -231,8 +266,9 @@ func NewTarballLoader(r io.Reader) DirectoryLoader {
|
||||
// with the baseURL.
|
||||
func NewTarballLoaderWithBaseURL(r io.Reader, baseURL string) DirectoryLoader {
|
||||
l := tarballLoader{
|
||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
r: r,
|
||||
baseURL: strings.TrimSuffix(baseURL, "/"),
|
||||
r: r,
|
||||
pathFormat: Passthrough,
|
||||
}
|
||||
return &l
|
||||
}
|
||||
@@ -243,6 +279,12 @@ func (t *tarballLoader) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
|
||||
return t
|
||||
}
|
||||
|
||||
// WithPathFormat specifies how a path is formatted in a Descriptor
|
||||
func (t *tarballLoader) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
|
||||
t.pathFormat = pathFormat
|
||||
return t
|
||||
}
|
||||
|
||||
// NextFile iterates to the next file in the directory tree
|
||||
// and returns a file Descriptor for the file.
|
||||
func (t *tarballLoader) NextFile() (*Descriptor, error) {
|
||||
@@ -329,7 +371,10 @@ func (t *tarballLoader) NextFile() (*Descriptor, error) {
|
||||
f := t.files[t.idx]
|
||||
t.idx++
|
||||
|
||||
return newDescriptor(path.Join(t.baseURL, f.name), f.name, f.reader), nil
|
||||
cleanedPath := formatPath(f.name, "", t.pathFormat)
|
||||
d := newDescriptor(filepath.Join(t.baseURL, cleanedPath), cleanedPath, f.reader)
|
||||
return d, nil
|
||||
|
||||
}
|
||||
|
||||
// Next implements the storage.Iterator interface.
|
||||
|
||||
+20
-4
@@ -23,16 +23,26 @@ type dirLoaderFS struct {
|
||||
files []string
|
||||
idx int
|
||||
filter filter.LoaderFilter
|
||||
root string
|
||||
pathFormat PathFormat
|
||||
}
|
||||
|
||||
// NewFSLoader returns a basic DirectoryLoader implementation
|
||||
// that will load files from a fs.FS interface
|
||||
func NewFSLoader(filesystem fs.FS) (DirectoryLoader, error) {
|
||||
return NewFSLoaderWithRoot(filesystem, defaultFSLoaderRoot), nil
|
||||
}
|
||||
|
||||
// NewFSLoaderWithRoot returns a basic DirectoryLoader implementation
|
||||
// that will load files from a fs.FS interface at the supplied root
|
||||
func NewFSLoaderWithRoot(filesystem fs.FS, root string) DirectoryLoader {
|
||||
d := dirLoaderFS{
|
||||
filesystem: filesystem,
|
||||
root: normalizeRootDirectory(root),
|
||||
pathFormat: Chrooted,
|
||||
}
|
||||
|
||||
return &d, nil
|
||||
return &d
|
||||
}
|
||||
|
||||
func (d *dirLoaderFS) walkDir(path string, dirEntry fs.DirEntry, err error) error {
|
||||
@@ -67,6 +77,12 @@ func (d *dirLoaderFS) WithFilter(filter filter.LoaderFilter) DirectoryLoader {
|
||||
return d
|
||||
}
|
||||
|
||||
// WithPathFormat specifies how a path is formatted in a Descriptor
|
||||
func (d *dirLoaderFS) WithPathFormat(pathFormat PathFormat) DirectoryLoader {
|
||||
d.pathFormat = pathFormat
|
||||
return d
|
||||
}
|
||||
|
||||
// NextFile iterates to the next file in the directory tree
|
||||
// and returns a file Descriptor for the file.
|
||||
func (d *dirLoaderFS) NextFile() (*Descriptor, error) {
|
||||
@@ -74,7 +90,7 @@ func (d *dirLoaderFS) NextFile() (*Descriptor, error) {
|
||||
defer d.Unlock()
|
||||
|
||||
if d.files == nil {
|
||||
err := fs.WalkDir(d.filesystem, defaultFSLoaderRoot, d.walkDir)
|
||||
err := fs.WalkDir(d.filesystem, d.root, d.walkDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list files: %w", err)
|
||||
}
|
||||
@@ -94,7 +110,7 @@ func (d *dirLoaderFS) NextFile() (*Descriptor, error) {
|
||||
return nil, fmt.Errorf("failed to open file %s: %w", fileName, err)
|
||||
}
|
||||
|
||||
fileNameWithSlash := fmt.Sprintf("/%s", fileName)
|
||||
f := newDescriptor(fileNameWithSlash, fileNameWithSlash, fh).withCloser(fh)
|
||||
cleanedPath := formatPath(fileName, d.root, d.pathFormat)
|
||||
f := newDescriptor(cleanedPath, cleanedPath, fh).withCloser(fh)
|
||||
return f, nil
|
||||
}
|
||||
|
||||
+17
-11
@@ -13,6 +13,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
iCompiler "github.com/open-policy-agent/opa/internal/compiler"
|
||||
"github.com/open-policy-agent/opa/internal/json/patch"
|
||||
"github.com/open-policy-agent/opa/metrics"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
@@ -291,14 +292,15 @@ func readEtagFromStore(ctx context.Context, store storage.Store, txn storage.Tra
|
||||
|
||||
// ActivateOpts defines options for the Activate API call.
|
||||
type ActivateOpts struct {
|
||||
Ctx context.Context
|
||||
Store storage.Store
|
||||
Txn storage.Transaction
|
||||
TxnCtx *storage.Context
|
||||
Compiler *ast.Compiler
|
||||
Metrics metrics.Metrics
|
||||
Bundles map[string]*Bundle // Optional
|
||||
ExtraModules map[string]*ast.Module // Optional
|
||||
Ctx context.Context
|
||||
Store storage.Store
|
||||
Txn storage.Transaction
|
||||
TxnCtx *storage.Context
|
||||
Compiler *ast.Compiler
|
||||
Metrics metrics.Metrics
|
||||
Bundles map[string]*Bundle // Optional
|
||||
ExtraModules map[string]*ast.Module // Optional
|
||||
AuthorizationDecisionRef ast.Ref
|
||||
|
||||
legacy bool
|
||||
}
|
||||
@@ -450,7 +452,7 @@ func activateBundles(opts *ActivateOpts) error {
|
||||
remainingAndExtra[name] = mod
|
||||
}
|
||||
|
||||
err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy)
|
||||
err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -755,7 +757,7 @@ func writeData(ctx context.Context, store storage.Store, txn storage.Transaction
|
||||
return nil
|
||||
}
|
||||
|
||||
func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool) error {
|
||||
func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref) error {
|
||||
|
||||
m.Timer(metrics.RegoModuleCompile).Start()
|
||||
defer m.Timer(metrics.RegoModuleCompile).Stop()
|
||||
@@ -789,7 +791,11 @@ func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[strin
|
||||
return compiler.Errors
|
||||
}
|
||||
|
||||
return nil
|
||||
if authorizationDecisionRef.Equal(ast.EmptyRef()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return iCompiler.VerifyAuthorizationPolicySchema(compiler, authorizationDecisionRef)
|
||||
}
|
||||
|
||||
func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool) error {
|
||||
|
||||
+4615
File diff suppressed because it is too large
Load Diff
+4640
File diff suppressed because it is too large
Load Diff
+4640
File diff suppressed because it is too large
Load Diff
+4640
File diff suppressed because it is too large
Load Diff
+4665
File diff suppressed because it is too large
Load Diff
+4688
File diff suppressed because it is too large
Load Diff
+4710
File diff suppressed because it is too large
Load Diff
+4710
File diff suppressed because it is too large
Load Diff
+4710
File diff suppressed because it is too large
Load Diff
+4737
File diff suppressed because it is too large
Load Diff
+258
@@ -0,0 +1,258 @@
|
||||
// Copyright 2018 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 config implements OPA configuration file parsing and validation.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/internal/ref"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
"github.com/open-policy-agent/opa/version"
|
||||
)
|
||||
|
||||
// Config represents the configuration file that OPA can be started with.
|
||||
type Config struct {
|
||||
Services json.RawMessage `json:"services,omitempty"`
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
Discovery json.RawMessage `json:"discovery,omitempty"`
|
||||
Bundle json.RawMessage `json:"bundle,omitempty"` // Deprecated: Use `bundles` instead
|
||||
Bundles json.RawMessage `json:"bundles,omitempty"`
|
||||
DecisionLogs json.RawMessage `json:"decision_logs,omitempty"`
|
||||
Status json.RawMessage `json:"status,omitempty"`
|
||||
Plugins map[string]json.RawMessage `json:"plugins,omitempty"`
|
||||
Keys json.RawMessage `json:"keys,omitempty"`
|
||||
DefaultDecision *string `json:"default_decision,omitempty"`
|
||||
DefaultAuthorizationDecision *string `json:"default_authorization_decision,omitempty"`
|
||||
Caching json.RawMessage `json:"caching,omitempty"`
|
||||
NDBuiltinCache bool `json:"nd_builtin_cache,omitempty"`
|
||||
PersistenceDirectory *string `json:"persistence_directory,omitempty"`
|
||||
DistributedTracing json.RawMessage `json:"distributed_tracing,omitempty"`
|
||||
Server *struct {
|
||||
Encoding json.RawMessage `json:"encoding,omitempty"`
|
||||
Metrics json.RawMessage `json:"metrics,omitempty"`
|
||||
} `json:"server,omitempty"`
|
||||
Storage *struct {
|
||||
Disk json.RawMessage `json:"disk,omitempty"`
|
||||
} `json:"storage,omitempty"`
|
||||
Extra map[string]json.RawMessage `json:"-"`
|
||||
}
|
||||
|
||||
// ParseConfig returns a valid Config object with defaults injected. The id
|
||||
// and version parameters will be set in the labels map.
|
||||
func ParseConfig(raw []byte, id string) (*Config, error) {
|
||||
// NOTE(sr): based on https://stackoverflow.com/a/33499066/993018
|
||||
var result Config
|
||||
objValue := reflect.ValueOf(&result).Elem()
|
||||
knownFields := map[string]reflect.Value{}
|
||||
for i := 0; i != objValue.NumField(); i++ {
|
||||
jsonName := strings.Split(objValue.Type().Field(i).Tag.Get("json"), ",")[0]
|
||||
knownFields[jsonName] = objValue.Field(i)
|
||||
}
|
||||
|
||||
if err := util.Unmarshal(raw, &result.Extra); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, chunk := range result.Extra {
|
||||
if field, found := knownFields[key]; found {
|
||||
if err := util.Unmarshal(chunk, field.Addr().Interface()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delete(result.Extra, key)
|
||||
}
|
||||
}
|
||||
if len(result.Extra) == 0 {
|
||||
result.Extra = nil
|
||||
}
|
||||
return &result, result.validateAndInjectDefaults(id)
|
||||
}
|
||||
|
||||
// PluginNames returns a sorted list of names of enabled plugins.
|
||||
func (c Config) PluginNames() (result []string) {
|
||||
if c.Bundle != nil || c.Bundles != nil {
|
||||
result = append(result, "bundles")
|
||||
}
|
||||
if c.Status != nil {
|
||||
result = append(result, "status")
|
||||
}
|
||||
if c.DecisionLogs != nil {
|
||||
result = append(result, "decision_logs")
|
||||
}
|
||||
for name := range c.Plugins {
|
||||
result = append(result, name)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// PluginsEnabled returns true if one or more plugin features are enabled.
|
||||
//
|
||||
// Deprecated. Use PluginNames instead.
|
||||
func (c Config) PluginsEnabled() bool {
|
||||
return c.Bundle != nil || c.Bundles != nil || c.DecisionLogs != nil || c.Status != nil || len(c.Plugins) > 0
|
||||
}
|
||||
|
||||
// DefaultDecisionRef returns the default decision as a reference.
|
||||
func (c Config) DefaultDecisionRef() ast.Ref {
|
||||
r, _ := ref.ParseDataPath(*c.DefaultDecision)
|
||||
return r
|
||||
}
|
||||
|
||||
// DefaultAuthorizationDecisionRef returns the default authorization decision
|
||||
// as a reference.
|
||||
func (c Config) DefaultAuthorizationDecisionRef() ast.Ref {
|
||||
r, _ := ref.ParseDataPath(*c.DefaultAuthorizationDecision)
|
||||
return r
|
||||
}
|
||||
|
||||
// NDBuiltinCacheEnabled returns if the ND builtins cache should be used.
|
||||
func (c Config) NDBuiltinCacheEnabled() bool {
|
||||
return c.NDBuiltinCache
|
||||
}
|
||||
|
||||
func (c *Config) validateAndInjectDefaults(id string) error {
|
||||
|
||||
if c.DefaultDecision == nil {
|
||||
s := defaultDecisionPath
|
||||
c.DefaultDecision = &s
|
||||
}
|
||||
|
||||
_, err := ref.ParseDataPath(*c.DefaultDecision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.DefaultAuthorizationDecision == nil {
|
||||
s := defaultAuthorizationDecisionPath
|
||||
c.DefaultAuthorizationDecision = &s
|
||||
}
|
||||
|
||||
_, err = ref.ParseDataPath(*c.DefaultAuthorizationDecision)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.Labels == nil {
|
||||
c.Labels = map[string]string{}
|
||||
}
|
||||
|
||||
c.Labels["id"] = id
|
||||
c.Labels["version"] = version.Version
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPersistenceDirectory returns the configured persistence directory, or $PWD/.opa if none is configured
|
||||
func (c Config) GetPersistenceDirectory() (string, error) {
|
||||
if c.PersistenceDirectory == nil {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(pwd, ".opa"), nil
|
||||
}
|
||||
return *c.PersistenceDirectory, nil
|
||||
}
|
||||
|
||||
// ActiveConfig returns OPA's active configuration
|
||||
// with the credentials and crypto keys removed
|
||||
func (c *Config) ActiveConfig() (interface{}, error) {
|
||||
bs, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := util.UnmarshalJSON(bs, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, e := range c.Extra {
|
||||
var v any
|
||||
if err := util.UnmarshalJSON(e, &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
if err := removeServiceCredentials(result["services"]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := removeCryptoKeys(result["keys"]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func removeServiceCredentials(x interface{}) error {
|
||||
switch x := x.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case []interface{}:
|
||||
for _, v := range x {
|
||||
err := removeKey(v, "credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case map[string]interface{}:
|
||||
for _, v := range x {
|
||||
err := removeKey(v, "credentials")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("illegal service config type: %T", x)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeCryptoKeys(x interface{}) error {
|
||||
switch x := x.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case map[string]interface{}:
|
||||
for _, v := range x {
|
||||
err := removeKey(v, "key", "private_key")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("illegal keys config type: %T", x)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeKey(x interface{}, keys ...string) error {
|
||||
val, ok := x.(map[string]interface{})
|
||||
if !ok {
|
||||
return fmt.Errorf("type assertion error")
|
||||
}
|
||||
|
||||
for _, key := range keys {
|
||||
delete(val, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
defaultDecisionPath = "/system/main"
|
||||
defaultAuthorizationDecisionPath = "/system/authz/allow"
|
||||
)
|
||||
+117
-28
@@ -11,6 +11,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/internal/future"
|
||||
@@ -24,6 +25,8 @@ type Opts struct {
|
||||
// of partial evaluation, arguments maybe have been shuffled around, but still
|
||||
// carry along their original source locations.
|
||||
IgnoreLocations bool
|
||||
|
||||
RegoV1 bool
|
||||
}
|
||||
|
||||
// defaultLocationFile is the file name used in `Ast()` for terms
|
||||
@@ -35,15 +38,27 @@ const defaultLocationFile = "__format_default__"
|
||||
// Rego module. If they don't, Source will return an error resulting from the attempt
|
||||
// to parse the bytes.
|
||||
func Source(filename string, src []byte) ([]byte, error) {
|
||||
return SourceWithOpts(filename, src, Opts{})
|
||||
}
|
||||
|
||||
func SourceWithOpts(filename string, src []byte, opts Opts) ([]byte, error) {
|
||||
module, err := ast.ParseModule(filename, string(src))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
formatted, err := Ast(module)
|
||||
if opts.RegoV1 {
|
||||
errors := ast.CheckRegoV1(module)
|
||||
if len(errors) > 0 {
|
||||
return nil, errors
|
||||
}
|
||||
}
|
||||
|
||||
formatted, err := AstWithOpts(module, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %v", filename, err)
|
||||
}
|
||||
|
||||
return formatted, nil
|
||||
}
|
||||
|
||||
@@ -80,6 +95,8 @@ type fmtOpts struct {
|
||||
// for ref heads -- if they do, we'll print all of them in a different way
|
||||
// than if they don't.
|
||||
refHeads bool
|
||||
|
||||
regoV1 bool
|
||||
}
|
||||
|
||||
func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
@@ -98,6 +115,12 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
|
||||
o := fmtOpts{}
|
||||
|
||||
if opts.RegoV1 {
|
||||
o.regoV1 = true
|
||||
o.ifs = true
|
||||
o.contains = true
|
||||
}
|
||||
|
||||
// Preprocess the AST. Set any required defaults and calculate
|
||||
// values required for printing the formatted output.
|
||||
ast.WalkNodes(x, func(x ast.Node) bool {
|
||||
@@ -119,6 +142,9 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
|
||||
case *ast.Import:
|
||||
switch {
|
||||
case isRegoV1Compatible(n):
|
||||
o.contains = true
|
||||
o.ifs = true
|
||||
case future.IsAllFutureKeywords(n):
|
||||
o.contains = true
|
||||
o.ifs = true
|
||||
@@ -150,8 +176,15 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
|
||||
switch x := x.(type) {
|
||||
case *ast.Module:
|
||||
for kw := range extraFutureKeywordImports {
|
||||
x.Imports = ensureFutureKeywordImport(x.Imports, kw)
|
||||
if o.regoV1 {
|
||||
x.Imports = ensureRegoV1Import(x.Imports)
|
||||
}
|
||||
if o.regoV1 || moduleIsRegoV1Compatible(x) {
|
||||
x.Imports = future.FilterFutureImports(x.Imports)
|
||||
} else {
|
||||
for kw := range extraFutureKeywordImports {
|
||||
x.Imports = ensureFutureKeywordImport(x.Imports, kw)
|
||||
}
|
||||
}
|
||||
w.writeModule(x, o)
|
||||
case *ast.Package:
|
||||
@@ -263,13 +296,12 @@ func (w *writer) writeModule(module *ast.Module, o fmtOpts) {
|
||||
return locLess(comments[i], comments[j])
|
||||
})
|
||||
|
||||
// XXX: The parser currently duplicates comments for some reason, so we need
|
||||
// to remove duplicates here.
|
||||
comments = dedupComments(comments)
|
||||
sort.Slice(others, func(i, j int) bool {
|
||||
return locLess(others[i], others[j])
|
||||
})
|
||||
|
||||
comments = trimTrailingWhitespaceInComments(comments)
|
||||
|
||||
comments = w.writePackage(pkg, comments)
|
||||
var imports []*ast.Import
|
||||
var rules []*ast.Rule
|
||||
@@ -288,6 +320,14 @@ func (w *writer) writeModule(module *ast.Module, o fmtOpts) {
|
||||
}
|
||||
}
|
||||
|
||||
func trimTrailingWhitespaceInComments(comments []*ast.Comment) []*ast.Comment {
|
||||
for _, c := range comments {
|
||||
c.Text = bytes.TrimRightFunc(c.Text, unicode.IsSpace)
|
||||
}
|
||||
|
||||
return comments
|
||||
}
|
||||
|
||||
func (w *writer) writePackage(pkg *ast.Package, comments []*ast.Comment) []*ast.Comment {
|
||||
comments = w.insertComments(comments, pkg.Location)
|
||||
|
||||
@@ -345,7 +385,7 @@ func (w *writer) writeRule(rule *ast.Rule, isElse bool, o fmtOpts, comments []*a
|
||||
return comments
|
||||
}
|
||||
|
||||
if o.ifs && partialSetException {
|
||||
if (o.regoV1 || o.ifs) && partialSetException {
|
||||
w.write(" if")
|
||||
if len(rule.Body) == 1 {
|
||||
if rule.Body[0].Location.Row == rule.Head.Location.Row {
|
||||
@@ -453,7 +493,7 @@ func (w *writer) writeElse(rule *ast.Rule, o fmtOpts, comments []*ast.Comment) [
|
||||
|
||||
func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst bool, o fmtOpts, comments []*ast.Comment) []*ast.Comment {
|
||||
ref := head.Ref()
|
||||
if head.Key != nil && head.Value == nil {
|
||||
if head.Key != nil && head.Value == nil && !head.HasDynamicRef() {
|
||||
ref = ref.GroundPrefix()
|
||||
}
|
||||
if o.refHeads || len(ref) == 1 {
|
||||
@@ -484,8 +524,26 @@ func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst bool, o fm
|
||||
w.write("]")
|
||||
}
|
||||
}
|
||||
if head.Value != nil && (head.Key != nil || ast.Compare(head.Value, ast.BooleanTerm(true)) != 0 || isExpandedConst || isDefault) {
|
||||
if head.Assign {
|
||||
|
||||
if head.Value != nil &&
|
||||
(head.Key != nil || ast.Compare(head.Value, ast.BooleanTerm(true)) != 0 || isExpandedConst || isDefault) {
|
||||
|
||||
// in rego v1, explicitly print value for ref-head constants that aren't partial set assignments, e.g.:
|
||||
// * a -> parser error, won't reach here
|
||||
// * a.b -> a contains "b"
|
||||
// * a.b.c -> a.b.c := true
|
||||
// * a.b.c.d -> a.b.c.d := true
|
||||
isRegoV1RefConst := o.regoV1 && isExpandedConst && head.Key == nil && len(head.Args) == 0
|
||||
|
||||
if head.Location == head.Value.Location && head.Name != "else" && !isRegoV1RefConst {
|
||||
// If the value location is the same as the location of the head,
|
||||
// we know that the value is generated, i.e. f(1)
|
||||
// Don't print the value (` = true`) as it is implied.
|
||||
return comments
|
||||
}
|
||||
|
||||
if head.Assign || o.regoV1 {
|
||||
// preserve assignment operator, and enforce it if formatting for Rego v1
|
||||
w.write(" := ")
|
||||
} else {
|
||||
w.write(" = ")
|
||||
@@ -732,7 +790,12 @@ func (w *writer) writeTermParens(parens bool, term *ast.Term, comments []*ast.Co
|
||||
|
||||
func (w *writer) writeRef(x ast.Ref) {
|
||||
if len(x) > 0 {
|
||||
w.writeTerm(x[0], nil)
|
||||
parens := false
|
||||
_, ok := x[0].Value.(ast.Call)
|
||||
if ok {
|
||||
parens = x[0].Location.Text[0] == 40 // Starts with "("
|
||||
}
|
||||
w.writeTermParens(parens, x[0], nil)
|
||||
path := x[1:]
|
||||
for _, t := range path {
|
||||
switch p := t.Value.(type) {
|
||||
@@ -807,6 +870,7 @@ func (w *writer) writeCall(parens bool, x ast.Call, loc *ast.Location, comments
|
||||
}
|
||||
|
||||
func (w *writer) writeInOperator(parens bool, operands []*ast.Term, comments []*ast.Comment, loc *ast.Location, f *types.Function) []*ast.Comment {
|
||||
|
||||
if len(operands) != len(f.Args()) {
|
||||
// The number of operands does not math the arity of the `in` operator
|
||||
operator := ast.Member.Name
|
||||
@@ -909,12 +973,17 @@ func (w *writer) writeObjectComprehension(object *ast.ObjectComprehension, loc *
|
||||
}
|
||||
|
||||
func (w *writer) writeComprehension(open, close byte, term *ast.Term, body ast.Body, loc *ast.Location, comments []*ast.Comment) []*ast.Comment {
|
||||
if term.Location.Row-loc.Row > 1 {
|
||||
if term.Location.Row-loc.Row >= 1 {
|
||||
w.endLine()
|
||||
w.startLine()
|
||||
}
|
||||
|
||||
comments = w.writeTerm(term, comments)
|
||||
parens := false
|
||||
_, ok := term.Value.(ast.Call)
|
||||
if ok {
|
||||
parens = term.Location.Text[0] == 40 // Starts with "("
|
||||
}
|
||||
comments = w.writeTermParens(parens, term, comments)
|
||||
w.write(" |")
|
||||
|
||||
return w.writeComprehensionBody(open, close, body, term.Location, loc, comments)
|
||||
@@ -1280,21 +1349,6 @@ func skipPast(open, close byte, loc *ast.Location) (int, int) {
|
||||
return i, offset
|
||||
}
|
||||
|
||||
func dedupComments(comments []*ast.Comment) []*ast.Comment {
|
||||
if len(comments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := []*ast.Comment{comments[0]}
|
||||
for i := 1; i < len(comments); i++ {
|
||||
if comments[i].Location.Equal(comments[i-1].Location) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, comments[i])
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
// startLine begins a line with the current indentation level.
|
||||
func (w *writer) startLine() {
|
||||
w.inline = true
|
||||
@@ -1386,6 +1440,24 @@ func ensureFutureKeywordImport(imps []*ast.Import, kw string) []*ast.Import {
|
||||
return append(imps, imp)
|
||||
}
|
||||
|
||||
func ensureRegoV1Import(imps []*ast.Import) []*ast.Import {
|
||||
return ensureImport(imps, ast.RegoV1CompatibleRef)
|
||||
}
|
||||
|
||||
func ensureImport(imps []*ast.Import, path ast.Ref) []*ast.Import {
|
||||
for _, imp := range imps {
|
||||
p := imp.Path.Value.(ast.Ref)
|
||||
if p.Equal(path) {
|
||||
return imps
|
||||
}
|
||||
}
|
||||
imp := &ast.Import{
|
||||
Path: ast.NewTerm(path),
|
||||
}
|
||||
imp.Location = defaultLocation(imp)
|
||||
return append(imps, imp)
|
||||
}
|
||||
|
||||
// ArgErrDetail but for `fmt` checks since compiler has not run yet.
|
||||
type ArityFormatErrDetail struct {
|
||||
Have []string `json:"have"`
|
||||
@@ -1418,3 +1490,20 @@ func (d *ArityFormatErrDetail) Lines() []string {
|
||||
"want: " + "(" + strings.Join(d.Want, ",") + ")",
|
||||
}
|
||||
}
|
||||
|
||||
func moduleIsRegoV1Compatible(m *ast.Module) bool {
|
||||
for _, imp := range m.Imports {
|
||||
if isRegoV1Compatible(imp) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isRegoV1Compatible returns true if the passed *ast.Import is `rego.v1`
|
||||
func isRegoV1Compatible(imp *ast.Import) bool {
|
||||
path := imp.Path.Value.(ast.Ref)
|
||||
return len(path) == 2 &&
|
||||
ast.RegoRootDocument.Equal(path[0]) &&
|
||||
path[1].Equal(ast.StringTerm("v1"))
|
||||
}
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2023 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 hooks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/config"
|
||||
)
|
||||
|
||||
// Hook is a hook to be called in some select places in OPA's operation.
|
||||
//
|
||||
// The base Hook interface is any, and wherever a hook can occur, the calling code
|
||||
// will check if your hook implements an appropriate interface. If so, your hook
|
||||
// is called.
|
||||
//
|
||||
// This allows you to only hook in to behavior you care about, and it allows the
|
||||
// OPA to add more hooks in the future.
|
||||
//
|
||||
// All hook interfaces in this package have Hook in the name. Hooks must be safe
|
||||
// for concurrent use. It is expected that hooks are fast; if a hook needs to take
|
||||
// time, then copy what you need and ensure the hook is async.
|
||||
//
|
||||
// When multiple instances of a hook are provided, they are all going to be executed
|
||||
// in an unspecified order (it's a map-range call underneath). If you need hooks to
|
||||
// be run in order, you can wrap them into another hook, and configure that one.
|
||||
type Hook any
|
||||
|
||||
// Hooks is the type used for every struct in OPA that can work with hooks.
|
||||
type Hooks struct {
|
||||
m map[Hook]struct{} // we are NOT providing a stable invocation ordering
|
||||
}
|
||||
|
||||
// New creates a new instance of Hooks.
|
||||
func New(hs ...Hook) Hooks {
|
||||
h := Hooks{m: make(map[Hook]struct{}, len(hs))}
|
||||
for i := range hs {
|
||||
h.m[hs[i]] = struct{}{}
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (hs Hooks) Each(fn func(Hook)) {
|
||||
for h := range hs.m {
|
||||
fn(h)
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigHook allows inspecting or rewriting the configuration when the plugin
|
||||
// manager is processing it.
|
||||
// Note that this hook is not run when the plugin manager is reconfigured. This
|
||||
// usually only happens when there's a new config from a discovery bundle, and
|
||||
// for processing _that_, there's `ConfigDiscoveryHook`.
|
||||
type ConfigHook interface {
|
||||
OnConfig(context.Context, *config.Config) (*config.Config, error)
|
||||
}
|
||||
|
||||
// ConfigHook allows inspecting or rewriting the discovered configuration when
|
||||
// the discovery plugin is processing it.
|
||||
type ConfigDiscoveryHook interface {
|
||||
OnConfigDiscovery(context.Context, *config.Config) (*config.Config, error)
|
||||
}
|
||||
|
||||
func (hs Hooks) Validate() error {
|
||||
for h := range hs.m {
|
||||
switch h.(type) {
|
||||
case ConfigHook,
|
||||
ConfigDiscoveryHook: // OK
|
||||
default:
|
||||
return fmt.Errorf("unknown hook type %T", h)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// Copyright 2023 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 compiler
|
||||
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/schemas"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
type SchemaFile string
|
||||
|
||||
const (
|
||||
AuthorizationPolicySchema SchemaFile = "authorizationPolicy.json"
|
||||
)
|
||||
|
||||
var schemaDefinitions = map[SchemaFile]interface{}{}
|
||||
|
||||
// VerifyAuthorizationPolicySchema performs type checking on rules against the schema for the Authorization Policy
|
||||
// Input document.
|
||||
// NOTE: The provided compiler should have already run the compilation process on the input modules
|
||||
func VerifyAuthorizationPolicySchema(compiler *ast.Compiler, ref ast.Ref) error {
|
||||
|
||||
rules := getRulesWithDependencies(compiler, ref)
|
||||
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
schemaSet := ast.NewSchemaSet()
|
||||
schemaSet.Put(ast.SchemaRootRef, schemaDefinitions[AuthorizationPolicySchema])
|
||||
|
||||
errs := ast.NewCompiler().WithSchemas(schemaSet).PassesTypeCheckRules(rules)
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRulesWithDependencies returns a slice of rules that are referred to by ref along with their dependencies
|
||||
func getRulesWithDependencies(compiler *ast.Compiler, ref ast.Ref) []*ast.Rule {
|
||||
allRules := compiler.GetRules(ref)
|
||||
|
||||
deps := map[*ast.Rule]struct{}{}
|
||||
for _, rule := range allRules {
|
||||
transitiveDependencies(compiler, rule, deps)
|
||||
}
|
||||
|
||||
for dep := range deps {
|
||||
allRules = append(allRules, dep)
|
||||
}
|
||||
|
||||
return allRules
|
||||
}
|
||||
|
||||
func transitiveDependencies(compiler *ast.Compiler, rule *ast.Rule, deps map[*ast.Rule]struct{}) {
|
||||
for x := range compiler.Graph.Dependencies(rule) {
|
||||
other := x.(*ast.Rule)
|
||||
deps[other] = struct{}{}
|
||||
transitiveDependencies(compiler, other, deps)
|
||||
}
|
||||
}
|
||||
|
||||
func loadAuthorizationPolicySchema() {
|
||||
|
||||
cont, err := schemas.FS.ReadFile(string(AuthorizationPolicySchema))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(cont) == 0 {
|
||||
panic("expected authorization policy schema file to be present")
|
||||
}
|
||||
|
||||
var schema interface{}
|
||||
if err := util.Unmarshal(cont, &schema); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
schemaDefinitions[AuthorizationPolicySchema] = schema
|
||||
}
|
||||
|
||||
func init() {
|
||||
loadAuthorizationPolicySchema()
|
||||
}
|
||||
+56
-25
@@ -344,12 +344,18 @@ opa_json_writer_write,opa_json_writer_emit_value
|
||||
opa_json_writer_write,opa_free
|
||||
opa_json_dump,opa_json_writer_write
|
||||
opa_value_dump,opa_json_writer_write
|
||||
move_freelists,opa_abort
|
||||
opa_heap_blocks_stash,move_freelists
|
||||
opa_heap_blocks_restore,move_freelists
|
||||
opa_malloc,opa_free_bulk_commit
|
||||
opa_malloc,opa_abort
|
||||
opa_free_bulk_commit,merge_sort_blocks
|
||||
opa_realloc,opa_malloc
|
||||
opa_realloc,memcpy
|
||||
opa_realloc,opa_free
|
||||
opa_builtin_cache_get,opa_abort
|
||||
opa_builtin_cache_set,opa_abort
|
||||
merge_sort_blocks,merge_sort_blocks
|
||||
opa_memoize_init,opa_malloc
|
||||
opa_memoize_init,opa_object
|
||||
opa_memoize_push,opa_malloc
|
||||
@@ -569,14 +575,14 @@ opa_sets_intersection,opa_value_type
|
||||
opa_sets_intersection,opa_set
|
||||
opa_sets_intersection,opa_set_union
|
||||
opa_sets_intersection,opa_set_intersection
|
||||
opa_sets_intersection,opa_value_free
|
||||
opa_sets_intersection,opa_value_free_shallow
|
||||
opa_set_union,opa_value_type
|
||||
opa_set_union,opa_set
|
||||
opa_set_union,opa_set_add
|
||||
opa_sets_union,opa_value_type
|
||||
opa_sets_union,opa_set
|
||||
opa_sets_union,opa_set_add
|
||||
opa_sets_union,opa_value_free
|
||||
opa_sets_union,opa_value_free_shallow
|
||||
opa_strings_any_prefix_match,opa_value_type
|
||||
opa_strings_any_prefix_match,opa_value_iter
|
||||
opa_strings_any_prefix_match,opa_value_get
|
||||
@@ -785,10 +791,31 @@ opa_object_keys,opa_strncmp
|
||||
opa_object_keys,opa_value_compare_object
|
||||
opa_object_keys,opa_abort
|
||||
opa_object_keys,opa_value_compare_set
|
||||
opa_value_free,opa_free
|
||||
opa_array_free,__opa_value_free
|
||||
opa_array_free,opa_free
|
||||
opa_array_free,opa_free_bulk
|
||||
__opa_value_free,opa_free_bulk
|
||||
__opa_value_free,opa_free
|
||||
__opa_value_free,opa_array_free
|
||||
__opa_value_free,__opa_object_buckets_free
|
||||
__opa_value_free,__opa_set_buckets_free
|
||||
__opa_object_buckets_free,opa_free
|
||||
__opa_object_buckets_free,opa_array_free
|
||||
__opa_object_buckets_free,__opa_object_buckets_free
|
||||
__opa_object_buckets_free,__opa_set_buckets_free
|
||||
__opa_object_buckets_free,__opa_value_free
|
||||
__opa_object_buckets_free,opa_free_bulk
|
||||
__opa_set_buckets_free,opa_free
|
||||
__opa_set_buckets_free,opa_array_free
|
||||
__opa_set_buckets_free,__opa_object_buckets_free
|
||||
__opa_set_buckets_free,__opa_set_buckets_free
|
||||
__opa_set_buckets_free,__opa_value_free
|
||||
__opa_set_buckets_free,opa_free_bulk
|
||||
opa_value_free,__opa_value_free
|
||||
opa_value_free_shallow,__opa_value_free
|
||||
opa_value_merge,opa_malloc
|
||||
opa_value_merge,opa_value_get
|
||||
opa_value_merge,opa_object_insert
|
||||
opa_value_merge,__opa_object_insert
|
||||
opa_value_merge,opa_value_merge
|
||||
opa_value_merge,opa_abort
|
||||
opa_value_merge,opa_atoi64
|
||||
@@ -798,19 +825,12 @@ opa_value_merge,opa_strncmp
|
||||
opa_value_merge,opa_value_compare
|
||||
opa_value_merge,opa_value_compare_object
|
||||
opa_value_merge,opa_value_compare_set
|
||||
opa_object_insert,opa_value_hash
|
||||
opa_object_insert,opa_value_compare
|
||||
opa_object_insert,__opa_object_grow
|
||||
opa_object_insert,opa_malloc
|
||||
__opa_object_grow,opa_malloc
|
||||
__opa_object_grow,opa_value_hash
|
||||
__opa_object_grow,opa_value_compare_number
|
||||
__opa_object_grow,opa_strncmp
|
||||
__opa_object_grow,opa_value_compare
|
||||
__opa_object_grow,opa_value_compare_object
|
||||
__opa_object_grow,opa_value_compare_set
|
||||
__opa_object_grow,opa_abort
|
||||
__opa_object_grow,opa_free
|
||||
__opa_object_insert,opa_value_hash
|
||||
__opa_object_insert,opa_value_compare
|
||||
__opa_object_insert,__opa_value_free
|
||||
__opa_object_insert,__opa_object_grow
|
||||
__opa_object_insert,opa_malloc
|
||||
opa_object_insert,__opa_object_insert
|
||||
opa_boolean,opa_malloc
|
||||
opa_number_ref,opa_malloc
|
||||
opa_number_int,opa_malloc
|
||||
@@ -818,7 +838,7 @@ opa_string,opa_malloc
|
||||
opa_value_shallow_copy_object,opa_malloc
|
||||
opa_value_shallow_copy_object,opa_value_iter
|
||||
opa_value_shallow_copy_object,opa_value_get
|
||||
opa_value_shallow_copy_object,opa_object_insert
|
||||
opa_value_shallow_copy_object,__opa_object_insert
|
||||
opa_value_shallow_copy_set,opa_malloc
|
||||
opa_value_shallow_copy_set,opa_value_iter
|
||||
opa_value_shallow_copy_set,opa_set_add
|
||||
@@ -861,15 +881,27 @@ opa_array_with_cap,opa_free
|
||||
opa_object,opa_malloc
|
||||
opa_set,opa_malloc
|
||||
opa_set_with_cap,opa_malloc
|
||||
__opa_object_grow,opa_malloc
|
||||
__opa_object_grow,opa_value_hash
|
||||
__opa_object_grow,opa_value_compare_number
|
||||
__opa_object_grow,opa_strncmp
|
||||
__opa_object_grow,opa_value_compare
|
||||
__opa_object_grow,opa_value_compare_object
|
||||
__opa_object_grow,opa_value_compare_set
|
||||
__opa_object_grow,opa_abort
|
||||
__opa_object_grow,opa_free
|
||||
opa_object_remove,opa_value_hash
|
||||
opa_object_remove,opa_value_compare
|
||||
opa_object_remove,__opa_value_free
|
||||
opa_object_remove,opa_free_bulk
|
||||
opa_object_remove,opa_free
|
||||
opa_string_copy,opa_malloc
|
||||
opa_value_add_path,opa_value_get
|
||||
opa_value_add_path,opa_malloc
|
||||
opa_value_add_path,opa_object_insert
|
||||
opa_value_add_path,opa_value_free
|
||||
opa_value_add_path,__opa_object_insert
|
||||
opa_value_add_path,__opa_value_free
|
||||
opa_value_remove_path,opa_value_get
|
||||
opa_value_remove_path,opa_value_hash
|
||||
opa_value_remove_path,opa_value_compare
|
||||
opa_value_remove_path,opa_value_free
|
||||
opa_value_remove_path,opa_free
|
||||
opa_value_remove_path,opa_object_remove
|
||||
opa_lookup,opa_value_get
|
||||
opa_lookup,opa_value_iter
|
||||
opa_lookup,opa_atoi64
|
||||
@@ -1036,7 +1068,6 @@ opa_regex_find_all_string_submatch,memcpy
|
||||
opa_regex_find_all_string_submatch,compile\28char\20const*\29
|
||||
opa_regex_find_all_string_submatch,opa_array
|
||||
opa_regex_find_all_string_submatch,memset
|
||||
opa_regex_find_all_string_submatch,strlen
|
||||
opa_regex_find_all_string_submatch,re2::RE2::Match\28re2::StringPiece\20const&\2c\20unsigned\20long\2c\20unsigned\20long\2c\20re2::RE2::Anchor\2c\20re2::StringPiece*\2c\20int\29\20const
|
||||
opa_regex_find_all_string_submatch,fullrune
|
||||
opa_regex_find_all_string_submatch,chartorune
|
||||
|
||||
|
BIN
Binary file not shown.
+5
-21
@@ -28,7 +28,7 @@ import (
|
||||
const (
|
||||
opaWasmABIVersionVal = 1
|
||||
opaWasmABIVersionVar = "opa_wasm_abi_version"
|
||||
opaWasmABIMinorVersionVal = 2
|
||||
opaWasmABIMinorVersionVal = 3
|
||||
opaWasmABIMinorVersionVar = "opa_wasm_abi_minor_version"
|
||||
)
|
||||
|
||||
@@ -1085,27 +1085,11 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
|
||||
instrs = append(instrs, instruction.Call{Index: c.function(opaNumberSize)})
|
||||
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
|
||||
case *ir.EqualStmt:
|
||||
if stmt.A != stmt.B { // constants, or locals, being equal here can skip the check
|
||||
instrs = append(instrs, c.instrRead(stmt.A))
|
||||
instrs = append(instrs, c.instrRead(stmt.B))
|
||||
instrs = append(instrs, instruction.Call{Index: c.function(opaValueCompare)})
|
||||
instrs = append(instrs, instruction.BrIf{Index: 0})
|
||||
}
|
||||
instrs = append(instrs, c.instrRead(stmt.A))
|
||||
instrs = append(instrs, c.instrRead(stmt.B))
|
||||
instrs = append(instrs, instruction.Call{Index: c.function(opaValueCompare)})
|
||||
instrs = append(instrs, instruction.BrIf{Index: 0})
|
||||
case *ir.NotEqualStmt:
|
||||
if stmt.A == stmt.B { // same local, same bool constant, or same string constant
|
||||
instrs = append(instrs, instruction.Br{Index: 0})
|
||||
continue
|
||||
}
|
||||
_, okA := stmt.A.Value.(ir.Bool)
|
||||
if _, okB := stmt.B.Value.(ir.Bool); okA && okB {
|
||||
// not equal (checked above), but both booleans => not equal
|
||||
continue
|
||||
}
|
||||
_, okA = stmt.A.Value.(ir.StringIndex)
|
||||
if _, okB := stmt.B.Value.(ir.StringIndex); okA && okB {
|
||||
// not equal (checked above), but both strings => not equal
|
||||
continue
|
||||
}
|
||||
instrs = append(instrs, c.instrRead(stmt.A))
|
||||
instrs = append(instrs, c.instrRead(stmt.B))
|
||||
instrs = append(instrs, instruction.Call{Index: c.function(opaValueCompare)})
|
||||
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
// 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 config implements helper functions to parse OPA's configuration.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/strvals"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
"github.com/open-policy-agent/opa/plugins/rest"
|
||||
"github.com/open-policy-agent/opa/tracing"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
// ServiceOptions stores the options passed to ParseServicesConfig
|
||||
type ServiceOptions struct {
|
||||
Raw json.RawMessage
|
||||
AuthPlugin rest.AuthPluginLookupFunc
|
||||
Keys map[string]*keys.Config
|
||||
Logger logging.Logger
|
||||
DistributedTacingOpts tracing.Options
|
||||
}
|
||||
|
||||
// ParseServicesConfig returns a set of named service clients. The service
|
||||
// clients can be specified either as an array or as a map. Some systems (e.g.,
|
||||
// Helm) do not have proper support for configuration values nested under
|
||||
// arrays, so just support both here.
|
||||
func ParseServicesConfig(opts ServiceOptions) (map[string]rest.Client, error) {
|
||||
|
||||
services := map[string]rest.Client{}
|
||||
|
||||
var arr []json.RawMessage
|
||||
var obj map[string]json.RawMessage
|
||||
|
||||
if err := util.Unmarshal(opts.Raw, &arr); err == nil {
|
||||
for _, s := range arr {
|
||||
client, err := rest.New(s, opts.Keys, rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
services[client.Service()] = client
|
||||
}
|
||||
} else if util.Unmarshal(opts.Raw, &obj) == nil {
|
||||
for k := range obj {
|
||||
client, err := rest.New(obj[k], opts.Keys, rest.Name(k), rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
services[client.Service()] = client
|
||||
}
|
||||
} else {
|
||||
// Return error from array decode as that is the default format.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return services, nil
|
||||
}
|
||||
|
||||
// Load implements configuration file loading. The supplied config file will be
|
||||
// read from disk (if specified) and overrides will be applied. If no config file is
|
||||
// specified, the overrides can still be applied to an empty config.
|
||||
func Load(configFile string, overrides []string, overrideFiles []string) ([]byte, error) {
|
||||
baseConf := map[string]interface{}{}
|
||||
|
||||
// User specified config file
|
||||
if configFile != "" {
|
||||
var bytes []byte
|
||||
var err error
|
||||
bytes, err = os.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
processedConf := subEnvVars(string(bytes))
|
||||
|
||||
if err := yaml.Unmarshal([]byte(processedConf), &baseConf); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %s: %s", configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
overrideConf := map[string]interface{}{}
|
||||
|
||||
// User specified a config override via --set
|
||||
for _, override := range overrides {
|
||||
processedOverride := subEnvVars(override)
|
||||
if err := strvals.ParseInto(processedOverride, overrideConf); err != nil {
|
||||
return nil, fmt.Errorf("failed parsing --set data: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// User specified a config override value via --set-file
|
||||
for _, override := range overrideFiles {
|
||||
reader := func(rs []rune) (interface{}, error) {
|
||||
bytes, err := os.ReadFile(string(rs))
|
||||
value := strings.TrimSpace(string(bytes))
|
||||
return value, err
|
||||
}
|
||||
if err := strvals.ParseIntoFile(override, overrideConf, reader); err != nil {
|
||||
return nil, fmt.Errorf("failed parsing --set-file data: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge together base config file and overrides, prefer the overrides
|
||||
conf := mergeValues(baseConf, overrideConf)
|
||||
|
||||
// Take the patched config and marshal back to YAML
|
||||
return yaml.Marshal(conf)
|
||||
}
|
||||
|
||||
// regex looking for ${...} notation strings
|
||||
var envRegex = regexp.MustCompile(`(?U:\${.*})`)
|
||||
|
||||
// subEnvVars will look for any environment variables in the passed in string
|
||||
// with the syntax of ${VAR_NAME} and replace that string with ENV[VAR_NAME]
|
||||
func subEnvVars(s string) string {
|
||||
updatedConfig := envRegex.ReplaceAllStringFunc(s, func(s string) string {
|
||||
// Trim off the '${' and '}'
|
||||
if len(s) <= 3 {
|
||||
// This should never happen..
|
||||
return ""
|
||||
}
|
||||
varName := s[2 : len(s)-1]
|
||||
|
||||
// Lookup the variable in the environment. We play by
|
||||
// bash rules.. if its undefined we'll treat it as an
|
||||
// empty string instead of raising an error.
|
||||
return os.Getenv(varName)
|
||||
})
|
||||
|
||||
return updatedConfig
|
||||
}
|
||||
|
||||
// mergeValues will merge source and destination map, preferring values from the source map
|
||||
func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} {
|
||||
for k, v := range src {
|
||||
// If the key doesn't exist already, then just set the key to that value
|
||||
if _, exists := dest[k]; !exists {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
nextMap, ok := v.(map[string]interface{})
|
||||
// If it isn't another map, overwrite the value
|
||||
if !ok {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
// Edge case: If the key exists in the destination, but isn't a map
|
||||
destMap, isMap := dest[k].(map[string]interface{})
|
||||
// If the source map has a map for this key, prefer it
|
||||
if !isMap {
|
||||
dest[k] = v
|
||||
continue
|
||||
}
|
||||
// If we got to this point, it is a map in both, so merge them
|
||||
dest[k] = mergeValues(destMap, nextMap)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2022 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.20
|
||||
|
||||
package errors
|
||||
|
||||
// Join returns an error that wraps the given errors.
|
||||
// Any nil error values are discarded.
|
||||
// Join returns nil if errs contains no non-nil values.
|
||||
// The error formats as the concatenation of the strings obtained
|
||||
// by calling the Error method of each element of errs, with a newline
|
||||
// between each string.
|
||||
func Join(errs ...error) error {
|
||||
n := 0
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
e := &joinError{
|
||||
errs: make([]error, 0, n),
|
||||
}
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
e.errs = append(e.errs, err)
|
||||
}
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
type joinError struct {
|
||||
errs []error
|
||||
}
|
||||
|
||||
func (e *joinError) Error() string {
|
||||
var b []byte
|
||||
for i, err := range e.errs {
|
||||
if i > 0 {
|
||||
b = append(b, '\n')
|
||||
}
|
||||
b = append(b, err.Error()...)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (e *joinError) Unwrap() []error {
|
||||
return e.errs
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//go:build go1.20
|
||||
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
var Join = errors.Join
|
||||
+2
-2
@@ -716,7 +716,7 @@ func (v *SubSchema) validateString(currentSubSchema *SubSchema, value interface{
|
||||
|
||||
// minLength & maxLength:
|
||||
if currentSubSchema.minLength != nil {
|
||||
if utf8.RuneCount([]byte(stringValue)) < *currentSubSchema.minLength {
|
||||
if utf8.RuneCountInString(stringValue) < *currentSubSchema.minLength {
|
||||
result.addInternalError(
|
||||
new(StringLengthGTEError),
|
||||
context,
|
||||
@@ -726,7 +726,7 @@ func (v *SubSchema) validateString(currentSubSchema *SubSchema, value interface{
|
||||
}
|
||||
}
|
||||
if currentSubSchema.maxLength != nil {
|
||||
if utf8.RuneCount([]byte(stringValue)) > *currentSubSchema.maxLength {
|
||||
if utf8.RuneCountInString(stringValue) > *currentSubSchema.maxLength {
|
||||
result.addInternalError(
|
||||
new(StringLengthLTEError),
|
||||
context,
|
||||
|
||||
+198
-44
@@ -25,6 +25,8 @@ type QuerySet struct {
|
||||
}
|
||||
|
||||
type planiter func() error
|
||||
type planLocalIter func(ir.Local) error
|
||||
type stmtFactory func(ir.Local) ir.Stmt
|
||||
|
||||
// Planner implements a query planner for Rego queries.
|
||||
type Planner struct {
|
||||
@@ -147,32 +149,31 @@ func (p *Planner) buildFunctrie() error {
|
||||
}
|
||||
|
||||
for _, rule := range module.Rules {
|
||||
r := rule.Ref()
|
||||
switch r[len(r)-1].Value.(type) {
|
||||
case ast.String: // pass
|
||||
default: // cut off
|
||||
r = r[:len(r)-1]
|
||||
}
|
||||
r := rule.Ref().StringPrefix()
|
||||
val := p.rules.LookupOrInsert(r)
|
||||
|
||||
val.rules = val.DescendantRules()
|
||||
val.rules = append(val.rules, rule)
|
||||
val.children = nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
// We know the rules with closer to the root (shorter static path) are ordered first.
|
||||
pathRef := rules[0].Ref()
|
||||
|
||||
// figure out what our rules' collective name/path is:
|
||||
// if we're planning both p.q.r and p.q[s], we'll name
|
||||
// the function p.q (for the mapping table)
|
||||
// TODO(sr): this has to change when allowing `p[v].q.r[w]` ref rules
|
||||
// including the mapping lookup structure and lookup functions
|
||||
pieces := len(pathRef)
|
||||
for i := range rules {
|
||||
r := rules[i].Ref()
|
||||
if _, ok := r[len(r)-1].Value.(ast.String); !ok {
|
||||
pieces = len(r) - 1
|
||||
for j, t := range r {
|
||||
if _, ok := t.Value.(ast.String); !ok && j > 0 && j < pieces {
|
||||
pieces = j
|
||||
}
|
||||
}
|
||||
}
|
||||
// control if p.a = 1 is to return 1 directly; or insert 1 under key "a" into an object
|
||||
@@ -236,7 +237,11 @@ func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeObjectStmt{Target: fn.Return}))
|
||||
}
|
||||
case ast.MultiValue:
|
||||
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeSetStmt{Target: fn.Return}))
|
||||
if buildObject {
|
||||
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeObjectStmt{Target: fn.Return}))
|
||||
} else {
|
||||
fn.Blocks = append(fn.Blocks, p.blockWithStmt(&ir.MakeSetStmt{Target: fn.Return}))
|
||||
}
|
||||
}
|
||||
|
||||
// For complete document rules, allocate one local variable for output
|
||||
@@ -252,6 +257,12 @@ func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
var defaultRule *ast.Rule
|
||||
var ruleLoc *location.Location
|
||||
|
||||
// We sort rules by ref length, to ensure that when merged, we can detect conflicts when one
|
||||
// rule attempts to override values (deep and shallow) defined by another rule.
|
||||
sort.Slice(rules, func(i, j int) bool {
|
||||
return len(rules[i].Ref()) > len(rules[j].Ref())
|
||||
})
|
||||
|
||||
// Generate function blocks for rules.
|
||||
for i := range rules {
|
||||
|
||||
@@ -320,18 +331,19 @@ func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
switch rule.Head.RuleKind() {
|
||||
case ast.SingleValue:
|
||||
if buildObject {
|
||||
ref := rule.Head.Ref()
|
||||
last := ref[len(ref)-1]
|
||||
return p.planTerm(last, func() error {
|
||||
key := p.ltarget
|
||||
return p.planTerm(rule.Head.Value, func() error {
|
||||
value := p.ltarget
|
||||
p.appendStmt(&ir.ObjectInsertOnceStmt{
|
||||
Object: fn.Return,
|
||||
Key: key,
|
||||
Value: value,
|
||||
ref := rule.Ref()
|
||||
return p.planTerm(rule.Head.Value, func() error {
|
||||
value := p.ltarget
|
||||
return p.planNestedObjects(fn.Return, ref[pieces:len(ref)-1], func(obj ir.Local) error {
|
||||
return p.planTerm(ref[len(ref)-1], func() error {
|
||||
key := p.ltarget
|
||||
p.appendStmt(&ir.ObjectInsertOnceStmt{
|
||||
Object: obj,
|
||||
Key: key,
|
||||
Value: value,
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -343,6 +355,28 @@ func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
return nil
|
||||
})
|
||||
case ast.MultiValue:
|
||||
if buildObject {
|
||||
ref := rule.Ref()
|
||||
// we drop the trailing set key from the ref
|
||||
return p.planNestedObjects(fn.Return, ref[pieces:len(ref)-1], func(obj ir.Local) error {
|
||||
// Last term on rule ref is the key an which the set is assigned in the deepest nested object
|
||||
return p.planTerm(ref[len(ref)-1], func() error {
|
||||
key := p.ltarget
|
||||
return p.planTerm(rule.Head.Key, func() error {
|
||||
value := p.ltarget
|
||||
factory := func(v ir.Local) ir.Stmt { return &ir.MakeSetStmt{Target: v} }
|
||||
return p.planDotOr(obj, key, factory, func(set ir.Local) error {
|
||||
p.appendStmt(&ir.SetAddStmt{
|
||||
Set: set,
|
||||
Value: value,
|
||||
})
|
||||
p.appendStmt(&ir.ObjectInsertStmt{Key: key, Value: op(set), Object: obj})
|
||||
return nil
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
return p.planTerm(rule.Head.Key, func() error {
|
||||
p.appendStmt(&ir.SetAddStmt{
|
||||
Set: fn.Return,
|
||||
@@ -422,6 +456,63 @@ func (p *Planner) planRules(rules []*ast.Rule) (string, error) {
|
||||
return fn.Name, nil
|
||||
}
|
||||
|
||||
func (p *Planner) planDotOr(obj ir.Local, key ir.Operand, or stmtFactory, iter planLocalIter) error {
|
||||
// We're constructing the following plan:
|
||||
//
|
||||
// | block a
|
||||
// | | block b
|
||||
// | | | dot &{Source:Local<obj> Key:{Value:Local<key>} Target:Local<val>}
|
||||
// | | | break 1
|
||||
// | | or &{Target:Local<val>}
|
||||
// | iter &{Target:Local<val>} # may update Local<val>.
|
||||
// | *ir.ObjectInsertStmt &{Key:{Value:Local<key>} Value:{Value:Local<val>} Object:Local<obj>}
|
||||
|
||||
prev := p.curr
|
||||
dotBlock := &ir.Block{}
|
||||
p.curr = dotBlock
|
||||
|
||||
val := p.newLocal()
|
||||
p.appendStmt(&ir.DotStmt{
|
||||
Source: op(obj),
|
||||
Key: key,
|
||||
Target: val,
|
||||
})
|
||||
p.appendStmt(&ir.BreakStmt{Index: 1})
|
||||
|
||||
outerBlock := &ir.Block{
|
||||
Stmts: []ir.Stmt{
|
||||
&ir.BlockStmt{Blocks: []*ir.Block{dotBlock}}, // FIXME: Set Location
|
||||
or(val),
|
||||
},
|
||||
}
|
||||
|
||||
p.curr = prev
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outerBlock}})
|
||||
if err := iter(val); err != nil {
|
||||
return err
|
||||
}
|
||||
p.appendStmt(&ir.ObjectInsertStmt{Key: key, Value: op(val), Object: obj})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Planner) planNestedObjects(obj ir.Local, ref ast.Ref, iter planLocalIter) error {
|
||||
if len(ref) == 0 {
|
||||
//return fmt.Errorf("nested object construction didn't create object")
|
||||
return iter(obj)
|
||||
}
|
||||
|
||||
t := ref[0]
|
||||
|
||||
return p.planTerm(t, func() error {
|
||||
key := p.ltarget
|
||||
|
||||
factory := func(v ir.Local) ir.Stmt { return &ir.MakeObjectStmt{Target: v} }
|
||||
return p.planDotOr(obj, key, factory, func(childObj ir.Local) error {
|
||||
return p.planNestedObjects(childObj, ref[1:], iter)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Planner) planFuncParams(params []ir.Local, args ast.Args, idx int, iter planiter) error {
|
||||
if idx >= len(args) {
|
||||
return iter()
|
||||
@@ -754,13 +845,29 @@ func (p *Planner) dataRefsShadowRuletrie(refs []ast.Ref) bool {
|
||||
}
|
||||
|
||||
func (p *Planner) planExprTerm(e *ast.Expr, iter planiter) error {
|
||||
return p.planTerm(e.Terms.(*ast.Term), func() error {
|
||||
p.appendStmt(&ir.NotEqualStmt{
|
||||
A: p.ltarget,
|
||||
B: op(ir.Bool(false)),
|
||||
// NOTE(sr): There are only three cases to deal with when we see a naked term
|
||||
// in a rule body:
|
||||
// 1. it's `false` -- so we can stop, emit a break stmt
|
||||
// 2. it's a var or a ref, like `input` or `data.foo.bar`, where we need to
|
||||
// check what it ends up being (at run time) to determine if it's not false
|
||||
// 3. it's any other term -- `true`, a string, a number, whatever. We can skip
|
||||
// that, since it's true-ish enough for evaluating the rule body.
|
||||
switch t := e.Terms.(*ast.Term).Value.(type) {
|
||||
case ast.Boolean:
|
||||
if !bool(t) { // We know this cannot hold, break unconditionally
|
||||
p.appendStmt(&ir.BreakStmt{})
|
||||
return iter()
|
||||
}
|
||||
case ast.Ref, ast.Var: // We don't know these at plan-time
|
||||
return p.planTerm(e.Terms.(*ast.Term), func() error {
|
||||
p.appendStmt(&ir.NotEqualStmt{
|
||||
A: p.ltarget,
|
||||
B: op(ir.Bool(false)),
|
||||
})
|
||||
return iter()
|
||||
})
|
||||
return iter()
|
||||
})
|
||||
}
|
||||
return iter()
|
||||
}
|
||||
|
||||
func (p *Planner) planExprEvery(e *ast.Expr, iter planiter) error {
|
||||
@@ -1116,6 +1223,24 @@ func (p *Planner) planUnifyVar(a ast.Var, b *ast.Term, iter planiter) error {
|
||||
}
|
||||
|
||||
func (p *Planner) planUnifyLocal(a ir.Operand, b *ast.Term, iter planiter) error {
|
||||
// special cases: when a is StringIndex or Bool, and b is a string, or a bool, we can shortcut
|
||||
switch va := a.Value.(type) {
|
||||
case ir.StringIndex:
|
||||
if vb, ok := b.Value.(ast.String); ok {
|
||||
if va != ir.StringIndex(p.getStringConst(string(vb))) {
|
||||
p.appendStmt(&ir.BreakStmt{})
|
||||
}
|
||||
return iter() // Don't plan EqualStmt{A: "foo", B: "foo"}
|
||||
}
|
||||
case ir.Bool:
|
||||
if vb, ok := b.Value.(ast.Boolean); ok {
|
||||
if va != ir.Bool(vb) {
|
||||
p.appendStmt(&ir.BreakStmt{})
|
||||
}
|
||||
return iter() // Don't plan EqualStmt{A: true, B: true}
|
||||
}
|
||||
}
|
||||
|
||||
switch vb := b.Value.(type) {
|
||||
case ast.Null, ast.Boolean, ast.Number, ast.String, ast.Ref, ast.Set, *ast.SetComprehension, *ast.ArrayComprehension, *ast.ObjectComprehension:
|
||||
return p.planTerm(b, func() error {
|
||||
@@ -1565,16 +1690,14 @@ func (p *Planner) planComprehension(body ast.Body, closureIter planiter, target
|
||||
// below.
|
||||
p.vars.Push(map[ast.Var]ir.Local{})
|
||||
prev := p.curr
|
||||
p.curr = &ir.Block{}
|
||||
block := &ir.Block{}
|
||||
p.curr = block
|
||||
ploc := p.loc
|
||||
|
||||
if err := p.planQuery(body, 0, func() error {
|
||||
return closureIter()
|
||||
}); err != nil {
|
||||
if err := p.planQuery(body, 0, closureIter); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
block := p.curr
|
||||
p.curr = prev
|
||||
p.loc = ploc
|
||||
p.vars.Pop()
|
||||
@@ -1737,11 +1860,12 @@ func (p *Planner) planRefData(virtual *ruletrie, base *baseptr, ref ast.Ref, ind
|
||||
}},
|
||||
}}
|
||||
p.curr = outerBlock
|
||||
return p.planRefRec(ref, index+1, func() error { // rest of the ref
|
||||
p.curr = prev
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outerBlock}})
|
||||
return iter()
|
||||
})
|
||||
if err := p.planRefRec(ref, index+1, iter); err != nil { // rest of the ref
|
||||
return err
|
||||
}
|
||||
p.curr = prev
|
||||
p.appendStmt(&ir.BlockStmt{Blocks: []*ir.Block{outerBlock}})
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2236,6 +2360,7 @@ func (p *Planner) optimizeLookup(t *ruletrie, ref ast.Ref) ([][]*ast.Rule, []ir.
|
||||
var index int
|
||||
|
||||
// ref[0] is data, ignore
|
||||
outer:
|
||||
for i := 1; i < len(ref); i++ {
|
||||
index = i
|
||||
r := ref[i]
|
||||
@@ -2259,7 +2384,7 @@ func (p *Planner) optimizeLookup(t *ruletrie, ref ast.Ref) ([][]*ast.Rule, []ir.
|
||||
}
|
||||
}
|
||||
case ast.String:
|
||||
// take matching children
|
||||
// take all children that either match or have a var key
|
||||
for _, node := range nodes {
|
||||
if node := node.Get(r); node != nil {
|
||||
nextNodes = append(nextNodes, node)
|
||||
@@ -2272,15 +2397,44 @@ func (p *Planner) optimizeLookup(t *ruletrie, ref ast.Ref) ([][]*ast.Rule, []ir.
|
||||
|
||||
nodes = nextNodes
|
||||
|
||||
// if all nodes have 0 children, abort ref check and optimize
|
||||
all := true
|
||||
// if all nodes have rules() > 0, abort ref check and optimize
|
||||
// NOTE(sr): for a.b[c] = ... and a.b.d = ..., we stop at a.b, as its rules()
|
||||
// will collect the children rules
|
||||
// We keep the "all nodes have 0 children" check since it's cheaper and might
|
||||
// let us break, too.
|
||||
all := 0
|
||||
for _, node := range nodes {
|
||||
all = all && len(node.Children()) == 0
|
||||
all += node.ChildrenCount()
|
||||
}
|
||||
if all {
|
||||
if all == 0 {
|
||||
p.debugf("ref %s: all nodes have 0 children, break", ref[0:index+1])
|
||||
break
|
||||
}
|
||||
|
||||
// NOTE(sr): we only need this check for the penultimate part:
|
||||
// When planning the ref data.pkg.a[input.x][input.y],
|
||||
// We want to capture this situation:
|
||||
// a.b[c] := "x" if c := "c"
|
||||
// a.b.d := "y"
|
||||
//
|
||||
// Not this:
|
||||
// a.b[c] := "x" if c := "c"
|
||||
// a.d := "y"
|
||||
// since the length doesn't add up. Even if input.x was "d", the second
|
||||
// rule (a.d) wouldn't contribute anything to the result, since we cannot
|
||||
// "dot it".
|
||||
if index == len(ref)-2 {
|
||||
for _, node := range nodes {
|
||||
anyNonGround := false
|
||||
for _, r := range node.Rules() {
|
||||
anyNonGround = anyNonGround || !r.Ref().IsGround()
|
||||
}
|
||||
if anyNonGround {
|
||||
p.debugf("ref %s: at least one node has 1+ non-ground ref rules, break", ref[0:index+1])
|
||||
break outer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var res [][]*ast.Rule
|
||||
@@ -2295,7 +2449,7 @@ func (p *Planner) optimizeLookup(t *ruletrie, ref ast.Ref) ([][]*ast.Rule, []ir.
|
||||
for _, node := range nodes {
|
||||
// we're done with ref, check if there's only ruleset leaves; collect rules
|
||||
if index == len(ref)-1 {
|
||||
if len(node.Rules()) == 0 && len(node.Children()) > 0 {
|
||||
if len(node.Rules()) == 0 && node.ChildrenCount() > 0 {
|
||||
p.debugf("no optimization of %s: unbalanced ruletrie", ref)
|
||||
return dont()
|
||||
}
|
||||
|
||||
+39
-1
@@ -98,6 +98,7 @@ func (t *ruletrie) Rules() []*ast.Rule {
|
||||
//
|
||||
// and we're retrieving a.b, we want Rules() to include the rule body
|
||||
// of a.b.c.
|
||||
// FIXME: We need to go deeper than just immediate children (?)
|
||||
for _, rs := range t.children {
|
||||
if r := rs[len(rs)-1].rules; r != nil {
|
||||
rules = append(rules, r...)
|
||||
@@ -157,13 +158,50 @@ func (t *ruletrie) Lookup(key ast.Ref) *ruletrie {
|
||||
return node
|
||||
}
|
||||
|
||||
func (t *ruletrie) LookupShallowest(key ast.Ref) *ruletrie {
|
||||
node := t
|
||||
for _, elem := range key {
|
||||
node = node.Get(elem.Value)
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if len(node.rules) > 0 {
|
||||
return node
|
||||
}
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
// TODO: Collapse rules with overlapping extent to same node(?)
|
||||
func (t *ruletrie) LookupOrInsert(key ast.Ref) *ruletrie {
|
||||
if val := t.Lookup(key); val != nil {
|
||||
if val := t.LookupShallowest(key); val != nil {
|
||||
|
||||
return val
|
||||
}
|
||||
return t.Insert(key)
|
||||
}
|
||||
|
||||
func (t *ruletrie) DescendantRules() []*ast.Rule {
|
||||
if len(t.children) == 0 {
|
||||
return t.rules
|
||||
}
|
||||
|
||||
rules := make([]*ast.Rule, len(t.rules), len(t.rules)+len(t.children)) // could be too little
|
||||
copy(rules, t.rules)
|
||||
|
||||
for _, cs := range t.children {
|
||||
for _, c := range cs {
|
||||
rules = append(rules, c.DescendantRules()...)
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
func (t *ruletrie) ChildrenCount() int {
|
||||
return len(t.children)
|
||||
}
|
||||
|
||||
func (t *ruletrie) Children() []ast.Value {
|
||||
if t == nil {
|
||||
return nil
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/version"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
// Values taken from
|
||||
// https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
|
||||
const (
|
||||
ecrGetAuthorizationTokenTarget = "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken"
|
||||
ecrEndpointFmt = "https://ecr.%s.amazonaws.com/"
|
||||
)
|
||||
|
||||
// ECR is used to request tokens from Elastic Container Registry.
|
||||
type ECR struct {
|
||||
// endpoint returns the region-specifc ECR endpoint.
|
||||
// It can be overridden by tests.
|
||||
endpoint func(region string) string
|
||||
|
||||
// client is used to send authorization tokens requests.
|
||||
client *http.Client
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func NewECR(logger logging.Logger) *ECR {
|
||||
return &ECR{
|
||||
endpoint: func(region string) string {
|
||||
return fmt.Sprintf(ecrEndpointFmt, region)
|
||||
},
|
||||
client: &http.Client{},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthorizationToken requests a token that can be used to authenticate image pull requests.
|
||||
func (e *ECR) GetAuthorizationToken(ctx context.Context, creds Credentials, signatureVersion string) (ECRAuthorizationToken, error) {
|
||||
endpoint := e.endpoint(creds.RegionName)
|
||||
body := strings.NewReader("{}")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
|
||||
if err != nil {
|
||||
return ECRAuthorizationToken{}, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Amz-Target", ecrGetAuthorizationTokenTarget)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
||||
req.Header.Set("User-Agent", version.UserAgent)
|
||||
|
||||
e.logger.Debug("Signing ECR authorization token request")
|
||||
|
||||
if err := SignRequest(req, "ecr", creds, time.Now(), signatureVersion); err != nil {
|
||||
return ECRAuthorizationToken{}, fmt.Errorf("failed to sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithClient(req, e.client, "ecr get authorization token", e.logger)
|
||||
if err != nil {
|
||||
return ECRAuthorizationToken{}, err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
AuthorizationData []struct {
|
||||
AuthorizationToken string `json:"authorizationToken"`
|
||||
ExpiresAt json.Number `json:"expiresAt"`
|
||||
} `json:"authorizationData"`
|
||||
}
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return ECRAuthorizationToken{}, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
if len(data.AuthorizationData) < 1 {
|
||||
return ECRAuthorizationToken{}, errors.New("empty authorization data")
|
||||
}
|
||||
|
||||
// The GetAuthorizationToken request returns a list of tokens for
|
||||
// backwards compatibility reasons. We should only ever get one token back
|
||||
// because we don't define any registryIDs in the request.
|
||||
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html#API_GetAuthorizationToken_ResponseSyntax
|
||||
resultToken := data.AuthorizationData[0]
|
||||
|
||||
expiresAt, err := parseTimestamp(resultToken.ExpiresAt)
|
||||
if err != nil {
|
||||
return ECRAuthorizationToken{}, fmt.Errorf("failed to parse expiresAt: %w", err)
|
||||
}
|
||||
|
||||
return ECRAuthorizationToken{
|
||||
AuthorizationToken: resultToken.AuthorizationToken,
|
||||
ExpiresAt: expiresAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ECRAuthorizationToken can sign requests to AWS ECR.
|
||||
//
|
||||
// It corresponds to data returned by the AWS GetAuthorizationToken API.
|
||||
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_AuthorizationData.html
|
||||
type ECRAuthorizationToken struct {
|
||||
AuthorizationToken string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// IsValid returns true if the token is set and not expired.
|
||||
// It respects a margin of error for time handling and will mark it as expired early.
|
||||
func (t *ECRAuthorizationToken) IsValid() bool {
|
||||
const tokenExpirationMargin = 5 * time.Minute
|
||||
|
||||
expired := time.Now().Add(tokenExpirationMargin).After(t.ExpiresAt)
|
||||
return t.AuthorizationToken != "" && !expired
|
||||
}
|
||||
|
||||
var millisecondsFloat = new(big.Float).SetInt64(1e3)
|
||||
|
||||
// parseTimestamp parses the AWS format for timestamps.
|
||||
// The time precision is in milliseconds.
|
||||
//
|
||||
// The logic is taken from
|
||||
// https://github.com/aws/aws-sdk-go/blob/41717ba2c04d3fd03f94d09ea984a10899574935/private/protocol/json/jsonutil/unmarshal.go#L294-L302
|
||||
func parseTimestamp(raw json.Number) (time.Time, error) {
|
||||
s := raw.String()
|
||||
|
||||
float, ok := new(big.Float).SetString(s)
|
||||
if !ok {
|
||||
return time.Time{}, fmt.Errorf("not a float: %q", raw)
|
||||
}
|
||||
|
||||
// The float is expected to be in second resolution with millisecond
|
||||
// decimal places.
|
||||
// Multiply by millisecondsFloat to obtain an integer in millisecond
|
||||
// resolution
|
||||
ms, _ := float.Mul(float, millisecondsFloat).Int64()
|
||||
|
||||
// Multiply again to obtain nanosecond resolution for time.Unix
|
||||
ns := ms * 1e6
|
||||
|
||||
t := time.Unix(0, ns).UTC()
|
||||
|
||||
return t, nil
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/version"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
// Values taken from
|
||||
// https://docs.aws.amazon.com/kms/latest/APIReference/Welcome.html
|
||||
// https://docs.aws.amazon.com/general/latest/gr/kms.html
|
||||
const (
|
||||
kmsSignTarget = "TrentService.Sign"
|
||||
kmsEndpointFmt = "https://kms.%s.amazonaws.com/"
|
||||
)
|
||||
|
||||
// KMS is used to sign payloads using AWS Key Management Service.
|
||||
type KMS struct {
|
||||
// endpoint returns the region-specifc KMS endpoint.
|
||||
// It can be overridden by tests.
|
||||
endpoint func(region string) string
|
||||
|
||||
// client is used to send authorization tokens requests.
|
||||
client *http.Client
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func NewKMS(logger logging.Logger) *KMS {
|
||||
return &KMS{
|
||||
endpoint: func(region string) string {
|
||||
return fmt.Sprintf(kmsEndpointFmt, region)
|
||||
},
|
||||
client: &http.Client{},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func NewKMSWithURLClient(url string, client *http.Client, logger logging.Logger) *KMS {
|
||||
return &KMS{
|
||||
endpoint: func(string) string { return url },
|
||||
client: client,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
type KMSSignRequest struct {
|
||||
KeyID string `json:"KeyId"`
|
||||
Message string `json:"Message"`
|
||||
MessageType string `json:"MessageType"`
|
||||
SigningAlgorithm string `json:"SigningAlgorithm"`
|
||||
}
|
||||
type KMSSignResponse struct {
|
||||
KeyID string `json:"KeyId"`
|
||||
Signature string `json:"Signature"`
|
||||
SigningAlgorithm string `json:"SigningAlgorithm"`
|
||||
}
|
||||
|
||||
// SignDigest signs a digest using KMS.
|
||||
func (k *KMS) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string, creds Credentials, signatureVersion string) (string, error) {
|
||||
endpoint := k.endpoint(creds.RegionName)
|
||||
|
||||
kmsRequest := KMSSignRequest{
|
||||
KeyID: keyID,
|
||||
Message: base64.StdEncoding.EncodeToString(digest),
|
||||
MessageType: "DIGEST",
|
||||
SigningAlgorithm: signingAlgorithm,
|
||||
}
|
||||
requestJSONBytes, err := json.Marshal(kmsRequest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshall request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(requestJSONBytes))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Amz-Target", kmsSignTarget)
|
||||
req.Header.Set("Accept-Encoding", "identity")
|
||||
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
||||
req.Header.Set("User-Agent", version.UserAgent)
|
||||
|
||||
if err := SignRequest(req, "kms", creds, time.Now(), signatureVersion); err != nil {
|
||||
return "", fmt.Errorf("failed to sign request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithClient(req, k.client, "kms sign digest", k.logger)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var data KMSSignResponse
|
||||
if err := json.Unmarshal(resp, &data); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal response: %w", err)
|
||||
}
|
||||
|
||||
return data.Signature, nil
|
||||
}
|
||||
+41
-1
@@ -5,9 +5,13 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -74,6 +78,42 @@ func sortKeys(strMap map[string][]string) []string {
|
||||
return keys
|
||||
}
|
||||
|
||||
// SignRequest modifies an http.Request to include an AWS V4 signature based on the provided credentials.
|
||||
func SignRequest(req *http.Request, service string, creds Credentials, theTime time.Time, sigVersion string) error {
|
||||
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
||||
// S3 ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
|
||||
// APIGateway ref. https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/
|
||||
|
||||
var body []byte
|
||||
if req.Body == nil {
|
||||
body = []byte("")
|
||||
} else {
|
||||
var err error
|
||||
body, err = io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
return errors.New("error getting request body: " + err.Error())
|
||||
}
|
||||
// Since ReadAll consumed the body ReadCloser, we must create a new ReadCloser for the request so that the
|
||||
// subsequent read starts from the beginning
|
||||
req.Body = io.NopCloser(bytes.NewReader(body))
|
||||
}
|
||||
|
||||
now := theTime.UTC()
|
||||
|
||||
if sigVersion == "4a" {
|
||||
signedHeaders := SignV4a(req.Header, req.Method, req.URL, body, service, creds, now)
|
||||
req.Header = signedHeaders
|
||||
} else {
|
||||
authHeader, awsHeaders := SignV4(req.Header, req.Method, req.URL, body, service, creds, now)
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
for k, v := range awsHeaders {
|
||||
req.Header.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignV4 modifies a map[string][]string of headers to generate an AWS V4 signature + headers based on the config/credentials provided.
|
||||
func SignV4(headers map[string][]string, method string, theURL *url.URL, body []byte, service string, awsCreds Credentials, theTime time.Time) (string, map[string]string) {
|
||||
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
||||
@@ -118,7 +158,7 @@ func SignV4(headers map[string][]string, method string, theURL *url.URL, body []
|
||||
}
|
||||
|
||||
// the "canonical request" is the normalized version of the AWS service access
|
||||
// that we're attempting to perform; in this case, a GET from an S3 bucket
|
||||
// that we're attempting to perform
|
||||
canonicalReq := method + "\n" // HTTP method
|
||||
canonicalReq += theURL.EscapedPath() + "\n" // URI-escaped path
|
||||
canonicalReq += theURL.RawQuery + "\n" // RAW Query String
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
// DoRequestWithClient is a convenience function to get the body of an http response with
|
||||
// appropriate error-handling boilerplate and logging.
|
||||
func DoRequestWithClient(req *http.Request, client *http.Client, desc string, logger logging.Logger) ([]byte, error) {
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// some kind of catastrophe talking to the service
|
||||
return nil, errors.New(desc + " HTTP request failed: " + err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
logger.WithFields(map[string]interface{}{
|
||||
"url": req.URL.String(),
|
||||
"status": resp.Status,
|
||||
"headers": resp.Header,
|
||||
}).Debug("Received response from " + desc + " service.")
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if logger.GetLevel() == logging.Debug {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err == nil {
|
||||
logger.Debug("Error response with response body: %s", body)
|
||||
}
|
||||
}
|
||||
// could be 404 for role that's not available, but cover all the bases
|
||||
return nil, errors.New(desc + " HTTP request returned unexpected status: " + resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
// deal with problems reading the body, whatever those might be
|
||||
return nil, errors.New(desc + " HTTP response body could not be read: " + err.Error())
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
// 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 init is an internal package with helpers for data and policy loading during initialization.
|
||||
package init
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/bundle"
|
||||
storedversion "github.com/open-policy-agent/opa/internal/version"
|
||||
"github.com/open-policy-agent/opa/loader"
|
||||
"github.com/open-policy-agent/opa/metrics"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
)
|
||||
|
||||
// InsertAndCompileOptions contains the input for the operation.
|
||||
type InsertAndCompileOptions struct {
|
||||
Store storage.Store
|
||||
Txn storage.Transaction
|
||||
Files loader.Result
|
||||
Bundles map[string]*bundle.Bundle
|
||||
MaxErrors int
|
||||
EnablePrintStatements bool
|
||||
}
|
||||
|
||||
// InsertAndCompileResult contains the output of the operation.
|
||||
type InsertAndCompileResult struct {
|
||||
Compiler *ast.Compiler
|
||||
Metrics metrics.Metrics
|
||||
}
|
||||
|
||||
// InsertAndCompile writes data and policy into the store and returns a compiler for the
|
||||
// store contents.
|
||||
func InsertAndCompile(ctx context.Context, opts InsertAndCompileOptions) (*InsertAndCompileResult, error) {
|
||||
if len(opts.Files.Documents) > 0 {
|
||||
if err := opts.Store.Write(ctx, opts.Txn, storage.AddOp, storage.Path{}, opts.Files.Documents); err != nil {
|
||||
return nil, fmt.Errorf("storage error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
policies := make(map[string]*ast.Module, len(opts.Files.Modules))
|
||||
|
||||
for id, parsed := range opts.Files.Modules {
|
||||
policies[id] = parsed.Parsed
|
||||
}
|
||||
|
||||
compiler := ast.NewCompiler().
|
||||
SetErrorLimit(opts.MaxErrors).
|
||||
WithPathConflictsCheck(storage.NonEmpty(ctx, opts.Store, opts.Txn)).
|
||||
WithEnablePrintStatements(opts.EnablePrintStatements)
|
||||
m := metrics.New()
|
||||
|
||||
activation := &bundle.ActivateOpts{
|
||||
Ctx: ctx,
|
||||
Store: opts.Store,
|
||||
Txn: opts.Txn,
|
||||
Compiler: compiler,
|
||||
Metrics: m,
|
||||
Bundles: opts.Bundles,
|
||||
ExtraModules: policies,
|
||||
}
|
||||
|
||||
err := bundle.Activate(activation)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Policies in bundles will have already been added to the store, but
|
||||
// modules loaded outside of bundles will need to be added manually.
|
||||
for id, parsed := range opts.Files.Modules {
|
||||
if err := opts.Store.UpsertPolicy(ctx, opts.Txn, id, parsed.Raw); err != nil {
|
||||
return nil, fmt.Errorf("storage error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the version in the store last to prevent data files from overwriting.
|
||||
if err := storedversion.Write(ctx, opts.Store, opts.Txn); err != nil {
|
||||
return nil, fmt.Errorf("storage error: %w", err)
|
||||
}
|
||||
|
||||
return &InsertAndCompileResult{Compiler: compiler, Metrics: m}, nil
|
||||
}
|
||||
|
||||
// LoadPathsResult contains the output loading a set of paths.
|
||||
type LoadPathsResult struct {
|
||||
Bundles map[string]*bundle.Bundle
|
||||
Files loader.Result
|
||||
}
|
||||
|
||||
// WalkPathsResult contains the output loading a set of paths.
|
||||
type WalkPathsResult struct {
|
||||
BundlesLoader []BundleLoader
|
||||
FileDescriptors []*Descriptor
|
||||
}
|
||||
|
||||
// BundleLoader contains information about files in a bundle
|
||||
type BundleLoader struct {
|
||||
DirectoryLoader bundle.DirectoryLoader
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
// Descriptor contains information about a file
|
||||
type Descriptor struct {
|
||||
Root string
|
||||
Path string
|
||||
}
|
||||
|
||||
// LoadPaths reads data and policy from the given paths and returns a set of bundles or
|
||||
// raw loader file results.
|
||||
func LoadPaths(paths []string,
|
||||
filter loader.Filter,
|
||||
asBundle bool,
|
||||
bvc *bundle.VerificationConfig,
|
||||
skipVerify bool,
|
||||
processAnnotations bool,
|
||||
caps *ast.Capabilities,
|
||||
fsys fs.FS) (*LoadPathsResult, error) {
|
||||
|
||||
if caps == nil {
|
||||
caps = ast.CapabilitiesForThisVersion()
|
||||
}
|
||||
|
||||
// tar.gz files are automatically loaded as bundles
|
||||
var likelyBundles, nonBundlePaths []string
|
||||
if !asBundle {
|
||||
likelyBundles, nonBundlePaths = splitByTarGzExt(paths)
|
||||
paths = likelyBundles
|
||||
}
|
||||
|
||||
var result LoadPathsResult
|
||||
var err error
|
||||
if asBundle || len(likelyBundles) > 0 {
|
||||
result.Bundles = make(map[string]*bundle.Bundle, len(paths))
|
||||
for _, path := range paths {
|
||||
result.Bundles[path], err = loader.NewFileLoader().
|
||||
WithFS(fsys).
|
||||
WithBundleVerificationConfig(bvc).
|
||||
WithSkipBundleVerification(skipVerify).
|
||||
WithFilter(filter).
|
||||
WithProcessAnnotation(processAnnotations).
|
||||
WithCapabilities(caps).
|
||||
AsBundle(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(nonBundlePaths) == 0 {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
files, err := loader.NewFileLoader().
|
||||
WithFS(fsys).
|
||||
WithProcessAnnotation(processAnnotations).
|
||||
WithCapabilities(caps).
|
||||
Filtered(nonBundlePaths, filter)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.Files = *files
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// splitByTarGzExt splits the paths in 2 groups. Ones with .tar.gz and another with
|
||||
// non .tar.gz extensions.
|
||||
func splitByTarGzExt(paths []string) (targzs []string, nonTargzs []string) {
|
||||
for _, path := range paths {
|
||||
if strings.HasSuffix(path, ".tar.gz") {
|
||||
targzs = append(targzs, path)
|
||||
} else {
|
||||
nonTargzs = append(nonTargzs, path)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// WalkPaths reads data and policy from the given paths and returns a set of bundle directory loaders
|
||||
// or descriptors that contain information about files.
|
||||
func WalkPaths(paths []string, filter loader.Filter, asBundle bool) (*WalkPathsResult, error) {
|
||||
|
||||
var result WalkPathsResult
|
||||
|
||||
if asBundle {
|
||||
result.BundlesLoader = make([]BundleLoader, len(paths))
|
||||
for i, path := range paths {
|
||||
bundleLoader, isDir, err := loader.GetBundleDirectoryLoader(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.BundlesLoader[i] = BundleLoader{
|
||||
DirectoryLoader: bundleLoader,
|
||||
IsDir: isDir,
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
result.FileDescriptors = []*Descriptor{}
|
||||
for _, path := range paths {
|
||||
filePaths, err := loader.FilteredPaths([]string{path}, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, fp := range filePaths {
|
||||
// Trim off the root directory and return path as if chrooted
|
||||
cleanedPath := strings.TrimPrefix(fp, path)
|
||||
if path == "." && filepath.Base(fp) == bundle.ManifestExt {
|
||||
cleanedPath = fp
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(cleanedPath, "/") {
|
||||
cleanedPath = "/" + cleanedPath
|
||||
}
|
||||
|
||||
result.FileDescriptors = append(result.FileDescriptors, &Descriptor{
|
||||
Root: path,
|
||||
Path: cleanedPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
Package strvals provides tools for working with strval lines.
|
||||
|
||||
OPA runtime config supports a compressed format for YAML settings which we call strvals.
|
||||
The format is roughly like this:
|
||||
|
||||
name=value,topname.subname=value
|
||||
|
||||
The above is equivalent to the YAML document
|
||||
|
||||
name: value
|
||||
topname:
|
||||
subname: value
|
||||
|
||||
This package provides a parser and utilities for converting the strvals format
|
||||
to other formats.
|
||||
*/
|
||||
package strvals
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package strvals
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
// ErrNotList indicates that a non-list was treated as a list.
|
||||
var ErrNotList = errors.New("not a list")
|
||||
|
||||
// MaxIndex is the maximum index that will be allowed by setIndex.
|
||||
// The default value 65536 = 1024 * 64
|
||||
var MaxIndex = 65536
|
||||
|
||||
// ToYAML takes a string of arguments and converts to a YAML document.
|
||||
func ToYAML(s string) (string, error) {
|
||||
m, err := Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d, err := yaml.Marshal(m)
|
||||
return string(d), err
|
||||
}
|
||||
|
||||
// Parse parses a set line.
|
||||
//
|
||||
// A set line is of the form name1=value1,name2=value2
|
||||
func Parse(s string) (map[string]interface{}, error) {
|
||||
vals := map[string]interface{}{}
|
||||
scanner := bytes.NewBufferString(s)
|
||||
t := newParser(scanner, vals, false)
|
||||
err := t.parse()
|
||||
return vals, err
|
||||
}
|
||||
|
||||
// ParseString parses a set line and forces a string value.
|
||||
//
|
||||
// A set line is of the form name1=value1,name2=value2
|
||||
func ParseString(s string) (map[string]interface{}, error) {
|
||||
vals := map[string]interface{}{}
|
||||
scanner := bytes.NewBufferString(s)
|
||||
t := newParser(scanner, vals, true)
|
||||
err := t.parse()
|
||||
return vals, err
|
||||
}
|
||||
|
||||
// ParseInto parses a strvals line and merges the result into dest.
|
||||
//
|
||||
// If the strval string has a key that exists in dest, it overwrites the
|
||||
// dest version.
|
||||
func ParseInto(s string, dest map[string]interface{}) error {
|
||||
scanner := bytes.NewBufferString(s)
|
||||
t := newParser(scanner, dest, false)
|
||||
return t.parse()
|
||||
}
|
||||
|
||||
// ParseIntoFile parses a filevals line and merges the result into dest.
|
||||
//
|
||||
// This method always returns a string as the value.
|
||||
func ParseIntoFile(s string, dest map[string]interface{}, runesToVal runesToVal) error {
|
||||
scanner := bytes.NewBufferString(s)
|
||||
t := newFileParser(scanner, dest, runesToVal)
|
||||
return t.parse()
|
||||
}
|
||||
|
||||
// ParseIntoString parses a strvals line and merges the result into dest.
|
||||
//
|
||||
// This method always returns a string as the value.
|
||||
func ParseIntoString(s string, dest map[string]interface{}) error {
|
||||
scanner := bytes.NewBufferString(s)
|
||||
t := newParser(scanner, dest, true)
|
||||
return t.parse()
|
||||
}
|
||||
|
||||
// parser is a simple parser that takes a strvals line and parses it into a
|
||||
// map representation.
|
||||
//
|
||||
// where sc is the source of the original data being parsed
|
||||
// where data is the final parsed data from the parses with correct types
|
||||
// where st is a boolean to figure out if we're forcing it to parse values as string
|
||||
type parser struct {
|
||||
sc *bytes.Buffer
|
||||
data map[string]interface{}
|
||||
runesToVal runesToVal
|
||||
}
|
||||
|
||||
type runesToVal func([]rune) (interface{}, error)
|
||||
|
||||
func newParser(sc *bytes.Buffer, data map[string]interface{}, stringBool bool) *parser {
|
||||
rs2v := func(rs []rune) (interface{}, error) {
|
||||
return typedVal(rs, stringBool), nil
|
||||
}
|
||||
return &parser{sc: sc, data: data, runesToVal: rs2v}
|
||||
}
|
||||
|
||||
func newFileParser(sc *bytes.Buffer, data map[string]interface{}, runesToVal runesToVal) *parser {
|
||||
return &parser{sc: sc, data: data, runesToVal: runesToVal}
|
||||
}
|
||||
|
||||
func (t *parser) parse() error {
|
||||
for {
|
||||
err := t.key(t.data)
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func runeSet(r []rune) map[rune]bool {
|
||||
s := make(map[rune]bool, len(r))
|
||||
for _, rr := range r {
|
||||
s[rr] = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (t *parser) key(data map[string]interface{}) error {
|
||||
stop := runeSet([]rune{'=', '[', ',', '.'})
|
||||
for {
|
||||
switch k, last, err := runesUntil(t.sc, stop); {
|
||||
case err != nil:
|
||||
if len(k) == 0 {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("key %q has no value", string(k))
|
||||
//set(data, string(k), "")
|
||||
//return err
|
||||
case last == '[':
|
||||
// We are in a list index context, so we need to set an index.
|
||||
i, err := t.keyIndex()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing index: %s", err)
|
||||
}
|
||||
kk := string(k)
|
||||
// Find or create target list
|
||||
list := []interface{}{}
|
||||
if _, ok := data[kk]; ok {
|
||||
list = data[kk].([]interface{})
|
||||
}
|
||||
|
||||
// Now we need to get the value after the ].
|
||||
list, err = t.listItem(list, i)
|
||||
set(data, kk, list)
|
||||
return err
|
||||
case last == '=':
|
||||
//End of key. Consume =, Get value.
|
||||
// FIXME: Get value list first
|
||||
vl, e := t.valList()
|
||||
switch e {
|
||||
case nil:
|
||||
set(data, string(k), vl)
|
||||
return nil
|
||||
case io.EOF:
|
||||
set(data, string(k), "")
|
||||
return e
|
||||
case ErrNotList:
|
||||
rs, e := t.val()
|
||||
if e != nil && e != io.EOF {
|
||||
return e
|
||||
}
|
||||
v, e := t.runesToVal(rs)
|
||||
set(data, string(k), v)
|
||||
return e
|
||||
default:
|
||||
return e
|
||||
}
|
||||
|
||||
case last == ',':
|
||||
// No value given. Set the value to empty string. Return error.
|
||||
set(data, string(k), "")
|
||||
return fmt.Errorf("key %q has no value (cannot end with ,)", string(k))
|
||||
case last == '.':
|
||||
// First, create or find the target map.
|
||||
inner := map[string]interface{}{}
|
||||
if _, ok := data[string(k)]; ok {
|
||||
inner = data[string(k)].(map[string]interface{})
|
||||
}
|
||||
|
||||
// Recurse
|
||||
e := t.key(inner)
|
||||
if len(inner) == 0 {
|
||||
return fmt.Errorf("key map %q has no value", string(k))
|
||||
}
|
||||
set(data, string(k), inner)
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func set(data map[string]interface{}, key string, val interface{}) {
|
||||
// If key is empty, don't set it.
|
||||
if len(key) == 0 {
|
||||
return
|
||||
}
|
||||
data[key] = val
|
||||
}
|
||||
|
||||
func setIndex(list []interface{}, index int, val interface{}) (l2 []interface{}, err error) {
|
||||
// There are possible index values that are out of range on a target system
|
||||
// causing a panic. This will catch the panic and return an error instead.
|
||||
// The value of the index that causes a panic varies from system to system.
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("error processing index %d: %s", index, r)
|
||||
}
|
||||
}()
|
||||
|
||||
if index < 0 {
|
||||
return list, fmt.Errorf("negative %d index not allowed", index)
|
||||
}
|
||||
if index > MaxIndex {
|
||||
return list, fmt.Errorf("index of %d is greater than maximum supported index of %d", index, MaxIndex)
|
||||
}
|
||||
if len(list) <= index {
|
||||
newlist := make([]interface{}, index+1)
|
||||
copy(newlist, list)
|
||||
list = newlist
|
||||
}
|
||||
list[index] = val
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (t *parser) keyIndex() (int, error) {
|
||||
// First, get the key.
|
||||
stop := runeSet([]rune{']'})
|
||||
v, _, err := runesUntil(t.sc, stop)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// v should be the index
|
||||
return strconv.Atoi(string(v))
|
||||
|
||||
}
|
||||
func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) {
|
||||
if i < 0 {
|
||||
return list, fmt.Errorf("negative %d index not allowed", i)
|
||||
}
|
||||
stop := runeSet([]rune{'[', '.', '='})
|
||||
switch k, last, err := runesUntil(t.sc, stop); {
|
||||
case len(k) > 0:
|
||||
return list, fmt.Errorf("unexpected data at end of array index: %q", k)
|
||||
case err != nil:
|
||||
return list, err
|
||||
case last == '=':
|
||||
vl, e := t.valList()
|
||||
switch e {
|
||||
case nil:
|
||||
return setIndex(list, i, vl)
|
||||
case io.EOF:
|
||||
return setIndex(list, i, "")
|
||||
case ErrNotList:
|
||||
rs, e := t.val()
|
||||
if e != nil && e != io.EOF {
|
||||
return list, e
|
||||
}
|
||||
v, e := t.runesToVal(rs)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
return setIndex(list, i, v)
|
||||
default:
|
||||
return list, e
|
||||
}
|
||||
case last == '[':
|
||||
// now we have a nested list. Read the index and handle.
|
||||
i, err := t.keyIndex()
|
||||
if err != nil {
|
||||
return list, fmt.Errorf("error parsing index: %s", err)
|
||||
}
|
||||
// Now we need to get the value after the ].
|
||||
list2, err := t.listItem(list, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return setIndex(list, i, list2)
|
||||
case last == '.':
|
||||
// We have a nested object. Send to t.key
|
||||
inner := map[string]interface{}{}
|
||||
if len(list) > i {
|
||||
var ok bool
|
||||
inner, ok = list[i].(map[string]interface{})
|
||||
if !ok {
|
||||
// We have indices out of order. Initialize empty value.
|
||||
list[i] = map[string]interface{}{}
|
||||
inner = list[i].(map[string]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse
|
||||
e := t.key(inner)
|
||||
if e != nil {
|
||||
return list, e
|
||||
}
|
||||
return setIndex(list, i, inner)
|
||||
default:
|
||||
return nil, fmt.Errorf("parse error: unexpected token %v", last)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *parser) val() ([]rune, error) {
|
||||
stop := runeSet([]rune{','})
|
||||
v, _, err := runesUntil(t.sc, stop)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (t *parser) valList() ([]interface{}, error) {
|
||||
r, _, e := t.sc.ReadRune()
|
||||
if e != nil {
|
||||
return []interface{}{}, e
|
||||
}
|
||||
|
||||
if r != '{' {
|
||||
e = t.sc.UnreadRune()
|
||||
if e != nil {
|
||||
return []interface{}{}, e
|
||||
}
|
||||
return []interface{}{}, ErrNotList
|
||||
}
|
||||
|
||||
list := []interface{}{}
|
||||
stop := runeSet([]rune{',', '}'})
|
||||
for {
|
||||
switch rs, last, err := runesUntil(t.sc, stop); {
|
||||
case err != nil:
|
||||
if err == io.EOF {
|
||||
err = errors.New("list must terminate with '}'")
|
||||
}
|
||||
return list, err
|
||||
case last == '}':
|
||||
// If this is followed by ',', consume it.
|
||||
if r, _, e := t.sc.ReadRune(); e == nil && r != ',' {
|
||||
e = t.sc.UnreadRune()
|
||||
if e != nil {
|
||||
return []interface{}{}, e
|
||||
}
|
||||
}
|
||||
v, e := t.runesToVal(rs)
|
||||
list = append(list, v)
|
||||
return list, e
|
||||
case last == ',':
|
||||
v, e := t.runesToVal(rs)
|
||||
if e != nil {
|
||||
return list, e
|
||||
}
|
||||
list = append(list, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runesUntil(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
|
||||
var v []rune
|
||||
for {
|
||||
switch r, _, e := in.ReadRune(); {
|
||||
case e != nil:
|
||||
return v, r, e
|
||||
case inMap(r, stop):
|
||||
return v, r, nil
|
||||
case r == '\\':
|
||||
next, _, e := in.ReadRune()
|
||||
if e != nil {
|
||||
return v, next, e
|
||||
}
|
||||
v = append(v, next)
|
||||
default:
|
||||
v = append(v, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inMap(k rune, m map[rune]bool) bool {
|
||||
_, ok := m[k]
|
||||
return ok
|
||||
}
|
||||
|
||||
func typedVal(v []rune, st bool) interface{} {
|
||||
val := string(v)
|
||||
|
||||
if st {
|
||||
return val
|
||||
}
|
||||
|
||||
if strings.EqualFold(val, "true") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.EqualFold(val, "false") {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.EqualFold(val, "null") {
|
||||
return struct{}{}
|
||||
}
|
||||
|
||||
if strings.EqualFold(val, "0") {
|
||||
return int64(0)
|
||||
}
|
||||
|
||||
// If this value does not start with zero, try parsing it to an int
|
||||
if len(val) != 0 && val[0] != '0' {
|
||||
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||
return iv
|
||||
}
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
+93
@@ -7,6 +7,13 @@ package uuid
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
BILLION = 1000000000
|
||||
)
|
||||
|
||||
// New Create a version 4 random UUID
|
||||
@@ -20,3 +27,89 @@ func New(r io.Reader) (string, error) {
|
||||
bs[6] = bs[6]&^0xf0 | 0x40
|
||||
return fmt.Sprintf("%x-%x-%x-%x-%x", bs[0:4], bs[4:6], bs[6:8], bs[8:10], bs[10:]), nil
|
||||
}
|
||||
|
||||
// Parse will use the google/uuid library to parse the string into a uuid
|
||||
// if parsing fails, it will return an empty map. It will fill the map
|
||||
// with some decoded values with fillMap
|
||||
// ref: https://datatracker.ietf.org/doc/html/rfc4122
|
||||
func Parse(s string) (map[string]interface{}, error) {
|
||||
uuid, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make(map[string]interface{}, getVersionLen(int(uuid.Version())))
|
||||
fillMap(out, uuid)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Fills the map with values from the uuid. Version and variant for every version.
|
||||
// Version 1-2 has decodable values that could be of use, version 4 is random,
|
||||
// and version 3,5 is not feasible to extract data. Generated with either MD5 or SHA1 hash
|
||||
// ref: https://datatracker.ietf.org/doc/html/rfc4122 about creation of UUIDs
|
||||
func fillMap(m map[string]interface{}, u uuid.UUID) {
|
||||
m["version"] = int(u.Version())
|
||||
m["variant"] = u.Variant().String()
|
||||
switch version := m["version"]; version {
|
||||
case 1, 2:
|
||||
m["time"] = nanoUnix(u.Time())
|
||||
m["nodeid"] = byteDecimalToHexMAC(u.NodeID(), "-")
|
||||
m["macvariables"] = macVars(u.NodeID()[0])
|
||||
m["clocksequence"] = u.ClockSequence()
|
||||
if version == 2 {
|
||||
m["id"] = int(u.ID())
|
||||
m["domain"] = u.Domain().String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// macVars will take the first byte of a MAC-address and check for the
|
||||
// local/global bit and check for the unicast/multicast bit of the byte,
|
||||
// and return a string with this info.
|
||||
// ref: https://datatracker.ietf.org/doc/html/rfc7042#section-2.1
|
||||
func macVars(inpb byte) string {
|
||||
switch {
|
||||
case inpb&byte(0b11) == byte(0b11):
|
||||
return "local:multicast"
|
||||
case inpb&byte(0b01) == byte(0b01):
|
||||
return "global:multicast"
|
||||
case inpb&byte(0b10) == byte(0b10):
|
||||
return "local:unicast"
|
||||
}
|
||||
return "global:unicast"
|
||||
}
|
||||
|
||||
// loops through the byte array to convert all bytes to hexes.
|
||||
// It will also put the separator between every other to make it human-readable
|
||||
func byteDecimalToHexMAC(bytes []byte, sep string) string {
|
||||
hexs := strings.Builder{}
|
||||
l := len(bytes)
|
||||
hexs.Grow((l * 3) - 1) // 1 byte -> 2 hexes + 1 separator (if one char)
|
||||
|
||||
for i, b := range bytes {
|
||||
hexs.WriteString(fmt.Sprintf("%02x", b))
|
||||
if i < l-1 {
|
||||
hexs.WriteString(sep)
|
||||
}
|
||||
}
|
||||
|
||||
return hexs.String()
|
||||
}
|
||||
|
||||
// nanoUnix Converts the uuids encoded time into unix represented time in nanoseconds
|
||||
func nanoUnix(t uuid.Time) int64 {
|
||||
unixsec, unixnsec := t.UnixTime()
|
||||
return unixsec*BILLION + unixnsec
|
||||
}
|
||||
|
||||
// Helper function to make map with length based on version of uuid
|
||||
// Most are 2 in length (version, variant), but version 1 and 2 have more.
|
||||
func getVersionLen(version int) int {
|
||||
switch version {
|
||||
case 1:
|
||||
return 5
|
||||
case 2:
|
||||
return 7
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright 2023 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 extension
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var pluginMtx sync.Mutex
|
||||
var bundleExtensions map[string]Handler
|
||||
|
||||
// Handler is used to unmarshal a byte slice of a registered extension
|
||||
// EXPERIMENTAL: Please don't rely on this functionality, it may go
|
||||
// away or change in the future.
|
||||
type Handler func([]byte, any) error
|
||||
|
||||
// RegisterExtension registers a Handler for a certain file extension, including
|
||||
// the dot: ".json", not "json".
|
||||
// EXPERIMENTAL: Please don't rely on this functionality, it may go
|
||||
// away or change in the future.
|
||||
func RegisterExtension(name string, handler Handler) {
|
||||
pluginMtx.Lock()
|
||||
defer pluginMtx.Unlock()
|
||||
|
||||
if bundleExtensions == nil {
|
||||
bundleExtensions = map[string]Handler{}
|
||||
}
|
||||
bundleExtensions[name] = handler
|
||||
}
|
||||
|
||||
// FindExtension ios used to look up a registered extension Handler
|
||||
// EXPERIMENTAL: Please don't rely on this functionality, it may go
|
||||
// away or change in the future.
|
||||
func FindExtension(ext string) Handler {
|
||||
pluginMtx.Lock()
|
||||
defer pluginMtx.Unlock()
|
||||
return bundleExtensions[ext]
|
||||
}
|
||||
+69
-34
@@ -8,15 +8,17 @@ package loader
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
astJSON "github.com/open-policy-agent/opa/ast/json"
|
||||
"github.com/open-policy-agent/opa/bundle"
|
||||
fileurl "github.com/open-policy-agent/opa/internal/file/url"
|
||||
"github.com/open-policy-agent/opa/internal/merge"
|
||||
@@ -91,6 +93,7 @@ type FileLoader interface {
|
||||
All(paths []string) (*Result, error)
|
||||
Filtered(paths []string, filter Filter) (*Result, error)
|
||||
AsBundle(path string) (*bundle.Bundle, error)
|
||||
WithReader(io.Reader) FileLoader
|
||||
WithFS(fs.FS) FileLoader
|
||||
WithMetrics(metrics.Metrics) FileLoader
|
||||
WithFilter(Filter) FileLoader
|
||||
@@ -98,6 +101,9 @@ type FileLoader interface {
|
||||
WithSkipBundleVerification(bool) FileLoader
|
||||
WithProcessAnnotation(bool) FileLoader
|
||||
WithCapabilities(*ast.Capabilities) FileLoader
|
||||
WithJSONOptions(*astJSON.Options) FileLoader
|
||||
|
||||
WithRegoV1Compatible(bool) FileLoader
|
||||
}
|
||||
|
||||
// NewFileLoader returns a new FileLoader instance.
|
||||
@@ -116,6 +122,7 @@ type fileLoader struct {
|
||||
files map[string]bundle.FileInfo
|
||||
opts ast.ParserOptions
|
||||
fsys fs.FS
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// WithFS provides an fs.FS to use for loading files. You can pass nil to
|
||||
@@ -126,6 +133,14 @@ func (fl *fileLoader) WithFS(fsys fs.FS) FileLoader {
|
||||
return fl
|
||||
}
|
||||
|
||||
// WithReader provides an io.Reader to use for loading the bundle tarball.
|
||||
// An io.Reader passed via WithReader takes precedence over an fs.FS passed
|
||||
// via WithFS.
|
||||
func (fl *fileLoader) WithReader(rdr io.Reader) FileLoader {
|
||||
fl.reader = rdr
|
||||
return fl
|
||||
}
|
||||
|
||||
// WithMetrics provides the metrics instance to use while loading
|
||||
func (fl *fileLoader) WithMetrics(m metrics.Metrics) FileLoader {
|
||||
fl.metrics = m
|
||||
@@ -162,6 +177,17 @@ func (fl *fileLoader) WithCapabilities(caps *ast.Capabilities) FileLoader {
|
||||
return fl
|
||||
}
|
||||
|
||||
// WithJSONOptions sets the JSONOptions for use when parsing files
|
||||
func (fl *fileLoader) WithJSONOptions(opts *astJSON.Options) FileLoader {
|
||||
fl.opts.JSONOptions = opts
|
||||
return fl
|
||||
}
|
||||
|
||||
func (fl *fileLoader) WithRegoV1Compatible(compatible bool) FileLoader {
|
||||
fl.opts.RegoV1Compatible = compatible
|
||||
return fl
|
||||
}
|
||||
|
||||
// All returns a Result object loaded (recursively) from the specified paths.
|
||||
func (fl fileLoader) All(paths []string) (*Result, error) {
|
||||
return fl.Filtered(paths, nil)
|
||||
@@ -212,7 +238,14 @@ func (fl fileLoader) AsBundle(path string) (*bundle.Bundle, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bundleLoader, isDir, err := GetBundleDirectoryLoaderWithFilter(path, fl.filter)
|
||||
|
||||
var bundleLoader bundle.DirectoryLoader
|
||||
var isDir bool
|
||||
if fl.reader != nil {
|
||||
bundleLoader = bundle.NewTarballLoaderWithBaseURL(fl.reader, path).WithFilter(fl.filter)
|
||||
} else {
|
||||
bundleLoader, isDir, err = GetBundleDirectoryLoaderFS(fl.fsys, path, fl.filter)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -222,7 +255,8 @@ func (fl fileLoader) AsBundle(path string) (*bundle.Bundle, error) {
|
||||
WithBundleVerificationConfig(fl.bvc).
|
||||
WithSkipBundleVerification(fl.skipVerify).
|
||||
WithProcessAnnotations(fl.opts.ProcessAnnotation).
|
||||
WithCapabilities(fl.opts.Capabilities)
|
||||
WithCapabilities(fl.opts.Capabilities).
|
||||
WithJSONOptions(fl.opts.JSONOptions)
|
||||
|
||||
// For bundle directories add the full path in front of module file names
|
||||
// to simplify debugging.
|
||||
@@ -239,55 +273,57 @@ func (fl fileLoader) AsBundle(path string) (*bundle.Bundle, error) {
|
||||
}
|
||||
|
||||
// GetBundleDirectoryLoader returns a bundle directory loader which can be used to load
|
||||
// files in the directory.
|
||||
// files in the directory
|
||||
func GetBundleDirectoryLoader(path string) (bundle.DirectoryLoader, bool, error) {
|
||||
path, err := fileurl.Clean(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error reading %q: %s", path, err)
|
||||
}
|
||||
|
||||
var bundleLoader bundle.DirectoryLoader
|
||||
|
||||
if fi.IsDir() {
|
||||
bundleLoader = bundle.NewDirectoryLoader(path)
|
||||
} else {
|
||||
fh, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
bundleLoader = bundle.NewTarballLoaderWithBaseURL(fh, path)
|
||||
}
|
||||
return bundleLoader, fi.IsDir(), nil
|
||||
return GetBundleDirectoryLoaderFS(nil, path, nil)
|
||||
}
|
||||
|
||||
// GetBundleDirectoryLoaderWithFilter returns a bundle directory loader which can be used to load
|
||||
// files in the directory after applying the given filter.
|
||||
func GetBundleDirectoryLoaderWithFilter(path string, filter Filter) (bundle.DirectoryLoader, bool, error) {
|
||||
return GetBundleDirectoryLoaderFS(nil, path, filter)
|
||||
}
|
||||
|
||||
// GetBundleDirectoryLoaderFS returns a bundle directory loader which can be used to load
|
||||
// files in the directory.
|
||||
func GetBundleDirectoryLoaderFS(fsys fs.FS, path string, filter Filter) (bundle.DirectoryLoader, bool, error) {
|
||||
path, err := fileurl.Clean(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
fi, err := os.Stat(path)
|
||||
var fi fs.FileInfo
|
||||
if fsys != nil {
|
||||
fi, err = fs.Stat(fsys, path)
|
||||
} else {
|
||||
fi, err = os.Stat(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("error reading %q: %s", path, err)
|
||||
}
|
||||
|
||||
var bundleLoader bundle.DirectoryLoader
|
||||
|
||||
if fi.IsDir() {
|
||||
bundleLoader = bundle.NewDirectoryLoader(path).WithFilter(filter)
|
||||
if fsys != nil {
|
||||
bundleLoader = bundle.NewFSLoaderWithRoot(fsys, path)
|
||||
} else {
|
||||
bundleLoader = bundle.NewDirectoryLoader(path)
|
||||
}
|
||||
} else {
|
||||
fh, err := os.Open(path)
|
||||
var fh fs.File
|
||||
if fsys != nil {
|
||||
fh, err = fsys.Open(path)
|
||||
} else {
|
||||
fh, err = os.Open(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
bundleLoader = bundle.NewTarballLoaderWithBaseURL(fh, path).WithFilter(filter)
|
||||
bundleLoader = bundle.NewTarballLoaderWithBaseURL(fh, path)
|
||||
}
|
||||
|
||||
if filter != nil {
|
||||
bundleLoader = bundleLoader.WithFilter(filter)
|
||||
}
|
||||
return bundleLoader, fi.IsDir(), nil
|
||||
}
|
||||
@@ -721,11 +757,10 @@ func loadRego(path string, bs []byte, m metrics.Metrics, opts ast.ParserOptions)
|
||||
|
||||
func loadJSON(path string, bs []byte, m metrics.Metrics) (interface{}, error) {
|
||||
m.Timer(metrics.RegoDataParse).Start()
|
||||
buf := bytes.NewBuffer(bs)
|
||||
decoder := util.NewJSONDecoder(buf)
|
||||
var x interface{}
|
||||
err := decoder.Decode(&x)
|
||||
err := util.UnmarshalJSON(bs, &x)
|
||||
m.Timer(metrics.RegoDataParse).Stop()
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", path, err)
|
||||
}
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Level log level for Logger
|
||||
type Level uint8
|
||||
|
||||
const (
|
||||
// Error error log level
|
||||
Error Level = iota
|
||||
// Warn warn log level
|
||||
Warn
|
||||
// Info info log level
|
||||
Info
|
||||
// Debug debug log level
|
||||
Debug
|
||||
)
|
||||
|
||||
// Logger provides interface for OPA logger implementations
|
||||
type Logger interface {
|
||||
Debug(fmt string, a ...interface{})
|
||||
Info(fmt string, a ...interface{})
|
||||
Error(fmt string, a ...interface{})
|
||||
Warn(fmt string, a ...interface{})
|
||||
|
||||
WithFields(map[string]interface{}) Logger
|
||||
|
||||
GetLevel() Level
|
||||
SetLevel(Level)
|
||||
}
|
||||
|
||||
// StandardLogger is the default OPA logger implementation.
|
||||
type StandardLogger struct {
|
||||
logger *logrus.Logger
|
||||
fields map[string]interface{}
|
||||
}
|
||||
|
||||
// New returns a new standard logger.
|
||||
func New() *StandardLogger {
|
||||
return &StandardLogger{
|
||||
logger: logrus.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get returns the standard logger used throughout OPA.
|
||||
//
|
||||
// Deprecated. Do not rely on the global logger.
|
||||
func Get() *StandardLogger {
|
||||
return &StandardLogger{
|
||||
logger: logrus.StandardLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetOutput sets the underlying logrus output.
|
||||
func (l *StandardLogger) SetOutput(w io.Writer) {
|
||||
l.logger.SetOutput(w)
|
||||
}
|
||||
|
||||
// SetFormatter sets the underlying logrus formatter.
|
||||
func (l *StandardLogger) SetFormatter(formatter logrus.Formatter) {
|
||||
l.logger.SetFormatter(formatter)
|
||||
}
|
||||
|
||||
// WithFields provides additional fields to include in log output
|
||||
func (l *StandardLogger) WithFields(fields map[string]interface{}) Logger {
|
||||
cp := *l
|
||||
cp.fields = make(map[string]interface{})
|
||||
for k, v := range l.fields {
|
||||
cp.fields[k] = v
|
||||
}
|
||||
for k, v := range fields {
|
||||
cp.fields[k] = v
|
||||
}
|
||||
return &cp
|
||||
}
|
||||
|
||||
// getFields returns additional fields of this logger
|
||||
func (l *StandardLogger) getFields() map[string]interface{} {
|
||||
return l.fields
|
||||
}
|
||||
|
||||
// SetLevel sets the standard logger level.
|
||||
func (l *StandardLogger) SetLevel(level Level) {
|
||||
var logrusLevel logrus.Level
|
||||
switch level {
|
||||
case Error: // set logging level report Warn or higher (includes Error)
|
||||
logrusLevel = logrus.WarnLevel
|
||||
case Warn:
|
||||
logrusLevel = logrus.WarnLevel
|
||||
case Info:
|
||||
logrusLevel = logrus.InfoLevel
|
||||
case Debug:
|
||||
logrusLevel = logrus.DebugLevel
|
||||
default:
|
||||
l.Warn("unknown log level %v", level)
|
||||
logrusLevel = logrus.InfoLevel
|
||||
}
|
||||
|
||||
l.logger.SetLevel(logrusLevel)
|
||||
}
|
||||
|
||||
// GetLevel returns the standard logger level.
|
||||
func (l *StandardLogger) GetLevel() Level {
|
||||
logrusLevel := l.logger.GetLevel()
|
||||
|
||||
var level Level
|
||||
switch logrusLevel {
|
||||
case logrus.WarnLevel:
|
||||
level = Error
|
||||
case logrus.InfoLevel:
|
||||
level = Info
|
||||
case logrus.DebugLevel:
|
||||
level = Debug
|
||||
default:
|
||||
l.Warn("unknown log level %v", logrusLevel)
|
||||
level = Info
|
||||
}
|
||||
|
||||
return level
|
||||
}
|
||||
|
||||
// Debug logs at debug level
|
||||
func (l *StandardLogger) Debug(fmt string, a ...interface{}) {
|
||||
l.logger.WithFields(l.getFields()).Debugf(fmt, a...)
|
||||
}
|
||||
|
||||
// Info logs at info level
|
||||
func (l *StandardLogger) Info(fmt string, a ...interface{}) {
|
||||
l.logger.WithFields(l.getFields()).Infof(fmt, a...)
|
||||
}
|
||||
|
||||
// Error logs at error level
|
||||
func (l *StandardLogger) Error(fmt string, a ...interface{}) {
|
||||
l.logger.WithFields(l.getFields()).Errorf(fmt, a...)
|
||||
}
|
||||
|
||||
// Warn logs at warn level
|
||||
func (l *StandardLogger) Warn(fmt string, a ...interface{}) {
|
||||
l.logger.WithFields(l.getFields()).Warnf(fmt, a...)
|
||||
}
|
||||
|
||||
// NoOpLogger logging implementation that does nothing
|
||||
type NoOpLogger struct {
|
||||
level Level
|
||||
fields map[string]interface{}
|
||||
}
|
||||
|
||||
// NewNoOpLogger instantiates new NoOpLogger
|
||||
func NewNoOpLogger() *NoOpLogger {
|
||||
return &NoOpLogger{
|
||||
level: Info,
|
||||
}
|
||||
}
|
||||
|
||||
// WithFields provides additional fields to include in log output.
|
||||
// Implemented here primarily to be able to switch between implementations without loss of data.
|
||||
func (l *NoOpLogger) WithFields(fields map[string]interface{}) Logger {
|
||||
cp := *l
|
||||
cp.fields = fields
|
||||
return &cp
|
||||
}
|
||||
|
||||
// Debug noop
|
||||
func (*NoOpLogger) Debug(string, ...interface{}) {}
|
||||
|
||||
// Info noop
|
||||
func (*NoOpLogger) Info(string, ...interface{}) {}
|
||||
|
||||
// Error noop
|
||||
func (*NoOpLogger) Error(string, ...interface{}) {}
|
||||
|
||||
// Warn noop
|
||||
func (*NoOpLogger) Warn(string, ...interface{}) {}
|
||||
|
||||
// SetLevel set log level
|
||||
func (l *NoOpLogger) SetLevel(level Level) {
|
||||
l.level = level
|
||||
}
|
||||
|
||||
// GetLevel get log level
|
||||
func (l *NoOpLogger) GetLevel() Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
type requestContextKey string
|
||||
|
||||
const reqCtxKey = requestContextKey("request-context-key")
|
||||
|
||||
// RequestContext represents the request context used to store data
|
||||
// related to the request that could be used on logs.
|
||||
type RequestContext struct {
|
||||
ClientAddr string
|
||||
ReqID uint64
|
||||
ReqMethod string
|
||||
ReqPath string
|
||||
}
|
||||
|
||||
// Fields adapts the RequestContext fields to logrus.Fields.
|
||||
func (rctx RequestContext) Fields() logrus.Fields {
|
||||
return logrus.Fields{
|
||||
"client_addr": rctx.ClientAddr,
|
||||
"req_id": rctx.ReqID,
|
||||
"req_method": rctx.ReqMethod,
|
||||
"req_path": rctx.ReqPath,
|
||||
}
|
||||
}
|
||||
|
||||
// NewContext returns a copy of parent with an associated RequestContext.
|
||||
func NewContext(parent context.Context, val *RequestContext) context.Context {
|
||||
return context.WithValue(parent, reqCtxKey, val)
|
||||
}
|
||||
|
||||
// FromContext returns the RequestContext associated with ctx, if any.
|
||||
func FromContext(ctx context.Context) (*RequestContext, bool) {
|
||||
requestContext, ok := ctx.Value(reqCtxKey).(*RequestContext)
|
||||
return requestContext, ok
|
||||
}
|
||||
|
||||
const decisionCtxKey = requestContextKey("decision_id")
|
||||
|
||||
func WithDecisionID(parent context.Context, id string) context.Context {
|
||||
return context.WithValue(parent, decisionCtxKey, id)
|
||||
}
|
||||
|
||||
func DecisionIDFromContext(ctx context.Context) (string, bool) {
|
||||
s, ok := ctx.Value(decisionCtxKey).(string)
|
||||
return s, ok
|
||||
}
|
||||
+984
@@ -0,0 +1,984 @@
|
||||
// Copyright 2018 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 plugins implements plugin management for the policy engine.
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/bundle"
|
||||
"github.com/open-policy-agent/opa/config"
|
||||
"github.com/open-policy-agent/opa/hooks"
|
||||
bundleUtils "github.com/open-policy-agent/opa/internal/bundle"
|
||||
cfg "github.com/open-policy-agent/opa/internal/config"
|
||||
"github.com/open-policy-agent/opa/internal/errors"
|
||||
initload "github.com/open-policy-agent/opa/internal/runtime/init"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/loader"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
"github.com/open-policy-agent/opa/plugins/rest"
|
||||
"github.com/open-policy-agent/opa/resolver/wasm"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
"github.com/open-policy-agent/opa/topdown/cache"
|
||||
"github.com/open-policy-agent/opa/topdown/print"
|
||||
"github.com/open-policy-agent/opa/tracing"
|
||||
)
|
||||
|
||||
// Factory defines the interface OPA uses to instantiate your plugin.
|
||||
//
|
||||
// When OPA processes it's configuration it looks for factories that
|
||||
// have been registered by calling runtime.RegisterPlugin. Factories
|
||||
// are registered to a name which is used to key into the
|
||||
// configuration blob. If your plugin has not been configured, your
|
||||
// factory will not be invoked.
|
||||
//
|
||||
// plugins:
|
||||
// my_plugin1:
|
||||
// some_key: foo
|
||||
// # my_plugin2:
|
||||
// # some_key2: bar
|
||||
//
|
||||
// If OPA was started with the configuration above and received two
|
||||
// calls to runtime.RegisterPlugins (one with NAME "my_plugin1" and
|
||||
// one with NAME "my_plugin2"), it would only invoke the factory for
|
||||
// for my_plugin1.
|
||||
//
|
||||
// OPA instantiates and reconfigures plugins in two steps. First, OPA
|
||||
// will call Validate to check the configuration. Assuming the
|
||||
// configuration is valid, your factory should return a configuration
|
||||
// value that can be used to construct your plugin. Second, OPA will
|
||||
// call New to instantiate your plugin providing the configuration
|
||||
// value returned from the Validate call.
|
||||
//
|
||||
// Validate receives a slice of bytes representing plugin
|
||||
// configuration and returns a configuration value that can be used to
|
||||
// instantiate your plugin. The manager is provided to give access to
|
||||
// the OPA's compiler, storage layer, and global configuration. Your
|
||||
// Validate function will typically:
|
||||
//
|
||||
// 1. Deserialize the raw config bytes
|
||||
// 2. Validate the deserialized config for semantic errors
|
||||
// 3. Inject default values
|
||||
// 4. Return a deserialized/parsed config
|
||||
//
|
||||
// New receives a valid configuration for your plugin and returns a
|
||||
// plugin object. Your New function will typically:
|
||||
//
|
||||
// 1. Cast the config value to it's own type
|
||||
// 2. Instantiate a plugin object
|
||||
// 3. Return the plugin object
|
||||
// 4. Update status via `plugins.Manager#UpdatePluginStatus`
|
||||
//
|
||||
// After a plugin has been created subsequent status updates can be
|
||||
// send anytime the plugin enters a ready or error state.
|
||||
type Factory interface {
|
||||
Validate(manager *Manager, config []byte) (interface{}, error)
|
||||
New(manager *Manager, config interface{}) Plugin
|
||||
}
|
||||
|
||||
// Plugin defines the interface OPA uses to manage your plugin.
|
||||
//
|
||||
// When OPA starts it will start all of the plugins it was configured
|
||||
// to instantiate. Each time a new plugin is configured (via
|
||||
// discovery), OPA will start it. You can use the Start call to spawn
|
||||
// additional goroutines or perform initialization tasks.
|
||||
//
|
||||
// Currently OPA will not call Stop on plugins.
|
||||
//
|
||||
// When OPA receives new configuration for your plugin via discovery
|
||||
// it will first Validate the configuration using your factory and
|
||||
// then call Reconfigure.
|
||||
type Plugin interface {
|
||||
Start(ctx context.Context) error
|
||||
Stop(ctx context.Context)
|
||||
Reconfigure(ctx context.Context, config interface{})
|
||||
}
|
||||
|
||||
// Triggerable defines the interface plugins use for manual plugin triggers.
|
||||
type Triggerable interface {
|
||||
Trigger(context.Context) error
|
||||
}
|
||||
|
||||
// State defines the state that a Plugin instance is currently
|
||||
// in with pre-defined states.
|
||||
type State string
|
||||
|
||||
const (
|
||||
// StateNotReady indicates that the Plugin is not in an error state, but isn't
|
||||
// ready for normal operation yet. This should only happen at
|
||||
// initialization time.
|
||||
StateNotReady State = "NOT_READY"
|
||||
|
||||
// StateOK signifies that the Plugin is operating normally.
|
||||
StateOK State = "OK"
|
||||
|
||||
// StateErr indicates that the Plugin is in an error state and should not
|
||||
// be considered as functional.
|
||||
StateErr State = "ERROR"
|
||||
|
||||
// StateWarn indicates the Plugin is operating, but in a potentially dangerous or
|
||||
// degraded state. It may be used to indicate manual remediation is needed, or to
|
||||
// alert admins of some other noteworthy state.
|
||||
StateWarn State = "WARN"
|
||||
)
|
||||
|
||||
// TriggerMode defines the trigger mode utilized by a Plugin for bundle download,
|
||||
// log upload etc.
|
||||
type TriggerMode string
|
||||
|
||||
const (
|
||||
// TriggerPeriodic represents periodic polling mechanism
|
||||
TriggerPeriodic TriggerMode = "periodic"
|
||||
|
||||
// TriggerManual represents manual triggering mechanism
|
||||
TriggerManual TriggerMode = "manual"
|
||||
|
||||
// DefaultTriggerMode represents default trigger mechanism
|
||||
DefaultTriggerMode TriggerMode = "periodic"
|
||||
)
|
||||
|
||||
// Status has a Plugin's current status plus an optional Message.
|
||||
type Status struct {
|
||||
State State `json:"state"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Status) String() string {
|
||||
return fmt.Sprintf("{%v %q}", s.State, s.Message)
|
||||
}
|
||||
|
||||
// StatusListener defines a handler to register for status updates.
|
||||
type StatusListener func(status map[string]*Status)
|
||||
|
||||
// Manager implements lifecycle management of plugins and gives plugins access
|
||||
// to engine-wide components like storage.
|
||||
type Manager struct {
|
||||
Store storage.Store
|
||||
Config *config.Config
|
||||
Info *ast.Term
|
||||
ID string
|
||||
|
||||
compiler *ast.Compiler
|
||||
compilerMux sync.RWMutex
|
||||
wasmResolvers []*wasm.Resolver
|
||||
wasmResolversMtx sync.RWMutex
|
||||
services map[string]rest.Client
|
||||
keys map[string]*keys.Config
|
||||
plugins []namedplugin
|
||||
registeredTriggers []func(storage.Transaction)
|
||||
mtx sync.Mutex
|
||||
pluginStatus map[string]*Status
|
||||
pluginStatusListeners map[string]StatusListener
|
||||
initBundles map[string]*bundle.Bundle
|
||||
initFiles loader.Result
|
||||
maxErrors int
|
||||
initialized bool
|
||||
interQueryBuiltinCacheConfig *cache.Config
|
||||
gracefulShutdownPeriod int
|
||||
registeredCacheTriggers []func(*cache.Config)
|
||||
logger logging.Logger
|
||||
consoleLogger logging.Logger
|
||||
serverInitialized chan struct{}
|
||||
serverInitializedOnce sync.Once
|
||||
printHook print.Hook
|
||||
enablePrintStatements bool
|
||||
router *mux.Router
|
||||
prometheusRegister prometheus.Registerer
|
||||
tracerProvider *trace.TracerProvider
|
||||
distributedTacingOpts tracing.Options
|
||||
registeredNDCacheTriggers []func(bool)
|
||||
bootstrapConfigLabels map[string]string
|
||||
hooks hooks.Hooks
|
||||
}
|
||||
|
||||
type managerContextKey string
|
||||
type managerWasmResolverKey string
|
||||
|
||||
const managerCompilerContextKey = managerContextKey("compiler")
|
||||
const managerWasmResolverContextKey = managerWasmResolverKey("wasmResolvers")
|
||||
|
||||
// SetCompilerOnContext puts the compiler into the storage context. Calling this
|
||||
// function before committing updated policies to storage allows the manager to
|
||||
// skip parsing and compiling of modules. Instead, the manager will use the
|
||||
// compiler that was stored on the context.
|
||||
func SetCompilerOnContext(context *storage.Context, compiler *ast.Compiler) {
|
||||
context.Put(managerCompilerContextKey, compiler)
|
||||
}
|
||||
|
||||
// GetCompilerOnContext gets the compiler cached on the storage context.
|
||||
func GetCompilerOnContext(context *storage.Context) *ast.Compiler {
|
||||
compiler, ok := context.Get(managerCompilerContextKey).(*ast.Compiler)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return compiler
|
||||
}
|
||||
|
||||
// SetWasmResolversOnContext puts a set of Wasm Resolvers into the storage
|
||||
// context. Calling this function before committing updated wasm modules to
|
||||
// storage allows the manager to skip initializing modules before using them.
|
||||
// Instead, the manager will use the compiler that was stored on the context.
|
||||
func SetWasmResolversOnContext(context *storage.Context, rs []*wasm.Resolver) {
|
||||
context.Put(managerWasmResolverContextKey, rs)
|
||||
}
|
||||
|
||||
// getWasmResolversOnContext gets the resolvers cached on the storage context.
|
||||
func getWasmResolversOnContext(context *storage.Context) []*wasm.Resolver {
|
||||
resolvers, ok := context.Get(managerWasmResolverContextKey).([]*wasm.Resolver)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return resolvers
|
||||
}
|
||||
|
||||
func validateTriggerMode(mode TriggerMode) error {
|
||||
switch mode {
|
||||
case TriggerPeriodic, TriggerManual:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid trigger mode %q (want %q or %q)", mode, TriggerPeriodic, TriggerManual)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAndInjectDefaultsForTriggerMode validates the trigger mode and injects default values
|
||||
func ValidateAndInjectDefaultsForTriggerMode(a, b *TriggerMode) (*TriggerMode, error) {
|
||||
|
||||
if a == nil && b != nil {
|
||||
err := validateTriggerMode(*b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
} else if a != nil && b == nil {
|
||||
err := validateTriggerMode(*a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
} else if a != nil && b != nil {
|
||||
if *a != *b {
|
||||
return nil, fmt.Errorf("trigger mode mismatch: %s and %s (hint: check discovery configuration)", *a, *b)
|
||||
}
|
||||
err := validateTriggerMode(*a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a, nil
|
||||
|
||||
} else {
|
||||
t := DefaultTriggerMode
|
||||
return &t, nil
|
||||
}
|
||||
}
|
||||
|
||||
type namedplugin struct {
|
||||
name string
|
||||
plugin Plugin
|
||||
}
|
||||
|
||||
// Info sets the runtime information on the manager. The runtime information is
|
||||
// propagated to opa.runtime() built-in function calls.
|
||||
func Info(term *ast.Term) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.Info = term
|
||||
}
|
||||
}
|
||||
|
||||
// InitBundles provides the initial set of bundles to load.
|
||||
func InitBundles(b map[string]*bundle.Bundle) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.initBundles = b
|
||||
}
|
||||
}
|
||||
|
||||
// InitFiles provides the initial set of other data/policy files to load.
|
||||
func InitFiles(f loader.Result) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.initFiles = f
|
||||
}
|
||||
}
|
||||
|
||||
// MaxErrors sets the error limit for the manager's shared compiler.
|
||||
func MaxErrors(n int) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.maxErrors = n
|
||||
}
|
||||
}
|
||||
|
||||
// GracefulShutdownPeriod passes the configured graceful shutdown period to plugins
|
||||
func GracefulShutdownPeriod(gracefulShutdownPeriod int) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.gracefulShutdownPeriod = gracefulShutdownPeriod
|
||||
}
|
||||
}
|
||||
|
||||
// Logger configures the passed logger on the plugin manager (useful to
|
||||
// configure default fields)
|
||||
func Logger(logger logging.Logger) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.logger = logger
|
||||
}
|
||||
}
|
||||
|
||||
// ConsoleLogger sets the passed logger to be used by plugins that are
|
||||
// configured with console logging enabled.
|
||||
func ConsoleLogger(logger logging.Logger) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.consoleLogger = logger
|
||||
}
|
||||
}
|
||||
|
||||
func EnablePrintStatements(yes bool) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.enablePrintStatements = yes
|
||||
}
|
||||
}
|
||||
|
||||
func PrintHook(h print.Hook) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.printHook = h
|
||||
}
|
||||
}
|
||||
|
||||
func WithRouter(r *mux.Router) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.router = r
|
||||
}
|
||||
}
|
||||
|
||||
// WithPrometheusRegister sets the passed prometheus.Registerer to be used by plugins
|
||||
func WithPrometheusRegister(prometheusRegister prometheus.Registerer) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.prometheusRegister = prometheusRegister
|
||||
}
|
||||
}
|
||||
|
||||
// WithTracerProvider sets the passed *trace.TracerProvider to be used by plugins
|
||||
func WithTracerProvider(tracerProvider *trace.TracerProvider) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.tracerProvider = tracerProvider
|
||||
}
|
||||
}
|
||||
|
||||
// WithDistributedTracingOpts sets the options to be used by distributed tracing.
|
||||
func WithDistributedTracingOpts(tr tracing.Options) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.distributedTacingOpts = tr
|
||||
}
|
||||
}
|
||||
|
||||
// WithHooks allows passing hooks to the plugin manager.
|
||||
func WithHooks(hs hooks.Hooks) func(*Manager) {
|
||||
return func(m *Manager) {
|
||||
m.hooks = hs
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Manager using config.
|
||||
func New(raw []byte, id string, store storage.Store, opts ...func(*Manager)) (*Manager, error) {
|
||||
|
||||
parsedConfig, err := config.ParseConfig(raw, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
Store: store,
|
||||
Config: parsedConfig,
|
||||
ID: id,
|
||||
pluginStatus: map[string]*Status{},
|
||||
pluginStatusListeners: map[string]StatusListener{},
|
||||
maxErrors: -1,
|
||||
serverInitialized: make(chan struct{}),
|
||||
bootstrapConfigLabels: parsedConfig.Labels,
|
||||
}
|
||||
|
||||
for _, f := range opts {
|
||||
f(m)
|
||||
}
|
||||
|
||||
if m.logger == nil {
|
||||
m.logger = logging.Get()
|
||||
}
|
||||
|
||||
if m.consoleLogger == nil {
|
||||
m.consoleLogger = logging.New()
|
||||
}
|
||||
|
||||
m.hooks.Each(func(h hooks.Hook) {
|
||||
if f, ok := h.(hooks.ConfigHook); ok {
|
||||
if c, e := f.OnConfig(context.Background(), parsedConfig); e != nil {
|
||||
err = errors.Join(err, e)
|
||||
} else {
|
||||
parsedConfig = c
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// do after options and overrides
|
||||
m.keys, err = keys.ParseKeysConfig(parsedConfig.Keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.interQueryBuiltinCacheConfig, err = cache.ParseCachingConfig(parsedConfig.Caching)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceOpts := cfg.ServiceOptions{
|
||||
Raw: parsedConfig.Services,
|
||||
AuthPlugin: m.AuthPlugin,
|
||||
Keys: m.keys,
|
||||
Logger: m.logger,
|
||||
DistributedTacingOpts: m.distributedTacingOpts,
|
||||
}
|
||||
|
||||
m.services, err = cfg.ParseServicesConfig(serviceOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Init returns an error if the manager could not initialize itself. Init() should
|
||||
// be called before Start(). Init() is idempotent.
|
||||
func (m *Manager) Init(ctx context.Context) error {
|
||||
|
||||
if m.initialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
params := storage.TransactionParams{
|
||||
Write: true,
|
||||
Context: storage.NewContext(),
|
||||
}
|
||||
|
||||
err := storage.Txn(ctx, m.Store, params, func(txn storage.Transaction) error {
|
||||
|
||||
result, err := initload.InsertAndCompile(ctx, initload.InsertAndCompileOptions{
|
||||
Store: m.Store,
|
||||
Txn: txn,
|
||||
Files: m.initFiles,
|
||||
Bundles: m.initBundles,
|
||||
MaxErrors: m.maxErrors,
|
||||
EnablePrintStatements: m.enablePrintStatements,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
SetCompilerOnContext(params.Context, result.Compiler)
|
||||
|
||||
resolvers, err := bundleUtils.LoadWasmResolversFromStore(ctx, m.Store, txn, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SetWasmResolversOnContext(params.Context, resolvers)
|
||||
|
||||
_, err = m.Store.Register(ctx, txn, storage.TriggerConfig{OnCommit: m.onCommit})
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.initialized = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Labels returns the set of labels from the configuration.
|
||||
func (m *Manager) Labels() map[string]string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
return m.Config.Labels
|
||||
}
|
||||
|
||||
// InterQueryBuiltinCacheConfig returns the configuration for the inter-query cache.
|
||||
func (m *Manager) InterQueryBuiltinCacheConfig() *cache.Config {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
return m.interQueryBuiltinCacheConfig
|
||||
}
|
||||
|
||||
// Register adds a plugin to the manager. When the manager is started, all of
|
||||
// the plugins will be started.
|
||||
func (m *Manager) Register(name string, plugin Plugin) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.plugins = append(m.plugins, namedplugin{
|
||||
name: name,
|
||||
plugin: plugin,
|
||||
})
|
||||
if _, ok := m.pluginStatus[name]; !ok {
|
||||
m.pluginStatus[name] = &Status{State: StateNotReady}
|
||||
}
|
||||
}
|
||||
|
||||
// Plugins returns the list of plugins registered with the manager.
|
||||
func (m *Manager) Plugins() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
result := make([]string, len(m.plugins))
|
||||
for i := range m.plugins {
|
||||
result[i] = m.plugins[i].name
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Plugin returns the plugin registered with name or nil if name is not found.
|
||||
func (m *Manager) Plugin(name string) Plugin {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].name == name {
|
||||
return m.plugins[i].plugin
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthPlugin returns the HTTPAuthPlugin registered with name or nil if name is not found.
|
||||
func (m *Manager) AuthPlugin(name string) rest.HTTPAuthPlugin {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
for i := range m.plugins {
|
||||
if m.plugins[i].name == name {
|
||||
return m.plugins[i].plugin.(rest.HTTPAuthPlugin)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCompiler returns the manager's compiler.
|
||||
func (m *Manager) GetCompiler() *ast.Compiler {
|
||||
m.compilerMux.RLock()
|
||||
defer m.compilerMux.RUnlock()
|
||||
return m.compiler
|
||||
}
|
||||
|
||||
func (m *Manager) setCompiler(compiler *ast.Compiler) {
|
||||
m.compilerMux.Lock()
|
||||
defer m.compilerMux.Unlock()
|
||||
m.compiler = compiler
|
||||
}
|
||||
|
||||
// GetRouter returns the managers router if set
|
||||
func (m *Manager) GetRouter() *mux.Router {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
return m.router
|
||||
}
|
||||
|
||||
// RegisterCompilerTrigger registers for change notifications when the compiler
|
||||
// is changed.
|
||||
func (m *Manager) RegisterCompilerTrigger(f func(storage.Transaction)) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.registeredTriggers = append(m.registeredTriggers, f)
|
||||
}
|
||||
|
||||
// GetWasmResolvers returns the manager's set of Wasm Resolvers.
|
||||
func (m *Manager) GetWasmResolvers() []*wasm.Resolver {
|
||||
m.wasmResolversMtx.RLock()
|
||||
defer m.wasmResolversMtx.RUnlock()
|
||||
return m.wasmResolvers
|
||||
}
|
||||
|
||||
func (m *Manager) setWasmResolvers(rs []*wasm.Resolver) {
|
||||
m.wasmResolversMtx.Lock()
|
||||
defer m.wasmResolversMtx.Unlock()
|
||||
m.wasmResolvers = rs
|
||||
}
|
||||
|
||||
// Start starts the manager. Init() should be called once before Start().
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
|
||||
if m == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !m.initialized {
|
||||
if err := m.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var toStart []Plugin
|
||||
|
||||
func() {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
toStart = make([]Plugin, len(m.plugins))
|
||||
for i := range m.plugins {
|
||||
toStart[i] = m.plugins[i].plugin
|
||||
}
|
||||
}()
|
||||
|
||||
for i := range toStart {
|
||||
if err := toStart[i].Start(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the manager, stopping all the plugins registered with it.
|
||||
// Any plugin that needs to perform cleanup should do so within the duration
|
||||
// of the graceful shutdown period passed with the context as a timeout.
|
||||
// Note that a graceful shutdown period configured with the Manager instance
|
||||
// will override the timeout of the passed in context (if applicable).
|
||||
func (m *Manager) Stop(ctx context.Context) {
|
||||
var toStop []Plugin
|
||||
|
||||
func() {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
toStop = make([]Plugin, len(m.plugins))
|
||||
for i := range m.plugins {
|
||||
toStop[i] = m.plugins[i].plugin
|
||||
}
|
||||
}()
|
||||
|
||||
var cancel context.CancelFunc
|
||||
if m.gracefulShutdownPeriod > 0 {
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(m.gracefulShutdownPeriod)*time.Second)
|
||||
} else {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
}
|
||||
defer cancel()
|
||||
for i := range toStop {
|
||||
toStop[i].Stop(ctx)
|
||||
}
|
||||
if c, ok := m.Store.(interface{ Close(context.Context) error }); ok {
|
||||
if err := c.Close(ctx); err != nil {
|
||||
m.logger.Error("Error closing store: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reconfigure updates the configuration on the manager.
|
||||
func (m *Manager) Reconfigure(config *config.Config) error {
|
||||
opts := cfg.ServiceOptions{
|
||||
Raw: config.Services,
|
||||
AuthPlugin: m.AuthPlugin,
|
||||
Logger: m.logger,
|
||||
DistributedTacingOpts: m.distributedTacingOpts,
|
||||
}
|
||||
|
||||
keys, err := keys.ParseKeysConfig(config.Keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
opts.Keys = keys
|
||||
|
||||
services, err := cfg.ParseServicesConfig(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
interQueryBuiltinCacheConfig, err := cache.ParseCachingConfig(config.Caching)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
|
||||
// don't overwrite existing labels, only allow additions - always based on the boostrap config
|
||||
if config.Labels == nil {
|
||||
config.Labels = m.bootstrapConfigLabels
|
||||
} else {
|
||||
for label, value := range m.bootstrapConfigLabels {
|
||||
config.Labels[label] = value
|
||||
}
|
||||
}
|
||||
|
||||
// don't erase persistence directory
|
||||
if config.PersistenceDirectory == nil {
|
||||
config.PersistenceDirectory = m.Config.PersistenceDirectory
|
||||
}
|
||||
|
||||
m.Config = config
|
||||
m.interQueryBuiltinCacheConfig = interQueryBuiltinCacheConfig
|
||||
for name, client := range services {
|
||||
m.services[name] = client
|
||||
}
|
||||
|
||||
for name, key := range keys {
|
||||
m.keys[name] = key
|
||||
}
|
||||
|
||||
for _, trigger := range m.registeredCacheTriggers {
|
||||
trigger(interQueryBuiltinCacheConfig)
|
||||
}
|
||||
|
||||
for _, trigger := range m.registeredNDCacheTriggers {
|
||||
trigger(config.NDBuiltinCache)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PluginStatus returns the current statuses of any plugins registered.
|
||||
func (m *Manager) PluginStatus() map[string]*Status {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
|
||||
return m.copyPluginStatus()
|
||||
}
|
||||
|
||||
// RegisterPluginStatusListener registers a StatusListener to be
|
||||
// called when plugin status updates occur.
|
||||
func (m *Manager) RegisterPluginStatusListener(name string, listener StatusListener) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
|
||||
m.pluginStatusListeners[name] = listener
|
||||
}
|
||||
|
||||
// UnregisterPluginStatusListener removes a StatusListener registered with the
|
||||
// same name.
|
||||
func (m *Manager) UnregisterPluginStatusListener(name string) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
|
||||
delete(m.pluginStatusListeners, name)
|
||||
}
|
||||
|
||||
// UpdatePluginStatus updates a named plugins status. Any registered
|
||||
// listeners will be called with a copy of the new state of all
|
||||
// plugins.
|
||||
func (m *Manager) UpdatePluginStatus(pluginName string, status *Status) {
|
||||
|
||||
var toNotify map[string]StatusListener
|
||||
var statuses map[string]*Status
|
||||
|
||||
func() {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.pluginStatus[pluginName] = status
|
||||
toNotify = make(map[string]StatusListener, len(m.pluginStatusListeners))
|
||||
for k, v := range m.pluginStatusListeners {
|
||||
toNotify[k] = v
|
||||
}
|
||||
statuses = m.copyPluginStatus()
|
||||
}()
|
||||
|
||||
for _, l := range toNotify {
|
||||
l(statuses)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) copyPluginStatus() map[string]*Status {
|
||||
statusCpy := map[string]*Status{}
|
||||
for k, v := range m.pluginStatus {
|
||||
var cpy *Status
|
||||
if v != nil {
|
||||
cpy = &Status{
|
||||
State: v.State,
|
||||
Message: v.Message,
|
||||
}
|
||||
}
|
||||
statusCpy[k] = cpy
|
||||
}
|
||||
return statusCpy
|
||||
}
|
||||
|
||||
func (m *Manager) onCommit(ctx context.Context, txn storage.Transaction, event storage.TriggerEvent) {
|
||||
|
||||
compiler := GetCompilerOnContext(event.Context)
|
||||
|
||||
// If the context does not contain the compiler fallback to loading the
|
||||
// compiler from the store. Currently the bundle plugin sets the
|
||||
// compiler on the context but the server does not (nor would users
|
||||
// implementing their own policy loading.)
|
||||
if compiler == nil && event.PolicyChanged() {
|
||||
compiler, _ = loadCompilerFromStore(ctx, m.Store, txn, m.enablePrintStatements)
|
||||
}
|
||||
|
||||
if compiler != nil {
|
||||
m.setCompiler(compiler)
|
||||
for _, f := range m.registeredTriggers {
|
||||
f(txn)
|
||||
}
|
||||
}
|
||||
|
||||
// Similar to the compiler, look for a set of resolvers on the transaction
|
||||
// context. If they are not set we may need to reload from the store.
|
||||
resolvers := getWasmResolversOnContext(event.Context)
|
||||
if resolvers != nil {
|
||||
m.setWasmResolvers(resolvers)
|
||||
|
||||
} else if event.DataChanged() {
|
||||
if requiresWasmResolverReload(event) {
|
||||
resolvers, err := bundleUtils.LoadWasmResolversFromStore(ctx, m.Store, txn, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.setWasmResolvers(resolvers)
|
||||
} else {
|
||||
err := m.updateWasmResolversData(ctx, event)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadCompilerFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, enablePrintStatements bool) (*ast.Compiler, error) {
|
||||
policies, err := store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modules := map[string]*ast.Module{}
|
||||
|
||||
for _, policy := range policies {
|
||||
bs, err := store.GetPolicy(ctx, txn, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
module, err := ast.ParseModule(policy, string(bs))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modules[policy] = module
|
||||
}
|
||||
|
||||
compiler := ast.NewCompiler().WithEnablePrintStatements(enablePrintStatements)
|
||||
compiler.Compile(modules)
|
||||
return compiler, nil
|
||||
}
|
||||
|
||||
func requiresWasmResolverReload(event storage.TriggerEvent) bool {
|
||||
// If the data changes touched the bundle path (which includes
|
||||
// the wasm modules) we will reload them. Otherwise update
|
||||
// data for each module already on the manager.
|
||||
for _, dataEvent := range event.Data {
|
||||
if dataEvent.Path.HasPrefix(bundle.BundlesBasePath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Manager) updateWasmResolversData(ctx context.Context, event storage.TriggerEvent) error {
|
||||
m.wasmResolversMtx.Lock()
|
||||
defer m.wasmResolversMtx.Unlock()
|
||||
|
||||
for _, resolver := range m.wasmResolvers {
|
||||
for _, dataEvent := range event.Data {
|
||||
var err error
|
||||
if dataEvent.Removed {
|
||||
err = resolver.RemoveDataPath(ctx, dataEvent.Path)
|
||||
} else {
|
||||
err = resolver.SetDataPath(ctx, dataEvent.Path, dataEvent.Data)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update wasm runtime data: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublicKeys returns a public keys that can be used for verifying signed bundles.
|
||||
func (m *Manager) PublicKeys() map[string]*keys.Config {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
return m.keys
|
||||
}
|
||||
|
||||
// Client returns a client for communicating with a remote service.
|
||||
func (m *Manager) Client(name string) rest.Client {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
return m.services[name]
|
||||
}
|
||||
|
||||
// Services returns a list of services that m can provide clients for.
|
||||
func (m *Manager) Services() []string {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
s := make([]string, 0, len(m.services))
|
||||
for name := range m.services {
|
||||
s = append(s, name)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Logger gets the standard logger for this plugin manager.
|
||||
func (m *Manager) Logger() logging.Logger {
|
||||
return m.logger
|
||||
}
|
||||
|
||||
// ConsoleLogger gets the console logger for this plugin manager.
|
||||
func (m *Manager) ConsoleLogger() logging.Logger {
|
||||
return m.consoleLogger
|
||||
}
|
||||
|
||||
func (m *Manager) PrintHook() print.Hook {
|
||||
return m.printHook
|
||||
}
|
||||
|
||||
func (m *Manager) EnablePrintStatements() bool {
|
||||
return m.enablePrintStatements
|
||||
}
|
||||
|
||||
// ServerInitialized signals a channel indicating that the OPA
|
||||
// server has finished initialization.
|
||||
func (m *Manager) ServerInitialized() {
|
||||
m.serverInitializedOnce.Do(func() { close(m.serverInitialized) })
|
||||
}
|
||||
|
||||
// ServerInitializedChannel returns a receive-only channel that
|
||||
// is closed when the OPA server has finished initialization.
|
||||
// Be aware that the socket of the server listener may not be
|
||||
// open by the time this channel is closed. There is a very
|
||||
// small window where the socket may still be closed, due to
|
||||
// a race condition.
|
||||
func (m *Manager) ServerInitializedChannel() <-chan struct{} {
|
||||
return m.serverInitialized
|
||||
}
|
||||
|
||||
// RegisterCacheTrigger accepts a func that receives new inter-query cache config generated by
|
||||
// a reconfigure of the plugin manager, so that it can be propagated to existing inter-query caches.
|
||||
func (m *Manager) RegisterCacheTrigger(trigger func(*cache.Config)) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.registeredCacheTriggers = append(m.registeredCacheTriggers, trigger)
|
||||
}
|
||||
|
||||
// PrometheusRegister gets the prometheus.Registerer for this plugin manager.
|
||||
func (m *Manager) PrometheusRegister() prometheus.Registerer {
|
||||
return m.prometheusRegister
|
||||
}
|
||||
|
||||
// TracerProvider gets the *trace.TracerProvider for this plugin manager.
|
||||
func (m *Manager) TracerProvider() *trace.TracerProvider {
|
||||
return m.tracerProvider
|
||||
}
|
||||
|
||||
func (m *Manager) RegisterNDCacheTrigger(trigger func(bool)) {
|
||||
m.mtx.Lock()
|
||||
defer m.mtx.Unlock()
|
||||
m.registeredNDCacheTriggers = append(m.registeredNDCacheTriggers, trigger)
|
||||
}
|
||||
+915
@@ -0,0 +1,915 @@
|
||||
// Copyright 2019 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jwa"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jws"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jws/sign"
|
||||
"github.com/open-policy-agent/opa/internal/providers/aws"
|
||||
"github.com/open-policy-agent/opa/internal/uuid"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default to s3 when the service for sigv4 signing is not specified for backwards compatibility
|
||||
awsSigv4SigningDefaultService = "s3"
|
||||
)
|
||||
|
||||
// DefaultTLSConfig defines standard TLS configurations based on the Config
|
||||
func DefaultTLSConfig(c Config) (*tls.Config, error) {
|
||||
t := &tls.Config{}
|
||||
url, err := url.Parse(c.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if url.Scheme == "https" {
|
||||
t.InsecureSkipVerify = c.AllowInsecureTLS
|
||||
}
|
||||
|
||||
if c.TLS != nil && c.TLS.CACert != "" {
|
||||
caCert, err := os.ReadFile(c.TLS.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootCAs *x509.CertPool
|
||||
if c.TLS.SystemCARequired {
|
||||
rootCAs, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rootCAs = x509.NewCertPool()
|
||||
}
|
||||
|
||||
ok := rootCAs.AppendCertsFromPEM(caCert)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
|
||||
}
|
||||
t.RootCAs = rootCAs
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DefaultRoundTripperClient is a reasonable set of defaults for HTTP auth plugins
|
||||
func DefaultRoundTripperClient(t *tls.Config, timeout int64) *http.Client {
|
||||
// Ensure we use a http.Transport with proper settings: the zero values are not
|
||||
// a good choice, as they cause leaking connections:
|
||||
// https://github.com/golang/go/issues/19620
|
||||
|
||||
// copy, we don't want to alter the default client's Transport
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.ResponseHeaderTimeout = time.Duration(timeout) * time.Second
|
||||
tr.TLSClientConfig = t
|
||||
|
||||
c := *http.DefaultClient
|
||||
c.Transport = tr
|
||||
return &c
|
||||
}
|
||||
|
||||
// defaultAuthPlugin represents baseline 'no auth' behavior if no alternative plugin is specified for a service
|
||||
type defaultAuthPlugin struct{}
|
||||
|
||||
func (*defaultAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (*defaultAuthPlugin) Prepare(*http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type serverTLSConfig struct {
|
||||
CACert string `json:"ca_cert,omitempty"`
|
||||
SystemCARequired bool `json:"system_ca_required,omitempty"`
|
||||
}
|
||||
|
||||
// bearerAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
|
||||
type bearerAuthPlugin struct {
|
||||
Token string `json:"token"`
|
||||
TokenPath string `json:"token_path"`
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
|
||||
// encode is set to true for the OCIDownloader because
|
||||
// it expects tokens in plain text but needs them in base64.
|
||||
encode bool
|
||||
}
|
||||
|
||||
func (ap *bearerAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.Token != "" && ap.TokenPath != "" {
|
||||
return nil, errors.New("invalid config: specify a value for either the \"token\" or \"token_path\" field")
|
||||
}
|
||||
|
||||
if ap.Scheme == "" {
|
||||
ap.Scheme = "Bearer"
|
||||
}
|
||||
|
||||
if c.Type == "oci" {
|
||||
// Standard rest clients use the bearer token as it is defined in the Config
|
||||
// but the OCIDownloader needs it encoded to base64 before using to sign a request.
|
||||
ap.encode = true
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *bearerAuthPlugin) Prepare(req *http.Request) error {
|
||||
token := ap.Token
|
||||
|
||||
if ap.TokenPath != "" {
|
||||
bytes, err := os.ReadFile(ap.TokenPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token = strings.TrimSpace(string(bytes))
|
||||
}
|
||||
|
||||
if ap.encode {
|
||||
token = base64.StdEncoding.EncodeToString([]byte(token))
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("%v %v", ap.Scheme, token))
|
||||
return nil
|
||||
}
|
||||
|
||||
type tokenEndpointResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
type awsKmsKeyConfig struct {
|
||||
Name string `json:"name"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
}
|
||||
|
||||
func convertSignatureToBase64(alg string, der []byte) (string, error) {
|
||||
r, s, derErr := pointsFromDER(der)
|
||||
if derErr != nil {
|
||||
return "", fmt.Errorf("failed to read points from der %v", derErr)
|
||||
}
|
||||
|
||||
signatureData, err := convertPointsToBase64(alg, r.Bytes(), s.Bytes())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signatureData, nil
|
||||
}
|
||||
|
||||
func pointsFromDER(der []byte) (R, S *big.Int, err error) {
|
||||
R, S = &big.Int{}, &big.Int{}
|
||||
data := asn1.RawValue{}
|
||||
if _, err := asn1.Unmarshal(der, &data); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshall the signature from DER format %v", err)
|
||||
|
||||
}
|
||||
// https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html#API_Sign_ResponseSyntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc3279#section-2.2.3
|
||||
// The format of our DER string is 0x02 + rlen + r + 0x02 + slen + s
|
||||
rLen := data.Bytes[1] // The entire length of R + offset of 2 for 0x02 and rlen
|
||||
r := data.Bytes[2 : rLen+2]
|
||||
// Ignore the next 0x02 and slen bytes and just take the start of S to the end of the byte array
|
||||
s := data.Bytes[rLen+4:]
|
||||
R.SetBytes(r)
|
||||
S.SetBytes(s)
|
||||
return
|
||||
}
|
||||
|
||||
func convertPointsToBase64(alg string, r, s []byte) (string, error) {
|
||||
curveBits, err := retrieveCurveBits(alg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keyBytes := curveBits / 8
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
// We serialize the outputs (r and s) into big-endian byte arrays and pad
|
||||
// them with zeros on the left to make sure the sizes work out. Both arrays
|
||||
// must be keyBytes long, and the output must be 2*keyBytes long.
|
||||
rBytesPadded := make([]byte, keyBytes)
|
||||
copy(rBytesPadded[keyBytes-len(r):], r)
|
||||
sBytesPadded := make([]byte, keyBytes)
|
||||
copy(sBytesPadded[keyBytes-len(s):], s)
|
||||
signatureEnc := append(rBytesPadded, sBytesPadded...)
|
||||
|
||||
return base64.RawURLEncoding.EncodeToString(signatureEnc), nil
|
||||
}
|
||||
|
||||
func retrieveCurveBits(alg string) (int, error) {
|
||||
var curveBits int
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
curveBits = 256
|
||||
case "ECDSA_SHA_384":
|
||||
curveBits = 384
|
||||
case "ECDSA_SHA_512":
|
||||
curveBits = 512
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
return curveBits, nil
|
||||
}
|
||||
|
||||
func messageDigest(message []byte, alg string) ([]byte, error) {
|
||||
var digest hash.Hash
|
||||
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
digest = sha256.New()
|
||||
case "ECDSA_SHA_384":
|
||||
digest = sha512.New384()
|
||||
case "ECDSA_SHA_512":
|
||||
digest = sha512.New()
|
||||
default:
|
||||
return []byte{}, fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
|
||||
digest.Write(message)
|
||||
return digest.Sum(nil), nil
|
||||
}
|
||||
|
||||
// oauth2ClientCredentialsAuthPlugin represents authentication via a bearer token in the HTTP Authorization header
|
||||
// obtained through the OAuth2 client credentials flow
|
||||
type oauth2ClientCredentialsAuthPlugin struct {
|
||||
GrantType string `json:"grant_type"`
|
||||
TokenURL string `json:"token_url"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
SigningKeyID string `json:"signing_key"`
|
||||
Thumbprint string `json:"thumbprint"`
|
||||
Claims map[string]interface{} `json:"additional_claims"`
|
||||
IncludeJti bool `json:"include_jti_claim"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
AdditionalHeaders map[string]string `json:"additional_headers,omitempty"`
|
||||
AdditionalParameters map[string]string `json:"additional_parameters,omitempty"`
|
||||
AWSKmsKey *awsKmsKeyConfig `json:"aws_kms,omitempty"`
|
||||
AWSSigningPlugin *awsSigningAuthPlugin `json:"aws_signing,omitempty"`
|
||||
|
||||
signingKey *keys.Config
|
||||
signingKeyParsed interface{}
|
||||
tokenCache *oauth2Token
|
||||
tlsSkipVerify bool
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type oauth2Token struct {
|
||||
Token string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) createAuthJWT(ctx context.Context, claims map[string]interface{}, signingKey interface{}) (*string, error) {
|
||||
now := time.Now()
|
||||
baseClaims := map[string]interface{}{
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(10 * time.Minute).Unix(),
|
||||
}
|
||||
if claims == nil {
|
||||
claims = make(map[string]interface{})
|
||||
}
|
||||
for k, v := range baseClaims {
|
||||
claims[k] = v
|
||||
}
|
||||
|
||||
if len(ap.Scopes) > 0 {
|
||||
claims["scope"] = strings.Join(ap.Scopes, " ")
|
||||
}
|
||||
|
||||
if ap.IncludeJti {
|
||||
jti, err := uuid.New(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims["jti"] = jti
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var jwsHeaders []byte
|
||||
var signatureAlg string
|
||||
if ap.AWSKmsKey == nil {
|
||||
signatureAlg = ap.signingKey.Algorithm
|
||||
} else {
|
||||
signatureAlg, err = ap.mapKMSAlgToSign(ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if ap.Thumbprint != "" {
|
||||
bytes, err := hex.DecodeString(ap.Thumbprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x5t := base64.URLEncoding.EncodeToString(bytes)
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s","x5t":"%s"}`, signatureAlg, x5t))
|
||||
} else {
|
||||
jwsHeaders = []byte(fmt.Sprintf(`{"typ":"JWT","alg":"%s"}`, signatureAlg))
|
||||
}
|
||||
var jwsCompact []byte
|
||||
if ap.AWSKmsKey == nil {
|
||||
jwsCompact, err = jws.SignLiteral(payload,
|
||||
jwa.SignatureAlgorithm(signatureAlg),
|
||||
signingKey,
|
||||
jwsHeaders,
|
||||
rand.Reader)
|
||||
} else {
|
||||
jwsCompact, err = ap.SignWithKMS(ctx, payload, jwsHeaders)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jwt := string(jwsCompact)
|
||||
|
||||
return &jwt, nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) mapKMSAlgToSign(alg string) (string, error) {
|
||||
switch alg {
|
||||
case "ECDSA_SHA_256":
|
||||
return "ES256", nil
|
||||
case "ECDSA_SHA_384":
|
||||
return "ES384", nil
|
||||
case "ECDSA_SHA_512":
|
||||
return "ES512", nil
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported sign algorithm %s", alg)
|
||||
}
|
||||
}
|
||||
|
||||
// SignWithKMS will sign the JWT in AWS using the key stored in the supplied kmsArn
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) SignWithKMS(ctx context.Context, payload []byte, hdrBuf []byte) ([]byte, error) {
|
||||
|
||||
encodedHdr := base64.RawURLEncoding.EncodeToString(hdrBuf)
|
||||
encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
|
||||
input := strings.Join(
|
||||
[]string{
|
||||
encodedHdr,
|
||||
encodedPayload,
|
||||
}, ".",
|
||||
)
|
||||
digest, err := messageDigest([]byte(input), ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ap.AWSSigningPlugin != nil {
|
||||
signature, err := ap.AWSSigningPlugin.SignDigest(ctx, digest, ap.AWSKmsKey.Name, ap.AWSKmsKey.Algorithm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
der, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signatureData, err := convertSignatureToBase64(ap.AWSKmsKey.Algorithm, der)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedAssertion := input + "." + signatureData
|
||||
|
||||
return []byte(signedAssertion), nil
|
||||
}
|
||||
return nil, errors.New("missing AWS credentials, failed to sign the assertion with kms")
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) parseSigningKey(c Config) (err error) {
|
||||
if ap.SigningKeyID == "" {
|
||||
return errors.New("signing_key required for jwt_bearer grant type")
|
||||
}
|
||||
|
||||
if val, ok := c.keys[ap.SigningKeyID]; ok {
|
||||
if val.PrivateKey == "" {
|
||||
return errors.New("referenced signing_key does not include a private key")
|
||||
}
|
||||
ap.signingKey = val
|
||||
} else {
|
||||
return errors.New("signing_key refers to non-existent key")
|
||||
}
|
||||
|
||||
alg := jwa.SignatureAlgorithm(ap.signingKey.Algorithm)
|
||||
ap.signingKeyParsed, err = sign.GetSigningKey(ap.signingKey.PrivateKey, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.GrantType == "" {
|
||||
// Use client_credentials as default to not break existing config
|
||||
ap.GrantType = grantTypeClientCredentials
|
||||
} else if ap.GrantType != grantTypeClientCredentials && ap.GrantType != grantTypeJwtBearer {
|
||||
return nil, errors.New("grant_type must be either client_credentials or jwt_bearer")
|
||||
}
|
||||
|
||||
if ap.GrantType == grantTypeJwtBearer || (ap.GrantType == grantTypeClientCredentials && ap.SigningKeyID != "") {
|
||||
if err = ap.parseSigningKey(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Inherit skip verify from the "parent" settings. Should this be configurable on the credentials too?
|
||||
ap.tlsSkipVerify = c.AllowInsecureTLS
|
||||
|
||||
ap.logger = c.logger
|
||||
|
||||
if !strings.HasPrefix(ap.TokenURL, "https://") {
|
||||
return nil, errors.New("token_url required to use https scheme")
|
||||
}
|
||||
if ap.GrantType == grantTypeClientCredentials {
|
||||
if ap.AWSKmsKey != nil && (ap.ClientSecret != "" || ap.SigningKeyID != "") ||
|
||||
(ap.ClientSecret != "" && ap.SigningKeyID != "") {
|
||||
return nil, errors.New("can only use one of client_secret, signing_key or signing_kms_key for client_credentials")
|
||||
}
|
||||
if ap.SigningKeyID == "" && ap.AWSKmsKey == nil && (ap.ClientID == "" || ap.ClientSecret == "") {
|
||||
return nil, errors.New("client_id and client_secret required")
|
||||
}
|
||||
if ap.AWSKmsKey != nil {
|
||||
if ap.AWSSigningPlugin == nil {
|
||||
return nil, errors.New("aws_kms and aws_signing required")
|
||||
}
|
||||
// initialize the awsSigningAuthPlugin
|
||||
_, err = ap.AWSSigningPlugin.NewClient(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
// requestToken tries to obtain an access token using either the client credentials flow
|
||||
// https://tools.ietf.org/html/rfc6749#section-4.4
|
||||
// or the JWT authorization grant
|
||||
// https://tools.ietf.org/html/rfc7523
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) requestToken(ctx context.Context) (*oauth2Token, error) {
|
||||
body := url.Values{}
|
||||
if ap.GrantType == grantTypeJwtBearer {
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body.Add("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
|
||||
body.Add("assertion", *authJwt)
|
||||
} else {
|
||||
body.Add("grant_type", grantTypeClientCredentials)
|
||||
|
||||
if ap.SigningKeyID != "" || ap.AWSKmsKey != nil {
|
||||
authJwt, err := ap.createAuthJWT(ctx, ap.Claims, ap.signingKeyParsed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body.Add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
|
||||
body.Add("client_assertion", *authJwt)
|
||||
|
||||
if ap.ClientID != "" {
|
||||
body.Add("client_id", ap.ClientID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ap.Scopes) > 0 {
|
||||
body.Add("scope", strings.Join(ap.Scopes, " "))
|
||||
}
|
||||
|
||||
for k, v := range ap.AdditionalParameters {
|
||||
body.Set(k, v)
|
||||
}
|
||||
|
||||
r, err := http.NewRequestWithContext(ctx, "POST", ap.TokenURL, strings.NewReader(body.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
if ap.GrantType == grantTypeClientCredentials && ap.ClientSecret != "" {
|
||||
r.SetBasicAuth(ap.ClientID, ap.ClientSecret)
|
||||
}
|
||||
|
||||
for k, v := range ap.AdditionalHeaders {
|
||||
r.Header.Add(k, v)
|
||||
}
|
||||
|
||||
client := DefaultRoundTripperClient(&tls.Config{InsecureSkipVerify: ap.tlsSkipVerify}, 10)
|
||||
response, err := client.Do(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bodyRaw, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("error in response from OAuth2 token endpoint: %v", string(bodyRaw))
|
||||
}
|
||||
|
||||
var tokenResponse tokenEndpointResponse
|
||||
err = json.Unmarshal(bodyRaw, &tokenResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if strings.ToLower(tokenResponse.TokenType) != "bearer" {
|
||||
return nil, errors.New("unknown token type returned from token endpoint")
|
||||
}
|
||||
|
||||
return &oauth2Token{
|
||||
Token: strings.TrimSpace(tokenResponse.AccessToken),
|
||||
ExpiresAt: time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ap *oauth2ClientCredentialsAuthPlugin) Prepare(req *http.Request) error {
|
||||
minTokenLifetime := float64(10)
|
||||
if ap.tokenCache == nil || time.Until(ap.tokenCache.ExpiresAt).Seconds() < minTokenLifetime {
|
||||
ap.logger.Debug("Requesting token from token_url %v", ap.TokenURL)
|
||||
token, err := ap.requestToken(req.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ap.tokenCache = token
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", ap.tokenCache.Token))
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientTLSAuthPlugin represents authentication via client certificate on a TLS connection
|
||||
type clientTLSAuthPlugin struct {
|
||||
Cert string `json:"cert"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"`
|
||||
CACert string `json:"ca_cert,omitempty"` // Deprecated: Use `services[_].tls.ca_cert` instead
|
||||
SystemCARequired bool `json:"system_ca_required,omitempty"` // Deprecated: Use `services[_].tls.system_ca_required` instead
|
||||
}
|
||||
|
||||
func (ap *clientTLSAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
tlsConfig, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.Cert == "" {
|
||||
return nil, errors.New("client certificate is needed when client TLS is enabled")
|
||||
}
|
||||
if ap.PrivateKey == "" {
|
||||
return nil, errors.New("private key is needed when client TLS is enabled")
|
||||
}
|
||||
|
||||
var keyPEMBlock []byte
|
||||
data, err := os.ReadFile(ap.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, errors.New("PEM data could not be found")
|
||||
}
|
||||
|
||||
// nolint: staticcheck // We don't want to forbid users from using this encryption.
|
||||
if x509.IsEncryptedPEMBlock(block) {
|
||||
if ap.PrivateKeyPassphrase == "" {
|
||||
return nil, errors.New("client certificate passphrase is needed, because the certificate is password encrypted")
|
||||
}
|
||||
// nolint: staticcheck // We don't want to forbid users from using this encryption.
|
||||
block, err := x509.DecryptPEMBlock(block, []byte(ap.PrivateKeyPassphrase))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block)
|
||||
if err != nil {
|
||||
key, err = x509.ParsePKCS1PrivateKey(block)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("private key should be a PEM or plain PKCS1 or PKCS8; parse error: %v", err)
|
||||
}
|
||||
}
|
||||
rsa, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("private key is invalid")
|
||||
}
|
||||
keyPEMBlock = pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "RSA PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(rsa),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
keyPEMBlock = data
|
||||
}
|
||||
|
||||
certPEMBlock, err := os.ReadFile(ap.Cert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certPEMBlock, keyPEMBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
|
||||
var client *http.Client
|
||||
|
||||
if c.TLS != nil && c.TLS.CACert != "" {
|
||||
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
|
||||
} else {
|
||||
if ap.CACert != "" {
|
||||
c.logger.Warn("Deprecated 'services[_].credentials.client_tls.ca_cert' configuration specified. Use 'services[_].tls.ca_cert' instead. See https://www.openpolicyagent.org/docs/latest/configuration/#services")
|
||||
caCert, err := os.ReadFile(ap.CACert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var caCertPool *x509.CertPool
|
||||
if ap.SystemCARequired {
|
||||
caCertPool, err = x509.SystemCertPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
caCertPool = x509.NewCertPool()
|
||||
}
|
||||
|
||||
ok := caCertPool.AppendCertsFromPEM(caCert)
|
||||
if !ok {
|
||||
return nil, errors.New("unable to parse and append CA certificate to certificate pool")
|
||||
}
|
||||
tlsConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
client = DefaultRoundTripperClient(tlsConfig, *c.ResponseHeaderTimeoutSeconds)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (ap *clientTLSAuthPlugin) Prepare(req *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// awsSigningAuthPlugin represents authentication using AWS V4 HMAC signing in the Authorization header
|
||||
type awsSigningAuthPlugin struct {
|
||||
AWSEnvironmentCredentials *awsEnvironmentCredentialService `json:"environment_credentials,omitempty"`
|
||||
AWSMetadataCredentials *awsMetadataCredentialService `json:"metadata_credentials,omitempty"`
|
||||
AWSWebIdentityCredentials *awsWebIdentityCredentialService `json:"web_identity_credentials,omitempty"`
|
||||
AWSProfileCredentials *awsProfileCredentialService `json:"profile_credentials,omitempty"`
|
||||
|
||||
AWSService string `json:"service,omitempty"`
|
||||
AWSSignatureVersion string `json:"signature_version,omitempty"`
|
||||
|
||||
ecrAuthPlugin *ecrAuthPlugin
|
||||
kmsSignPlugin *awsKMSSignPlugin
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type awsCredentialServiceChain struct {
|
||||
awsCredentialServices []awsCredentialService
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (acs *awsCredentialServiceChain) addService(service awsCredentialService) {
|
||||
acs.awsCredentialServices = append(acs.awsCredentialServices, service)
|
||||
}
|
||||
|
||||
type awsCredentialCheckErrors []*awsCredentialCheckError
|
||||
|
||||
func (e awsCredentialCheckErrors) Error() string {
|
||||
|
||||
if len(e) == 0 {
|
||||
return "no error(s)"
|
||||
}
|
||||
|
||||
if len(e) == 1 {
|
||||
return fmt.Sprintf("1 error occurred: %v", e[0].Error())
|
||||
}
|
||||
|
||||
s := make([]string, len(e))
|
||||
for i, err := range e {
|
||||
s[i] = err.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d errors occurred:\n%s", len(e), strings.Join(s, "\n"))
|
||||
}
|
||||
|
||||
type awsCredentialCheckError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func newAWSCredentialError(message string) *awsCredentialCheckError {
|
||||
return &awsCredentialCheckError{
|
||||
message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *awsCredentialCheckError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (acs *awsCredentialServiceChain) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
var errs awsCredentialCheckErrors
|
||||
|
||||
for _, service := range acs.awsCredentialServices {
|
||||
credential, err := service.credentials(ctx)
|
||||
if err != nil {
|
||||
acs.logger.Debug("awsSigningAuthPlugin:%T failed: %v", service, err)
|
||||
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return aws.Credentials{}, err
|
||||
}
|
||||
|
||||
errs = append(errs, newAWSCredentialError(err.Error()))
|
||||
continue
|
||||
}
|
||||
|
||||
acs.logger.Debug("awsSigningAuthPlugin:%T successful", service)
|
||||
return credential, nil
|
||||
}
|
||||
|
||||
return aws.Credentials{}, fmt.Errorf("all AWS credential providers failed: %v", errs)
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) awsCredentialService() awsCredentialService {
|
||||
chain := awsCredentialServiceChain{
|
||||
logger: ap.logger,
|
||||
}
|
||||
|
||||
/*
|
||||
Here we maintain the order of addition to the chain inline with
|
||||
the order of credential providers followed by default by the
|
||||
AWS SDK. For example
|
||||
|
||||
https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
|
||||
*/
|
||||
|
||||
if ap.AWSEnvironmentCredentials != nil {
|
||||
ap.AWSEnvironmentCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSEnvironmentCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSWebIdentityCredentials != nil {
|
||||
ap.AWSWebIdentityCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSWebIdentityCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSProfileCredentials != nil {
|
||||
ap.AWSProfileCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSProfileCredentials)
|
||||
}
|
||||
|
||||
if ap.AWSMetadataCredentials != nil {
|
||||
ap.AWSMetadataCredentials.logger = ap.logger
|
||||
chain.addService(ap.AWSMetadataCredentials)
|
||||
}
|
||||
|
||||
return &chain
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ap.logger == nil {
|
||||
ap.logger = c.logger
|
||||
}
|
||||
|
||||
if err := ap.validateAndSetDefaults(c.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) Prepare(req *http.Request) error {
|
||||
switch ap.AWSService {
|
||||
case "ecr":
|
||||
return ap.ecrAuthPlugin.Prepare(req)
|
||||
default:
|
||||
creds, err := ap.awsCredentialService().credentials(req.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
ap.logger.Debug("Signing request with AWS credentials.")
|
||||
|
||||
return aws.SignRequest(req, ap.AWSService, creds, time.Now(), ap.AWSSignatureVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) validateAndSetDefaults(serviceType string) error {
|
||||
cfgs := map[bool]int{}
|
||||
cfgs[ap.AWSEnvironmentCredentials != nil]++
|
||||
cfgs[ap.AWSMetadataCredentials != nil]++
|
||||
cfgs[ap.AWSWebIdentityCredentials != nil]++
|
||||
cfgs[ap.AWSProfileCredentials != nil]++
|
||||
|
||||
if cfgs[true] == 0 {
|
||||
return errors.New("a AWS credential service must be specified when S3 signing is enabled")
|
||||
}
|
||||
|
||||
if ap.AWSMetadataCredentials != nil {
|
||||
if ap.AWSMetadataCredentials.RegionName == "" {
|
||||
return errors.New("at least aws_region must be specified for AWS metadata credential service")
|
||||
}
|
||||
}
|
||||
|
||||
if ap.AWSWebIdentityCredentials != nil {
|
||||
if err := ap.AWSWebIdentityCredentials.populateFromEnv(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ap.AWSService = strings.ToLower(ap.AWSService)
|
||||
|
||||
// Only allow ECR for OCI service types
|
||||
if serviceType == "oci" {
|
||||
if ap.AWSService == "" {
|
||||
ap.AWSService = "ecr"
|
||||
}
|
||||
|
||||
if ap.AWSService != "ecr" {
|
||||
return fmt.Errorf(`cannot use aws service %q with service type "oci"`, ap.AWSService)
|
||||
}
|
||||
|
||||
// We need to setup a special auth plugin for ECR.
|
||||
ap.ecrAuthPlugin = newECRAuthPlugin(ap)
|
||||
} else {
|
||||
// Disallow ECR for non-OCI service types
|
||||
if ap.AWSService == "ecr" {
|
||||
return errors.New(`aws service "ecr" must be used with service type "oci"`)
|
||||
}
|
||||
if ap.AWSService == "kms" && ap.kmsSignPlugin == nil {
|
||||
// We need a special plugin for KMS.
|
||||
ap.kmsSignPlugin = newKMSSignPlugin(ap)
|
||||
}
|
||||
if ap.AWSService == "" {
|
||||
ap.AWSService = awsSigv4SigningDefaultService
|
||||
}
|
||||
}
|
||||
|
||||
if ap.AWSSignatureVersion == "" {
|
||||
ap.AWSSignatureVersion = "4"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *awsSigningAuthPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
switch ap.AWSService {
|
||||
case "kms":
|
||||
return ap.kmsSignPlugin.SignDigest(ctx, digest, keyID, signingAlgorithm)
|
||||
default:
|
||||
return "", fmt.Errorf(`cannot use SignDigest with aws service %q`, ap.AWSService)
|
||||
}
|
||||
}
|
||||
+557
@@ -0,0 +1,557 @@
|
||||
// Copyright 2019 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 rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
"github.com/open-policy-agent/opa/internal/providers/aws"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
)
|
||||
|
||||
const (
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||
ec2DefaultCredServicePath = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
|
||||
ec2DefaultTokenPath = "http://169.254.169.254/latest/api/token"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/AmazonECS/latest/userguide/task-iam-roles.html
|
||||
ecsDefaultCredServicePath = "http://169.254.170.2"
|
||||
ecsRelativePathEnvVar = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
|
||||
stsDefaultDomain = "amazonaws.com"
|
||||
stsDefaultPath = "https://sts.%s"
|
||||
stsRegionPath = "https://sts.%s.%s"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html
|
||||
accessKeyEnvVar = "AWS_ACCESS_KEY_ID"
|
||||
secretKeyEnvVar = "AWS_SECRET_ACCESS_KEY"
|
||||
securityTokenEnvVar = "AWS_SECURITY_TOKEN"
|
||||
sessionTokenEnvVar = "AWS_SESSION_TOKEN"
|
||||
awsRegionEnvVar = "AWS_REGION"
|
||||
awsDomainEnvVar = "AWS_DOMAIN"
|
||||
awsRoleArnEnvVar = "AWS_ROLE_ARN"
|
||||
awsWebIdentityTokenFileEnvVar = "AWS_WEB_IDENTITY_TOKEN_FILE"
|
||||
awsCredentialsFileEnvVar = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
awsProfileEnvVar = "AWS_PROFILE"
|
||||
|
||||
// ref. https://docs.aws.amazon.com/sdkref/latest/guide/settings-global.html
|
||||
accessKeyGlobalSetting = "aws_access_key_id"
|
||||
secretKeyGlobalSetting = "aws_secret_access_key"
|
||||
securityTokenGlobalSetting = "aws_session_token"
|
||||
)
|
||||
|
||||
// awsCredentialService represents the interface for AWS credential providers
|
||||
type awsCredentialService interface {
|
||||
credentials(context.Context) (aws.Credentials, error)
|
||||
}
|
||||
|
||||
// awsEnvironmentCredentialService represents an static environment-variable credential provider for AWS
|
||||
type awsEnvironmentCredentialService struct {
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsEnvironmentCredentialService) credentials(context.Context) (aws.Credentials, error) {
|
||||
var creds aws.Credentials
|
||||
creds.AccessKey = os.Getenv(accessKeyEnvVar)
|
||||
if creds.AccessKey == "" {
|
||||
return creds, errors.New("no " + accessKeyEnvVar + " set in environment")
|
||||
}
|
||||
creds.SecretKey = os.Getenv(secretKeyEnvVar)
|
||||
if creds.SecretKey == "" {
|
||||
return creds, errors.New("no " + secretKeyEnvVar + " set in environment")
|
||||
}
|
||||
creds.RegionName = os.Getenv(awsRegionEnvVar)
|
||||
if creds.RegionName == "" {
|
||||
return creds, errors.New("no " + awsRegionEnvVar + " set in environment")
|
||||
}
|
||||
// SessionToken is required if using temporary ENV credentials from assumed IAM role
|
||||
// Missing SessionToken results with 403 s3 error.
|
||||
creds.SessionToken = os.Getenv(sessionTokenEnvVar)
|
||||
if creds.SessionToken == "" {
|
||||
// In case of missing SessionToken try to get SecurityToken
|
||||
// AWS switched to use SessionToken, but SecurityToken was left for backward compatibility
|
||||
creds.SessionToken = os.Getenv(securityTokenEnvVar)
|
||||
}
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
// awsProfileCredentialService represents a credential provider for AWS that extracts credentials from the AWS
|
||||
// credentials file
|
||||
type awsProfileCredentialService struct {
|
||||
|
||||
// Path to the credentials file.
|
||||
//
|
||||
// If empty will look for "AWS_SHARED_CREDENTIALS_FILE" env variable. If the
|
||||
// env value is empty will default to current user's home directory.
|
||||
// Linux/OSX: "$HOME/.aws/credentials"
|
||||
// Windows: "%USERPROFILE%\.aws\credentials"
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// AWS Profile to extract credentials from the credentials file. If empty
|
||||
// will default to environment variable "AWS_PROFILE" or "default" if
|
||||
// environment variable is also not set.
|
||||
Profile string `json:"profile,omitempty"`
|
||||
|
||||
RegionName string `json:"aws_region"`
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) credentials(context.Context) (aws.Credentials, error) {
|
||||
var creds aws.Credentials
|
||||
|
||||
filename, err := cs.path()
|
||||
if err != nil {
|
||||
return creds, err
|
||||
}
|
||||
|
||||
cfg, err := ini.Load(filename)
|
||||
if err != nil {
|
||||
return creds, fmt.Errorf("failed to read credentials file: %v", err)
|
||||
}
|
||||
|
||||
profile, err := cfg.GetSection(cs.profile())
|
||||
if err != nil {
|
||||
return creds, fmt.Errorf("failed to get profile: %v", err)
|
||||
}
|
||||
|
||||
creds.AccessKey = profile.Key(accessKeyGlobalSetting).String()
|
||||
if creds.AccessKey == "" {
|
||||
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, accessKeyGlobalSetting)
|
||||
}
|
||||
|
||||
creds.SecretKey = profile.Key(secretKeyGlobalSetting).String()
|
||||
if creds.SecretKey == "" {
|
||||
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, secretKeyGlobalSetting)
|
||||
}
|
||||
|
||||
creds.SessionToken = profile.Key(securityTokenGlobalSetting).String() // default to empty string
|
||||
|
||||
if cs.RegionName == "" {
|
||||
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
|
||||
return creds, errors.New("no " + awsRegionEnvVar + " set in environment or configuration")
|
||||
}
|
||||
}
|
||||
creds.RegionName = cs.RegionName
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) path() (string, error) {
|
||||
if len(cs.Path) != 0 {
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
if cs.Path = os.Getenv(awsCredentialsFileEnvVar); len(cs.Path) != 0 {
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("user home directory not found: %w", err)
|
||||
}
|
||||
|
||||
cs.Path = filepath.Join(homeDir, ".aws", "credentials")
|
||||
|
||||
return cs.Path, nil
|
||||
}
|
||||
|
||||
func (cs *awsProfileCredentialService) profile() string {
|
||||
if cs.Profile != "" {
|
||||
return cs.Profile
|
||||
}
|
||||
|
||||
cs.Profile = os.Getenv(awsProfileEnvVar)
|
||||
|
||||
if cs.Profile == "" {
|
||||
cs.Profile = "default"
|
||||
}
|
||||
|
||||
return cs.Profile
|
||||
}
|
||||
|
||||
// awsMetadataCredentialService represents an EC2 metadata service credential provider for AWS
|
||||
type awsMetadataCredentialService struct {
|
||||
RoleName string `json:"iam_role,omitempty"`
|
||||
RegionName string `json:"aws_region"`
|
||||
creds aws.Credentials
|
||||
expiration time.Time
|
||||
credServicePath string
|
||||
tokenPath string
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) urlForMetadataService() (string, error) {
|
||||
// override default path for testing
|
||||
if cs.credServicePath != "" {
|
||||
return cs.credServicePath + cs.RoleName, nil
|
||||
}
|
||||
// otherwise, normal flow
|
||||
// if a role name is provided, look up via the EC2 credential service
|
||||
if cs.RoleName != "" {
|
||||
return ec2DefaultCredServicePath + cs.RoleName, nil
|
||||
}
|
||||
// otherwise, check environment to see if it looks like we're in an ECS
|
||||
// container (with implied role association)
|
||||
if isECS() {
|
||||
return ecsDefaultCredServicePath + os.Getenv(ecsRelativePathEnvVar), nil
|
||||
}
|
||||
// if there's no role name and we don't appear to have a path to the
|
||||
// ECS container service, then the configuration is invalid
|
||||
return "", errors.New("metadata endpoint cannot be determined from settings and environment")
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) tokenRequest(ctx context.Context) (*http.Request, error) {
|
||||
tokenURL := ec2DefaultTokenPath
|
||||
if cs.tokenPath != "" {
|
||||
// override for testing
|
||||
tokenURL = cs.tokenPath
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, tokenURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// we are going to use the token in the immediate future, so a long TTL is not necessary
|
||||
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "60")
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) refreshFromService(ctx context.Context) error {
|
||||
// define the expected JSON payload from the EC2 credential service
|
||||
// ref. https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
||||
type metadataPayload struct {
|
||||
Code string
|
||||
AccessKeyID string `json:"AccessKeyId"`
|
||||
SecretAccessKey string
|
||||
Token string
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// Short circuit if a reasonable amount of time until credential expiration remains
|
||||
const tokenExpirationMargin = 5 * time.Minute
|
||||
|
||||
if time.Now().Add(tokenExpirationMargin).Before(cs.expiration) {
|
||||
cs.logger.Debug("Credentials previously obtained from metadata service still valid.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.logger.Debug("Obtaining credentials from metadata service.")
|
||||
metaDataURL, err := cs.urlForMetadataService()
|
||||
if err != nil {
|
||||
// configuration issue or missing ECS environment
|
||||
return err
|
||||
}
|
||||
|
||||
// construct an HTTP client with a reasonably short timeout
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, metaDataURL, nil)
|
||||
if err != nil {
|
||||
return errors.New("unable to construct metadata HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
// if in the EC2 environment, we will use IMDSv2, which requires a session cookie from a
|
||||
// PUT request on the token endpoint before it will give the credentials, this provides
|
||||
// protection from SSRF attacks
|
||||
if !isECS() {
|
||||
tokenReq, err := cs.tokenRequest(ctx)
|
||||
if err != nil {
|
||||
return errors.New("unable to construct metadata token HTTP request: " + err.Error())
|
||||
}
|
||||
body, err := aws.DoRequestWithClient(tokenReq, client, "metadata token", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// token is the body of response; add to header of metadata request
|
||||
req.Header.Set("X-aws-ec2-metadata-token", string(body))
|
||||
}
|
||||
|
||||
body, err := aws.DoRequestWithClient(req, client, "metadata", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload metadataPayload
|
||||
err = json.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
return errors.New("failed to parse credential response from metadata service: " + err.Error())
|
||||
}
|
||||
|
||||
// Only the EC2 endpoint returns the "Code" element which indicates whether the query was
|
||||
// successful; the ECS endpoint does not! Some other fields are missing in the ECS payload
|
||||
// but we do not depend on them.
|
||||
if cs.RoleName != "" && payload.Code != "Success" {
|
||||
return errors.New("metadata service query did not succeed: " + payload.Code)
|
||||
}
|
||||
|
||||
cs.expiration = payload.Expiration
|
||||
cs.creds.AccessKey = payload.AccessKeyID
|
||||
cs.creds.SecretKey = payload.SecretAccessKey
|
||||
cs.creds.SessionToken = payload.Token
|
||||
cs.creds.RegionName = cs.RegionName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsMetadataCredentialService) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
err := cs.refreshFromService(ctx)
|
||||
if err != nil {
|
||||
return cs.creds, err
|
||||
}
|
||||
return cs.creds, nil
|
||||
}
|
||||
|
||||
// awsWebIdentityCredentialService represents an STS WebIdentity credential services
|
||||
type awsWebIdentityCredentialService struct {
|
||||
RoleArn string
|
||||
WebIdentityTokenFile string
|
||||
RegionName string `json:"aws_region"`
|
||||
SessionName string `json:"session_name"`
|
||||
Domain string `json:"aws_domain"`
|
||||
stsURL string
|
||||
creds aws.Credentials
|
||||
expiration time.Time
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) populateFromEnv() error {
|
||||
cs.RoleArn = os.Getenv(awsRoleArnEnvVar)
|
||||
if cs.RoleArn == "" {
|
||||
return errors.New("no " + awsRoleArnEnvVar + " set in environment")
|
||||
}
|
||||
cs.WebIdentityTokenFile = os.Getenv(awsWebIdentityTokenFileEnvVar)
|
||||
if cs.WebIdentityTokenFile == "" {
|
||||
return errors.New("no " + awsWebIdentityTokenFileEnvVar + " set in environment")
|
||||
}
|
||||
|
||||
if cs.Domain == "" {
|
||||
cs.Domain = os.Getenv(awsDomainEnvVar)
|
||||
}
|
||||
|
||||
if cs.RegionName == "" {
|
||||
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
|
||||
return errors.New("no " + awsRegionEnvVar + " set in environment or configuration")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) stsPath() string {
|
||||
var domain string
|
||||
if cs.Domain != "" {
|
||||
domain = strings.ToLower(cs.Domain)
|
||||
} else {
|
||||
domain = stsDefaultDomain
|
||||
}
|
||||
|
||||
var stsPath string
|
||||
switch {
|
||||
case cs.stsURL != "":
|
||||
stsPath = cs.stsURL
|
||||
case cs.RegionName != "":
|
||||
stsPath = fmt.Sprintf(stsRegionPath, strings.ToLower(cs.RegionName), domain)
|
||||
default:
|
||||
stsPath = fmt.Sprintf(stsDefaultPath, domain)
|
||||
}
|
||||
return stsPath
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) refreshFromService(ctx context.Context) error {
|
||||
// define the expected JSON payload from the EC2 credential service
|
||||
// ref. https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html
|
||||
type responsePayload struct {
|
||||
Result struct {
|
||||
Credentials struct {
|
||||
SessionToken string
|
||||
SecretAccessKey string
|
||||
Expiration time.Time
|
||||
AccessKeyID string `xml:"AccessKeyId"`
|
||||
}
|
||||
} `xml:"AssumeRoleWithWebIdentityResult"`
|
||||
}
|
||||
|
||||
// short circuit if a reasonable amount of time until credential expiration remains
|
||||
if time.Now().Add(time.Minute * 5).Before(cs.expiration) {
|
||||
cs.logger.Debug("Credentials previously obtained from sts service still valid.")
|
||||
return nil
|
||||
}
|
||||
|
||||
cs.logger.Debug("Obtaining credentials from sts for role %s.", cs.RoleArn)
|
||||
|
||||
var sessionName string
|
||||
if cs.SessionName == "" {
|
||||
sessionName = "open-policy-agent"
|
||||
} else {
|
||||
sessionName = cs.SessionName
|
||||
}
|
||||
|
||||
tokenData, err := os.ReadFile(cs.WebIdentityTokenFile)
|
||||
if err != nil {
|
||||
return errors.New("unable to read web token for sts HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
token := string(tokenData)
|
||||
|
||||
queryVals := url.Values{
|
||||
"Action": []string{"AssumeRoleWithWebIdentity"},
|
||||
"RoleSessionName": []string{sessionName},
|
||||
"RoleArn": []string{cs.RoleArn},
|
||||
"WebIdentityToken": []string{token},
|
||||
"Version": []string{"2011-06-15"},
|
||||
}
|
||||
stsRequestURL, _ := url.Parse(cs.stsPath())
|
||||
|
||||
// construct an HTTP client with a reasonably short timeout
|
||||
client := &http.Client{Timeout: time.Second * 10}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, stsRequestURL.String(), strings.NewReader(queryVals.Encode()))
|
||||
if err != nil {
|
||||
return errors.New("unable to construct STS HTTP request: " + err.Error())
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
body, err := aws.DoRequestWithClient(req, client, "STS", cs.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload responsePayload
|
||||
err = xml.Unmarshal(body, &payload)
|
||||
if err != nil {
|
||||
return errors.New("failed to parse credential response from STS service: " + err.Error())
|
||||
}
|
||||
|
||||
cs.expiration = payload.Result.Credentials.Expiration
|
||||
cs.creds.AccessKey = payload.Result.Credentials.AccessKeyID
|
||||
cs.creds.SecretKey = payload.Result.Credentials.SecretAccessKey
|
||||
cs.creds.SessionToken = payload.Result.Credentials.SessionToken
|
||||
cs.creds.RegionName = cs.RegionName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *awsWebIdentityCredentialService) credentials(ctx context.Context) (aws.Credentials, error) {
|
||||
err := cs.refreshFromService(ctx)
|
||||
if err != nil {
|
||||
return cs.creds, err
|
||||
}
|
||||
return cs.creds, nil
|
||||
}
|
||||
|
||||
func isECS() bool {
|
||||
// the special relative path URI is set by the container agent in the ECS environment only
|
||||
_, isECS := os.LookupEnv(ecsRelativePathEnvVar)
|
||||
return isECS
|
||||
}
|
||||
|
||||
// ecrAuthPlugin authorizes requests to AWS ECR.
|
||||
type ecrAuthPlugin struct {
|
||||
token aws.ECRAuthorizationToken
|
||||
|
||||
// awsAuthPlugin is used to sign ecr authorization token requests.
|
||||
awsAuthPlugin *awsSigningAuthPlugin
|
||||
|
||||
// ecr represents the service we request tokens from.
|
||||
ecr ecr
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type ecr interface {
|
||||
GetAuthorizationToken(context.Context, aws.Credentials, string) (aws.ECRAuthorizationToken, error)
|
||||
}
|
||||
|
||||
func newECRAuthPlugin(ap *awsSigningAuthPlugin) *ecrAuthPlugin {
|
||||
return &ecrAuthPlugin{
|
||||
awsAuthPlugin: ap,
|
||||
ecr: aws.NewECR(ap.logger),
|
||||
logger: ap.logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare should be called with any request to AWS ECR.
|
||||
// It takes care of retrieving an ECR authorization token to sign
|
||||
// the request with.
|
||||
func (ap *ecrAuthPlugin) Prepare(r *http.Request) error {
|
||||
if !ap.token.IsValid() {
|
||||
ap.logger.Debug("Refreshing ECR auth token")
|
||||
if err := ap.refreshAuthorizationToken(r.Context()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ap.logger.Debug("Signing request with ECR authorization token")
|
||||
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Basic %s", ap.token.AuthorizationToken))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *ecrAuthPlugin) refreshAuthorizationToken(ctx context.Context) error {
|
||||
creds, err := ap.awsAuthPlugin.awsCredentialService().credentials(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
token, err := ap.ecr.GetAuthorizationToken(ctx, creds, ap.awsAuthPlugin.AWSSignatureVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ecr: failed to get authorization token: %w", err)
|
||||
}
|
||||
|
||||
ap.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
// awsKMSSignPlugin signs digests using AWS KMS.
|
||||
type awsKMSSignPlugin struct {
|
||||
|
||||
// awsAuthPlugin is used to sign kms sign requests.
|
||||
awsAuthPlugin *awsSigningAuthPlugin
|
||||
|
||||
// kms represents the service for signing digests.
|
||||
kms awskms
|
||||
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
type awskms interface {
|
||||
SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string, creds aws.Credentials, signatureVersion string) (string, error)
|
||||
}
|
||||
|
||||
func newKMSSignPlugin(ap *awsSigningAuthPlugin) *awsKMSSignPlugin {
|
||||
return &awsKMSSignPlugin{
|
||||
awsAuthPlugin: ap,
|
||||
kms: aws.NewKMS(ap.logger),
|
||||
logger: ap.logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (ap *awsKMSSignPlugin) SignDigest(ctx context.Context, digest []byte, keyID string, signingAlgorithm string) (string, error) {
|
||||
creds, err := ap.awsAuthPlugin.awsCredentialService().credentials(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get aws credentials: %w", err)
|
||||
}
|
||||
|
||||
signature, err := ap.kms.SignDigest(ctx, digest, keyID, signingAlgorithm, creds, ap.awsAuthPlugin.AWSSignatureVersion)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("kms: failed to sign digest: %w", err)
|
||||
}
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
|
||||
defaultAPIVersion = "2018-02-01"
|
||||
defaultResource = "https://storage.azure.com/"
|
||||
timeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// azureManagedIdentitiesToken holds a token for managed identities for Azure resources
|
||||
type azureManagedIdentitiesToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
ExpiresOn string `json:"expires_on"`
|
||||
NotBefore string `json:"not_before"`
|
||||
Resource string `json:"resource"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesError represents an error fetching an azureManagedIdentitiesToken
|
||||
type azureManagedIdentitiesError struct {
|
||||
Err string `json:"error"`
|
||||
Description string `json:"error_description"`
|
||||
Endpoint string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (e *azureManagedIdentitiesError) Error() string {
|
||||
return fmt.Sprintf("%v %s retrieving azure token from %s: %s", e.StatusCode, e.Err, e.Endpoint, e.Description)
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesAuthPlugin uses an azureManagedIdentitiesToken.AccessToken for bearer authorization
|
||||
type azureManagedIdentitiesAuthPlugin struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
APIVersion string `json:"api_version"`
|
||||
Resource string `json:"resource"`
|
||||
ObjectID string `json:"object_id"`
|
||||
ClientID string `json:"client_id"`
|
||||
MiResID string `json:"mi_res_id"`
|
||||
}
|
||||
|
||||
func (ap *azureManagedIdentitiesAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
if c.Type == "oci" {
|
||||
return nil, errors.New("azure managed identities auth: OCI service not supported")
|
||||
}
|
||||
|
||||
if ap.Endpoint == "" {
|
||||
ap.Endpoint = azureIMDSEndpoint
|
||||
}
|
||||
|
||||
if ap.Resource == "" {
|
||||
ap.Resource = defaultResource
|
||||
}
|
||||
|
||||
if ap.APIVersion == "" {
|
||||
ap.APIVersion = defaultAPIVersion
|
||||
}
|
||||
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *azureManagedIdentitiesAuthPlugin) Prepare(req *http.Request) error {
|
||||
token, err := azureManagedIdentitiesTokenRequest(
|
||||
ap.Endpoint, ap.APIVersion, ap.Resource,
|
||||
ap.ObjectID, ap.ClientID, ap.MiResID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
|
||||
return nil
|
||||
}
|
||||
|
||||
// azureManagedIdentitiesTokenRequest fetches an azureManagedIdentitiesToken
|
||||
func azureManagedIdentitiesTokenRequest(
|
||||
endpoint, apiVersion, resource, objectID, clientID, miResID string,
|
||||
) (azureManagedIdentitiesToken, error) {
|
||||
var token azureManagedIdentitiesToken
|
||||
e := buildAzureManagedIdentitiesRequestPath(endpoint, apiVersion, resource, objectID, clientID, miResID)
|
||||
|
||||
request, err := http.NewRequest("GET", e, nil)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
request.Header.Add("Metadata", "true")
|
||||
|
||||
httpClient := http.Client{Timeout: timeout}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
if s := response.StatusCode; s != http.StatusOK {
|
||||
var azureError azureManagedIdentitiesError
|
||||
err = json.Unmarshal(data, &azureError)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
azureError.Endpoint = e
|
||||
azureError.StatusCode = s
|
||||
return token, &azureError
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &token)
|
||||
if err != nil {
|
||||
return token, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// buildAzureManagedIdentitiesRequestPath constructs the request URL for an Azure managed identities token request
|
||||
func buildAzureManagedIdentitiesRequestPath(
|
||||
endpoint, apiVersion, resource, objectID, clientID, miResID string,
|
||||
) string {
|
||||
params := url.Values{
|
||||
"api-version": []string{apiVersion},
|
||||
"resource": []string{resource},
|
||||
}
|
||||
|
||||
if objectID != "" {
|
||||
params.Add("object_id", objectID)
|
||||
}
|
||||
|
||||
if clientID != "" {
|
||||
params.Add("client_id", clientID)
|
||||
}
|
||||
|
||||
if miResID != "" {
|
||||
params.Add("mi_res_id", miResID)
|
||||
}
|
||||
|
||||
return endpoint + "?" + params.Encode()
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
// 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 rest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultGCPMetadataEndpoint = "http://metadata.google.internal"
|
||||
defaultAccessTokenPath = "/computeMetadata/v1/instance/service-accounts/default/token"
|
||||
defaultIdentityTokenPath = "/computeMetadata/v1/instance/service-accounts/default/identity"
|
||||
)
|
||||
|
||||
// AccessToken holds a GCP access token.
|
||||
type AccessToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type gcpMetadataError struct {
|
||||
err error
|
||||
endpoint string
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (e *gcpMetadataError) Error() string {
|
||||
return fmt.Sprintf("error retrieving gcp ID token from %s %d: %v", e.endpoint, e.statusCode, e.err)
|
||||
}
|
||||
|
||||
func (e *gcpMetadataError) Unwrap() error { return e.err }
|
||||
|
||||
var (
|
||||
errGCPMetadataNotFound = errors.New("not found")
|
||||
errGCPMetadataInvalidRequest = errors.New("invalid request")
|
||||
errGCPMetadataUnexpected = errors.New("unexpected error")
|
||||
)
|
||||
|
||||
// gcpMetadataAuthPlugin represents authentication via GCP metadata service.
|
||||
type gcpMetadataAuthPlugin struct {
|
||||
AccessTokenPath string `json:"access_token_path"`
|
||||
Audience string `json:"audience"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
IdentityTokenPath string `json:"identity_token_path"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
func (ap *gcpMetadataAuthPlugin) NewClient(c Config) (*http.Client, error) {
|
||||
if ap.Audience == "" && len(ap.Scopes) == 0 {
|
||||
return nil, errors.New("audience or scopes is required when gcp metadata is enabled")
|
||||
}
|
||||
|
||||
if ap.Audience != "" && len(ap.Scopes) > 0 {
|
||||
return nil, errors.New("either audience or scopes can be set, not both, when gcp metadata is enabled")
|
||||
}
|
||||
|
||||
if ap.Endpoint == "" {
|
||||
ap.Endpoint = defaultGCPMetadataEndpoint
|
||||
}
|
||||
|
||||
if ap.AccessTokenPath == "" {
|
||||
ap.AccessTokenPath = defaultAccessTokenPath
|
||||
}
|
||||
|
||||
if ap.IdentityTokenPath == "" {
|
||||
ap.IdentityTokenPath = defaultIdentityTokenPath
|
||||
}
|
||||
|
||||
t, err := DefaultTLSConfig(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
|
||||
}
|
||||
|
||||
func (ap *gcpMetadataAuthPlugin) Prepare(req *http.Request) error {
|
||||
var err error
|
||||
var token string
|
||||
|
||||
if ap.Audience != "" {
|
||||
token, err = identityTokenFromMetadataService(ap.Endpoint, ap.IdentityTokenPath, ap.Audience)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving identity token from gcp metadata service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ap.Scopes) != 0 {
|
||||
token, err = accessTokenFromMetadataService(ap.Endpoint, ap.AccessTokenPath, ap.Scopes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error retrieving access token from gcp metadata service: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token))
|
||||
return nil
|
||||
}
|
||||
|
||||
// accessTokenFromMetadataService returns an access token based on the scopes.
|
||||
func accessTokenFromMetadataService(endpoint, path string, scopes []string) (string, error) {
|
||||
s := strings.Join(scopes, ",")
|
||||
|
||||
e := fmt.Sprintf("%s%s?scopes=%s", endpoint, path, s)
|
||||
|
||||
data, err := gcpMetadataServiceRequest(e)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var accessToken AccessToken
|
||||
err = json.Unmarshal(data, &accessToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return accessToken.AccessToken, nil
|
||||
}
|
||||
|
||||
// identityTokenFromMetadataService returns an identity token based on the audience.
|
||||
func identityTokenFromMetadataService(endpoint, path, audience string) (string, error) {
|
||||
e := fmt.Sprintf("%s%s?audience=%s", endpoint, path, audience)
|
||||
|
||||
data, err := gcpMetadataServiceRequest(e)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func gcpMetadataServiceRequest(endpoint string) ([]byte, error) {
|
||||
request, err := http.NewRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Add("Metadata-Flavor", "Google")
|
||||
|
||||
timeout := time.Duration(5) * time.Second
|
||||
httpClient := http.Client{Timeout: timeout}
|
||||
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
switch s := response.StatusCode; s {
|
||||
case 200:
|
||||
break
|
||||
case 400:
|
||||
return nil, &gcpMetadataError{errGCPMetadataInvalidRequest, endpoint, s}
|
||||
case 404:
|
||||
return nil, &gcpMetadataError{errGCPMetadataNotFound, endpoint, s}
|
||||
default:
|
||||
return nil, &gcpMetadataError{errGCPMetadataUnexpected, endpoint, s}
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
// Copyright 2018 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 rest implements a REST client for communicating with remote services.
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/version"
|
||||
"github.com/open-policy-agent/opa/keys"
|
||||
"github.com/open-policy-agent/opa/logging"
|
||||
"github.com/open-policy-agent/opa/tracing"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultResponseHeaderTimeoutSeconds = int64(10)
|
||||
defaultResponseSizeLimitBytes = 1024
|
||||
|
||||
grantTypeClientCredentials = "client_credentials"
|
||||
grantTypeJwtBearer = "jwt_bearer"
|
||||
)
|
||||
|
||||
var maskedHeaderKeys = map[string]struct{}{
|
||||
"Authorization": {},
|
||||
"X-Amz-Security-Token": {},
|
||||
}
|
||||
|
||||
// An HTTPAuthPlugin represents a mechanism to construct and configure HTTP authentication for a REST service
|
||||
type HTTPAuthPlugin interface {
|
||||
// implementations can assume NewClient will be called before Prepare
|
||||
NewClient(Config) (*http.Client, error)
|
||||
Prepare(*http.Request) error
|
||||
}
|
||||
|
||||
// Config represents configuration for a REST client.
|
||||
type Config struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
AllowInsecureTLS bool `json:"allow_insecure_tls,omitempty"`
|
||||
ResponseHeaderTimeoutSeconds *int64 `json:"response_header_timeout_seconds,omitempty"`
|
||||
TLS *serverTLSConfig `json:"tls,omitempty"`
|
||||
Credentials struct {
|
||||
Bearer *bearerAuthPlugin `json:"bearer,omitempty"`
|
||||
OAuth2 *oauth2ClientCredentialsAuthPlugin `json:"oauth2,omitempty"`
|
||||
ClientTLS *clientTLSAuthPlugin `json:"client_tls,omitempty"`
|
||||
S3Signing *awsSigningAuthPlugin `json:"s3_signing,omitempty"`
|
||||
GCPMetadata *gcpMetadataAuthPlugin `json:"gcp_metadata,omitempty"`
|
||||
AzureManagedIdentity *azureManagedIdentitiesAuthPlugin `json:"azure_managed_identity,omitempty"`
|
||||
Plugin *string `json:"plugin,omitempty"`
|
||||
} `json:"credentials"`
|
||||
Type string `json:"type,omitempty"`
|
||||
keys map[string]*keys.Config
|
||||
logger logging.Logger
|
||||
}
|
||||
|
||||
// Equal returns true if this client config is equal to the other.
|
||||
func (c *Config) Equal(other *Config) bool {
|
||||
otherWithoutLogger := *other
|
||||
otherWithoutLogger.logger = c.logger
|
||||
return reflect.DeepEqual(c, &otherWithoutLogger)
|
||||
}
|
||||
|
||||
// An AuthPluginLookupFunc can lookup auth plugins by their name.
|
||||
type AuthPluginLookupFunc func(name string) HTTPAuthPlugin
|
||||
|
||||
// AuthPlugin should be used to get an authentication method from the config.
|
||||
func (c *Config) AuthPlugin(lookup AuthPluginLookupFunc) (HTTPAuthPlugin, error) {
|
||||
var candidate HTTPAuthPlugin
|
||||
if c.Credentials.Plugin != nil {
|
||||
if lookup == nil {
|
||||
// if no authPluginLookup function is passed we can't resolve the plugin
|
||||
return nil, errors.New("missing auth plugin lookup function")
|
||||
}
|
||||
|
||||
candidate := lookup(*c.Credentials.Plugin)
|
||||
if candidate == nil {
|
||||
return nil, fmt.Errorf("auth plugin %q not found", *c.Credentials.Plugin)
|
||||
}
|
||||
|
||||
return candidate, nil
|
||||
}
|
||||
// reflection avoids need for this code to change as auth plugins are added
|
||||
s := reflect.ValueOf(c.Credentials)
|
||||
for i := 0; i < s.NumField(); i++ {
|
||||
if s.Field(i).IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
if candidate != nil {
|
||||
return nil, errors.New("a maximum one credential method must be specified")
|
||||
}
|
||||
|
||||
candidate = s.Field(i).Interface().(HTTPAuthPlugin)
|
||||
}
|
||||
|
||||
if candidate == nil {
|
||||
return &defaultAuthPlugin{}, nil
|
||||
}
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func (c *Config) authHTTPClient(lookup AuthPluginLookupFunc) (*http.Client, error) {
|
||||
plugin, err := c.AuthPlugin(lookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return plugin.NewClient(*c)
|
||||
}
|
||||
|
||||
func (c *Config) authPrepare(req *http.Request, lookup AuthPluginLookupFunc) error {
|
||||
plugin, err := c.AuthPlugin(lookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return plugin.Prepare(req)
|
||||
}
|
||||
|
||||
// Client implements an HTTP/REST client for communicating with remote
|
||||
// services.
|
||||
type Client struct {
|
||||
bytes *[]byte
|
||||
json *interface{}
|
||||
config Config
|
||||
headers map[string]string
|
||||
authPluginLookup AuthPluginLookupFunc
|
||||
logger logging.Logger
|
||||
loggerFields map[string]interface{}
|
||||
distributedTacingOpts tracing.Options
|
||||
}
|
||||
|
||||
// Name returns an option that overrides the service name on the client.
|
||||
func Name(s string) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.config.Name = s
|
||||
}
|
||||
}
|
||||
|
||||
// AuthPluginLookup assigns a function to lookup an HTTPAuthPlugin to a new Client.
|
||||
// It's intended to be used when creating a Client using New(). Usually this is passed
|
||||
// the plugins.AuthPlugin func, which retrieves a registered HTTPAuthPlugin from the
|
||||
// plugin manager.
|
||||
func AuthPluginLookup(l AuthPluginLookupFunc) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.authPluginLookup = l
|
||||
}
|
||||
}
|
||||
|
||||
// Logger assigns a logger to the client
|
||||
func Logger(l logging.Logger) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// DistributedTracingOpts sets the options to be used by distributed tracing.
|
||||
func DistributedTracingOpts(tr tracing.Options) func(*Client) {
|
||||
return func(c *Client) {
|
||||
c.distributedTacingOpts = tr
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new Client for config.
|
||||
func New(config []byte, keys map[string]*keys.Config, opts ...func(*Client)) (Client, error) {
|
||||
var parsedConfig Config
|
||||
if err := util.Unmarshal(config, &parsedConfig); err != nil {
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
parsedConfig.URL = strings.TrimRight(parsedConfig.URL, "/")
|
||||
|
||||
if parsedConfig.ResponseHeaderTimeoutSeconds == nil {
|
||||
timeout := defaultResponseHeaderTimeoutSeconds
|
||||
parsedConfig.ResponseHeaderTimeoutSeconds = &timeout
|
||||
}
|
||||
|
||||
parsedConfig.keys = keys
|
||||
|
||||
client := Client{
|
||||
config: parsedConfig,
|
||||
}
|
||||
|
||||
for _, f := range opts {
|
||||
f(&client)
|
||||
}
|
||||
|
||||
if client.logger == nil {
|
||||
client.logger = logging.Get()
|
||||
}
|
||||
|
||||
client.config.logger = client.logger
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// AuthPluginLookup returns the lookup function to find a custom registered
|
||||
// auth plugin by its name.
|
||||
func (c Client) AuthPluginLookup() AuthPluginLookupFunc {
|
||||
return c.authPluginLookup
|
||||
}
|
||||
|
||||
// Service returns the name of the service this Client is configured for.
|
||||
func (c Client) Service() string {
|
||||
return c.config.Name
|
||||
}
|
||||
|
||||
// Config returns this Client's configuration
|
||||
func (c Client) Config() *Config {
|
||||
return &c.config
|
||||
}
|
||||
|
||||
// SetResponseHeaderTimeout sets the "ResponseHeaderTimeout" in the http client's Transport
|
||||
func (c Client) SetResponseHeaderTimeout(timeout *int64) Client {
|
||||
c.config.ResponseHeaderTimeoutSeconds = timeout
|
||||
return c
|
||||
}
|
||||
|
||||
// Logger returns the logger assigned to the Client
|
||||
func (c Client) Logger() logging.Logger {
|
||||
return c.logger
|
||||
}
|
||||
|
||||
// LoggerFields returns the fields used for log statements used by Client
|
||||
func (c Client) LoggerFields() map[string]interface{} {
|
||||
return c.loggerFields
|
||||
}
|
||||
|
||||
// WithHeader returns a shallow copy of the client with a header to include the
|
||||
// requests.
|
||||
func (c Client) WithHeader(k, v string) Client {
|
||||
if v == "" {
|
||||
return c
|
||||
}
|
||||
if c.headers == nil {
|
||||
c.headers = map[string]string{}
|
||||
}
|
||||
c.headers[k] = v
|
||||
return c
|
||||
}
|
||||
|
||||
// WithJSON returns a shallow copy of the client with the JSON value set as the
|
||||
// message body to include the requests. This function sets the Content-Type
|
||||
// header.
|
||||
func (c Client) WithJSON(body interface{}) Client {
|
||||
c = c.WithHeader("Content-Type", "application/json")
|
||||
c.json = &body
|
||||
return c
|
||||
}
|
||||
|
||||
// WithBytes returns a shallow copy of the client with the bytes set as the
|
||||
// message body to include in the requests.
|
||||
func (c Client) WithBytes(body []byte) Client {
|
||||
c.bytes = &body
|
||||
return c
|
||||
}
|
||||
|
||||
// Do executes a request using the client.
|
||||
func (c Client) Do(ctx context.Context, method, path string) (*http.Response, error) {
|
||||
|
||||
httpClient, err := c.config.authHTTPClient(c.authPluginLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.distributedTacingOpts) > 0 {
|
||||
httpClient.Transport = tracing.NewTransport(httpClient.Transport, c.distributedTacingOpts)
|
||||
}
|
||||
|
||||
path = strings.Trim(path, "/")
|
||||
|
||||
var body io.Reader
|
||||
|
||||
if c.bytes != nil {
|
||||
body = bytes.NewReader(*c.bytes)
|
||||
} else if c.json != nil {
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(*c.json); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body = &buf
|
||||
}
|
||||
|
||||
url := c.config.URL + "/" + path
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
headers := map[string]string{
|
||||
"User-Agent": version.UserAgent,
|
||||
}
|
||||
|
||||
// Copy custom headers from config.
|
||||
for key, value := range c.config.Headers {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
// Overwrite with headers set directly on client.
|
||||
for key, value := range c.headers {
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
for key, value := range headers {
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
err = c.config.authPrepare(req, c.authPluginLookup)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.logger.GetLevel() >= logging.Debug {
|
||||
c.loggerFields = map[string]interface{}{
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": withMaskedHeaders(req.Header),
|
||||
}
|
||||
|
||||
c.logger.WithFields(c.loggerFields).Debug("Sending request.")
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
|
||||
if resp != nil && c.logger.GetLevel() >= logging.Debug {
|
||||
// Only log for debug purposes. If an error occurred, the caller should handle
|
||||
// that. In the non-error case, the caller may not do anything.
|
||||
c.loggerFields["status"] = resp.Status
|
||||
c.loggerFields["headers"] = resp.Header
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
dump, err := httputil.DumpResponse(resp, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(string(dump)) < defaultResponseSizeLimitBytes {
|
||||
c.loggerFields["response"] = string(dump)
|
||||
} else {
|
||||
c.loggerFields["response"] = fmt.Sprintf("%v...", string(dump[:defaultResponseSizeLimitBytes]))
|
||||
}
|
||||
}
|
||||
c.logger.WithFields(c.loggerFields).Debug("Received response.")
|
||||
}
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func withMaskedHeaders(headers http.Header) http.Header {
|
||||
masked := make(http.Header)
|
||||
for k, v := range headers {
|
||||
if _, ok := maskedHeaderKeys[k]; ok {
|
||||
masked.Set(k, "REDACTED")
|
||||
} else {
|
||||
masked[k] = v
|
||||
}
|
||||
}
|
||||
return masked
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2023 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 rego
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/ir"
|
||||
)
|
||||
|
||||
var targetPlugins = map[string]TargetPlugin{}
|
||||
var pluginMtx sync.Mutex
|
||||
|
||||
type TargetPlugin interface {
|
||||
IsTarget(string) bool
|
||||
PrepareForEval(context.Context, *ir.Policy, ...PrepareOption) (TargetPluginEval, error)
|
||||
}
|
||||
|
||||
type TargetPluginEval interface {
|
||||
Eval(context.Context, *EvalContext, ast.Value) (ast.Value, error)
|
||||
}
|
||||
|
||||
func (r *Rego) targetPlugin(tgt string) TargetPlugin {
|
||||
for _, p := range targetPlugins {
|
||||
if p.IsTarget(tgt) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RegisterPlugin(name string, p TargetPlugin) {
|
||||
pluginMtx.Lock()
|
||||
defer pluginMtx.Unlock()
|
||||
if _, ok := targetPlugins[name]; ok {
|
||||
panic("plugin already registered " + name)
|
||||
}
|
||||
targetPlugins[name] = p
|
||||
}
|
||||
+244
-92
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/open-policy-agent/opa/ir"
|
||||
"github.com/open-policy-agent/opa/loader"
|
||||
"github.com/open-policy-agent/opa/metrics"
|
||||
"github.com/open-policy-agent/opa/plugins"
|
||||
"github.com/open-policy-agent/opa/resolver"
|
||||
"github.com/open-policy-agent/opa/storage"
|
||||
"github.com/open-policy-agent/opa/storage/inmem"
|
||||
@@ -122,6 +123,55 @@ type EvalContext struct {
|
||||
copyMaps bool
|
||||
printHook print.Hook
|
||||
capabilities *ast.Capabilities
|
||||
strictBuiltinErrors bool
|
||||
}
|
||||
|
||||
func (e *EvalContext) RawInput() *interface{} {
|
||||
return e.rawInput
|
||||
}
|
||||
|
||||
func (e *EvalContext) ParsedInput() ast.Value {
|
||||
return e.parsedInput
|
||||
}
|
||||
|
||||
func (e *EvalContext) Time() time.Time {
|
||||
return e.time
|
||||
}
|
||||
|
||||
func (e *EvalContext) Seed() io.Reader {
|
||||
return e.seed
|
||||
}
|
||||
|
||||
func (e *EvalContext) InterQueryBuiltinCache() cache.InterQueryCache {
|
||||
return e.interQueryBuiltinCache
|
||||
}
|
||||
|
||||
func (e *EvalContext) PrintHook() print.Hook {
|
||||
return e.printHook
|
||||
}
|
||||
|
||||
func (e *EvalContext) Metrics() metrics.Metrics {
|
||||
return e.metrics
|
||||
}
|
||||
|
||||
func (e *EvalContext) StrictBuiltinErrors() bool {
|
||||
return e.strictBuiltinErrors
|
||||
}
|
||||
|
||||
func (e *EvalContext) NDBCache() builtins.NDBCache {
|
||||
return e.ndBuiltinCache
|
||||
}
|
||||
|
||||
func (e *EvalContext) CompiledQuery() ast.Body {
|
||||
return e.compiledQuery.query
|
||||
}
|
||||
|
||||
func (e *EvalContext) Capabilities() *ast.Capabilities {
|
||||
return e.capabilities
|
||||
}
|
||||
|
||||
func (e *EvalContext) Transaction() storage.Transaction {
|
||||
return e.txn
|
||||
}
|
||||
|
||||
// EvalOption defines a function to set an option on an EvalConfig
|
||||
@@ -314,23 +364,24 @@ func (pq preparedQuery) Modules() map[string]*ast.Module {
|
||||
// been opened.
|
||||
func (pq preparedQuery) newEvalContext(ctx context.Context, options []EvalOption) (*EvalContext, func(context.Context), error) {
|
||||
ectx := &EvalContext{
|
||||
hasInput: false,
|
||||
rawInput: nil,
|
||||
parsedInput: nil,
|
||||
metrics: nil,
|
||||
txn: nil,
|
||||
instrument: false,
|
||||
instrumentation: nil,
|
||||
partialNamespace: pq.r.partialNamespace,
|
||||
queryTracers: nil,
|
||||
unknowns: pq.r.unknowns,
|
||||
parsedUnknowns: pq.r.parsedUnknowns,
|
||||
compiledQuery: compiledQuery{},
|
||||
indexing: true,
|
||||
earlyExit: true,
|
||||
resolvers: pq.r.resolvers,
|
||||
printHook: pq.r.printHook,
|
||||
capabilities: pq.r.capabilities,
|
||||
hasInput: false,
|
||||
rawInput: nil,
|
||||
parsedInput: nil,
|
||||
metrics: nil,
|
||||
txn: nil,
|
||||
instrument: false,
|
||||
instrumentation: nil,
|
||||
partialNamespace: pq.r.partialNamespace,
|
||||
queryTracers: nil,
|
||||
unknowns: pq.r.unknowns,
|
||||
parsedUnknowns: pq.r.parsedUnknowns,
|
||||
compiledQuery: compiledQuery{},
|
||||
indexing: true,
|
||||
earlyExit: true,
|
||||
resolvers: pq.r.resolvers,
|
||||
printHook: pq.r.printHook,
|
||||
capabilities: pq.r.capabilities,
|
||||
strictBuiltinErrors: pq.r.strictBuiltinErrors,
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
@@ -377,7 +428,9 @@ func (pq preparedQuery) newEvalContext(ctx context.Context, options []EvalOption
|
||||
// Note that it could still be nil
|
||||
ectx.rawInput = pq.r.rawInput
|
||||
}
|
||||
if pq.r.target != targetWasm {
|
||||
|
||||
if pq.r.targetPlugin(pq.r.target) == nil && // no plugin claims this target
|
||||
pq.r.target != targetWasm {
|
||||
ectx.parsedInput, err = pq.r.parseRawInput(ectx.rawInput, ectx.metrics)
|
||||
if err != nil {
|
||||
return nil, finishFunc, err
|
||||
@@ -471,10 +524,10 @@ type queryType int
|
||||
// Define a query type for each of the top level Rego
|
||||
// API's that compile queries differently.
|
||||
const (
|
||||
evalQueryType queryType = iota
|
||||
partialResultQueryType queryType = iota
|
||||
partialQueryType queryType = iota
|
||||
compileQueryType queryType = iota
|
||||
evalQueryType queryType = iota
|
||||
partialResultQueryType
|
||||
partialQueryType
|
||||
compileQueryType
|
||||
)
|
||||
|
||||
type loadPaths struct {
|
||||
@@ -538,6 +591,9 @@ type Rego struct {
|
||||
enablePrintStatements bool
|
||||
distributedTacingOpts tracing.Options
|
||||
strict bool
|
||||
pluginMgr *plugins.Manager
|
||||
plugins []TargetPlugin
|
||||
targetPrepState TargetPluginEval
|
||||
}
|
||||
|
||||
// Function represents a built-in function that is callable in Rego.
|
||||
@@ -1157,6 +1213,12 @@ func New(options ...func(r *Rego)) *Rego {
|
||||
WithEnablePrintStatements(r.enablePrintStatements).
|
||||
WithStrict(r.strict).
|
||||
WithUseTypeCheckAnnotations(true)
|
||||
|
||||
// topdown could be target "" or "rego", but both could be overridden by
|
||||
// a target plugin (checked below)
|
||||
if r.target == targetWasm {
|
||||
r.compiler = r.compiler.WithEvalMode(ast.EvalModeIR)
|
||||
}
|
||||
}
|
||||
|
||||
if r.store == nil {
|
||||
@@ -1188,6 +1250,19 @@ func New(options ...func(r *Rego)) *Rego {
|
||||
r.generateJSON = generateJSON
|
||||
}
|
||||
|
||||
if r.pluginMgr != nil {
|
||||
for _, name := range r.pluginMgr.Plugins() {
|
||||
p := r.pluginMgr.Plugin(name)
|
||||
if p0, ok := p.(TargetPlugin); ok {
|
||||
r.plugins = append(r.plugins, p0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if t := r.targetPlugin(r.target); t != nil {
|
||||
r.compiler = r.compiler.WithEvalMode(ast.EvalModeIR)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -1401,64 +1476,32 @@ func (r *Rego) Compile(ctx context.Context, opts ...CompileOption) (*CompileResu
|
||||
queries = []ast.Body{r.compiledQueries[compileQueryType].query}
|
||||
}
|
||||
|
||||
return r.compileWasm(modules, queries, compileQueryType)
|
||||
if tgt := r.targetPlugin(r.target); tgt != nil {
|
||||
return nil, fmt.Errorf("unsupported for rego target plugins")
|
||||
}
|
||||
|
||||
return r.compileWasm(modules, queries, compileQueryType) // TODO(sr) control flow is funky here
|
||||
}
|
||||
|
||||
func (r *Rego) compileWasm(modules []*ast.Module, queries []ast.Body, qType queryType) (*CompileResult, error) {
|
||||
decls := make(map[string]*ast.Builtin, len(r.builtinDecls)+len(ast.BuiltinMap))
|
||||
|
||||
for k, v := range ast.BuiltinMap {
|
||||
decls[k] = v
|
||||
}
|
||||
|
||||
for k, v := range r.builtinDecls {
|
||||
decls[k] = v
|
||||
}
|
||||
|
||||
const queryName = "eval" // NOTE(tsandall): the query name is arbitrary
|
||||
|
||||
p := planner.New().
|
||||
WithQueries([]planner.QuerySet{
|
||||
{
|
||||
Name: queryName,
|
||||
Queries: queries,
|
||||
RewrittenVars: r.compiledQueries[qType].compiler.RewrittenVars(),
|
||||
},
|
||||
}).
|
||||
WithModules(modules).
|
||||
WithBuiltinDecls(decls).
|
||||
WithDebug(r.dump)
|
||||
policy, err := p.Plan()
|
||||
policy, err := r.planQuery(queries, qType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.dump != nil {
|
||||
fmt.Fprintln(r.dump, "PLAN:")
|
||||
fmt.Fprintln(r.dump, "-----")
|
||||
err = ir.Pretty(r.dump, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintln(r.dump)
|
||||
}
|
||||
|
||||
m, err := wasm.New().WithPolicy(policy).Compile()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
if err := encoding.WriteModule(&out, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &CompileResult{
|
||||
return &CompileResult{
|
||||
Bytes: out.Bytes(),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PrepareOption defines a function to set an option to control
|
||||
@@ -1470,6 +1513,7 @@ type PrepareOption func(*PrepareConfig)
|
||||
type PrepareConfig struct {
|
||||
doPartialEval bool
|
||||
disableInlining *[]string
|
||||
builtinFuncs map[string]*topdown.Builtin
|
||||
}
|
||||
|
||||
// WithPartialEval configures an option for PrepareForEval
|
||||
@@ -1488,6 +1532,25 @@ func WithNoInline(paths []string) PrepareOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithBuiltinFuncs carries the rego.Function{1,2,3} per-query function definitions
|
||||
// to the target plugins.
|
||||
func WithBuiltinFuncs(bis map[string]*topdown.Builtin) PrepareOption {
|
||||
return func(p *PrepareConfig) {
|
||||
if p.builtinFuncs == nil {
|
||||
p.builtinFuncs = make(map[string]*topdown.Builtin, len(bis))
|
||||
}
|
||||
for k, v := range bis {
|
||||
p.builtinFuncs[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuiltinFuncs allows retrieving the builtin funcs set via PrepareOption
|
||||
// WithBuiltinFuncs.
|
||||
func (p *PrepareConfig) BuiltinFuncs() map[string]*topdown.Builtin {
|
||||
return p.builtinFuncs
|
||||
}
|
||||
|
||||
// PrepareForEval will parse inputs, modules, and query arguments in preparation
|
||||
// of evaluating them.
|
||||
func (r *Rego) PrepareForEval(ctx context.Context, opts ...PrepareOption) (PreparedEvalQuery, error) {
|
||||
@@ -1541,7 +1604,8 @@ func (r *Rego) PrepareForEval(ctx context.Context, opts ...PrepareOption) (Prepa
|
||||
return PreparedEvalQuery{}, err
|
||||
}
|
||||
|
||||
if r.target == targetWasm {
|
||||
switch r.target {
|
||||
case targetWasm: // TODO(sr): make wasm a target plugin, too
|
||||
|
||||
if r.hasWasmModule() {
|
||||
_ = txnClose(ctx, err) // Ignore error
|
||||
@@ -1580,6 +1644,22 @@ func (r *Rego) PrepareForEval(ctx context.Context, opts ...PrepareOption) (Prepa
|
||||
return PreparedEvalQuery{}, err
|
||||
}
|
||||
r.opa = o
|
||||
|
||||
case targetRego: // do nothing, don't lookup default plugin
|
||||
default: // either a specific plugin target, or one that is default
|
||||
if tgt := r.targetPlugin(r.target); tgt != nil {
|
||||
queries := []ast.Body{r.compiledQueries[evalQueryType].query}
|
||||
pol, err := r.planQuery(queries, evalQueryType)
|
||||
if err != nil {
|
||||
return PreparedEvalQuery{}, err
|
||||
}
|
||||
// always add the builtins provided via rego.FunctionN options
|
||||
opts = append(opts, WithBuiltinFuncs(r.builtinFuncs))
|
||||
r.targetPrepState, err = tgt.PrepareForEval(ctx, pol, opts...)
|
||||
if err != nil {
|
||||
return PreparedEvalQuery{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
txnErr := txnClose(ctx, err) // Always call closer
|
||||
@@ -1689,22 +1769,20 @@ func (r *Rego) prepare(ctx context.Context, qType queryType, extras []extraStage
|
||||
}
|
||||
|
||||
func (r *Rego) parseModules(ctx context.Context, txn storage.Transaction, m metrics.Metrics) error {
|
||||
if len(r.modules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids, err := r.store.ListPolicies(ctx, txn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if there are no raw modules, nor modules in the store, then there
|
||||
// is nothing to do.
|
||||
if len(r.modules) == 0 && len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.Timer(metrics.RegoModuleParse).Start()
|
||||
defer m.Timer(metrics.RegoModuleParse).Stop()
|
||||
var errs Errors
|
||||
|
||||
// Parse any modules in the are saved to the store, but only if
|
||||
// Parse any modules that are saved to the store, but only if
|
||||
// another compile step is going to occur (ie. we have parsed modules
|
||||
// that need to be compiled).
|
||||
for _, id := range ids {
|
||||
@@ -1731,7 +1809,14 @@ func (r *Rego) parseModules(ctx context.Context, txn storage.Transaction, m metr
|
||||
for _, module := range r.modules {
|
||||
p, err := module.Parse()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
switch errorWithType := err.(type) {
|
||||
case ast.Errors:
|
||||
for _, e := range errorWithType {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
default:
|
||||
errs = append(errs, errorWithType)
|
||||
}
|
||||
}
|
||||
r.parsedModules[module.filename] = p
|
||||
}
|
||||
@@ -1952,8 +2037,20 @@ func (r *Rego) compileQuery(query ast.Body, imports []*ast.Import, m metrics.Met
|
||||
}
|
||||
|
||||
func (r *Rego) eval(ctx context.Context, ectx *EvalContext) (ResultSet, error) {
|
||||
if r.opa != nil {
|
||||
switch {
|
||||
case r.targetPrepState != nil: // target plugin flow
|
||||
var val ast.Value
|
||||
if r.runtime != nil {
|
||||
val = r.runtime.Value
|
||||
}
|
||||
s, err := r.targetPrepState.Eval(ctx, ectx, val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.valueToQueryResult(s, ectx)
|
||||
case r.target == targetWasm:
|
||||
return r.evalWasm(ctx, ectx)
|
||||
case r.target == targetRego: // continue
|
||||
}
|
||||
|
||||
q := topdown.NewQuery(ectx.compiledQuery.query).
|
||||
@@ -2050,7 +2147,11 @@ func (r *Rego) evalWasm(ctx context.Context, ectx *EvalContext) (ResultSet, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resultSet, ok := parsed.Value.(ast.Set)
|
||||
return r.valueToQueryResult(parsed.Value, ectx)
|
||||
}
|
||||
|
||||
func (r *Rego) valueToQueryResult(res ast.Value, ectx *EvalContext) (ResultSet, error) {
|
||||
resultSet, ok := res.(ast.Set)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("illegal result type")
|
||||
}
|
||||
@@ -2060,7 +2161,7 @@ func (r *Rego) evalWasm(ctx context.Context, ectx *EvalContext) (ResultSet, erro
|
||||
}
|
||||
|
||||
var rs ResultSet
|
||||
err = resultSet.Iter(func(term *ast.Term) error {
|
||||
err := resultSet.Iter(func(term *ast.Term) error {
|
||||
obj, ok := term.Value.(ast.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("illegal result type")
|
||||
@@ -2137,16 +2238,17 @@ func (r *Rego) partialResult(ctx context.Context, pCfg *PrepareConfig) (PartialR
|
||||
}
|
||||
|
||||
ectx := &EvalContext{
|
||||
parsedInput: r.parsedInput,
|
||||
metrics: r.metrics,
|
||||
txn: r.txn,
|
||||
partialNamespace: r.partialNamespace,
|
||||
queryTracers: r.queryTracers,
|
||||
compiledQuery: r.compiledQueries[partialResultQueryType],
|
||||
instrumentation: r.instrumentation,
|
||||
indexing: true,
|
||||
resolvers: r.resolvers,
|
||||
capabilities: r.capabilities,
|
||||
parsedInput: r.parsedInput,
|
||||
metrics: r.metrics,
|
||||
txn: r.txn,
|
||||
partialNamespace: r.partialNamespace,
|
||||
queryTracers: r.queryTracers,
|
||||
compiledQuery: r.compiledQueries[partialResultQueryType],
|
||||
instrumentation: r.instrumentation,
|
||||
indexing: true,
|
||||
resolvers: r.resolvers,
|
||||
capabilities: r.capabilities,
|
||||
strictBuiltinErrors: r.strictBuiltinErrors,
|
||||
}
|
||||
|
||||
disableInlining := r.disableInlining
|
||||
@@ -2249,7 +2351,7 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
|
||||
WithSkipPartialNamespace(r.skipPartialNamespace).
|
||||
WithShallowInlining(r.shallowInlining).
|
||||
WithInterQueryBuiltinCache(ectx.interQueryBuiltinCache).
|
||||
WithStrictBuiltinErrors(r.strictBuiltinErrors).
|
||||
WithStrictBuiltinErrors(ectx.strictBuiltinErrors).
|
||||
WithSeed(ectx.seed).
|
||||
WithPrintHook(ectx.printHook)
|
||||
|
||||
@@ -2383,11 +2485,13 @@ func (r *Rego) rewriteEqualsForPartialQueryCompile(_ ast.QueryCompiler, query as
|
||||
|
||||
func (r *Rego) generateTermVar() *ast.Term {
|
||||
r.termVarID++
|
||||
|
||||
if r.target == targetWasm {
|
||||
return ast.VarTerm(wasmVarPrefix + fmt.Sprintf("term%v", r.termVarID))
|
||||
prefix := ast.WildcardPrefix
|
||||
if p := r.targetPlugin(r.target); p != nil {
|
||||
prefix = wasmVarPrefix
|
||||
} else if r.target == targetWasm {
|
||||
prefix = wasmVarPrefix
|
||||
}
|
||||
return ast.VarTerm(ast.WildcardPrefix + fmt.Sprintf("term%v", r.termVarID))
|
||||
return ast.VarTerm(fmt.Sprintf("%sterm%v", prefix, r.termVarID))
|
||||
}
|
||||
|
||||
func (r Rego) hasQuery() bool {
|
||||
@@ -2570,17 +2674,19 @@ func finishFunction(name string, bctx topdown.BuiltinContext, result *ast.Term,
|
||||
if err != nil {
|
||||
var e *HaltError
|
||||
if errors.As(err, &e) {
|
||||
return topdown.Halt{Err: &topdown.Error{
|
||||
tdErr := &topdown.Error{
|
||||
Code: topdown.BuiltinErr,
|
||||
Message: fmt.Sprintf("%v: %v", name, e.Error()),
|
||||
Location: bctx.Location,
|
||||
}}
|
||||
}
|
||||
return topdown.Halt{Err: tdErr.Wrap(e)}
|
||||
}
|
||||
return &topdown.Error{
|
||||
tdErr := &topdown.Error{
|
||||
Code: topdown.BuiltinErr,
|
||||
Message: fmt.Sprintf("%v: %v", name, err.Error()),
|
||||
Location: bctx.Location,
|
||||
}
|
||||
return tdErr.Wrap(err)
|
||||
}
|
||||
if result == nil {
|
||||
return nil
|
||||
@@ -2610,3 +2716,49 @@ func generateJSON(term *ast.Term, ectx *EvalContext) (interface{}, error) {
|
||||
CopyMaps: ectx.copyMaps,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Rego) planQuery(queries []ast.Body, evalQueryType queryType) (*ir.Policy, error) {
|
||||
modules := make([]*ast.Module, 0, len(r.compiler.Modules))
|
||||
for _, module := range r.compiler.Modules {
|
||||
modules = append(modules, module)
|
||||
}
|
||||
|
||||
decls := make(map[string]*ast.Builtin, len(r.builtinDecls)+len(ast.BuiltinMap))
|
||||
|
||||
for k, v := range ast.BuiltinMap {
|
||||
decls[k] = v
|
||||
}
|
||||
|
||||
for k, v := range r.builtinDecls {
|
||||
decls[k] = v
|
||||
}
|
||||
|
||||
const queryName = "eval" // NOTE(tsandall): the query name is arbitrary
|
||||
|
||||
p := planner.New().
|
||||
WithQueries([]planner.QuerySet{
|
||||
{
|
||||
Name: queryName,
|
||||
Queries: queries,
|
||||
RewrittenVars: r.compiledQueries[evalQueryType].compiler.RewrittenVars(),
|
||||
},
|
||||
}).
|
||||
WithModules(modules).
|
||||
WithBuiltinDecls(decls).
|
||||
WithDebug(r.dump)
|
||||
|
||||
policy, err := p.Plan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.dump != nil {
|
||||
fmt.Fprintln(r.dump, "PLAN:")
|
||||
fmt.Fprintln(r.dump, "-----")
|
||||
err = ir.Pretty(r.dump, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Fprintln(r.dump)
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for the OPA Authorization Policy Input document",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"identity": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_certificates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"method": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"type": "object"
|
||||
},
|
||||
"headers": {
|
||||
"type": "object"
|
||||
},
|
||||
"body": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"identity",
|
||||
"client_certificates",
|
||||
"method",
|
||||
"path",
|
||||
"params",
|
||||
"headers",
|
||||
"body"
|
||||
]
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright 2023 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 schemas
|
||||
|
||||
import (
|
||||
"embed"
|
||||
)
|
||||
|
||||
// FS contains the known schemas for OPA's Authorization Policy etc.
|
||||
// "authorizationPolicy.json" contains the input schema for OPA's Authorization Policy
|
||||
//
|
||||
//go:embed *.json
|
||||
var FS embed.FS
|
||||
+74
-4
@@ -5,9 +5,8 @@
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
|
||||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
@@ -54,6 +53,54 @@ func arithFloor(a *big.Float) (*big.Float, error) {
|
||||
return new(big.Float).Sub(f, big.NewFloat(1.0)), nil
|
||||
}
|
||||
|
||||
func builtinPlus(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
n1, err := builtins.NumberOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n2, err := builtins.NumberOperand(operands[1].Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x, ok1 := n1.Int()
|
||||
y, ok2 := n2.Int()
|
||||
|
||||
if ok1 && ok2 && inSmallIntRange(x) && inSmallIntRange(y) {
|
||||
return iter(ast.IntNumberTerm(x + y))
|
||||
}
|
||||
|
||||
f, err := arithPlus(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return iter(ast.NewTerm(builtins.FloatToNumber(f)))
|
||||
}
|
||||
|
||||
func builtinMultiply(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
n1, err := builtins.NumberOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n2, err := builtins.NumberOperand(operands[1].Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x, ok1 := n1.Int()
|
||||
y, ok2 := n2.Int()
|
||||
|
||||
if ok1 && ok2 && inSmallIntRange(x) && inSmallIntRange(y) {
|
||||
return iter(ast.IntNumberTerm(x * y))
|
||||
}
|
||||
|
||||
f, err := arithMultiply(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return iter(ast.NewTerm(builtins.FloatToNumber(f)))
|
||||
}
|
||||
|
||||
func arithPlus(a, b *big.Float) (*big.Float, error) {
|
||||
return new(big.Float).Add(a, b), nil
|
||||
}
|
||||
@@ -119,6 +166,14 @@ func builtinMinus(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
|
||||
n2, ok2 := operands[1].Value.(ast.Number)
|
||||
|
||||
if ok1 && ok2 {
|
||||
|
||||
x, okx := n1.Int()
|
||||
y, oky := n2.Int()
|
||||
|
||||
if okx && oky && inSmallIntRange(x) && inSmallIntRange(y) {
|
||||
return iter(ast.IntNumberTerm(x - y))
|
||||
}
|
||||
|
||||
f, err := arithMinus(builtins.NumberToFloat(n1), builtins.NumberToFloat(n2))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -150,6 +205,17 @@ func builtinRem(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
|
||||
|
||||
if ok1 && ok2 {
|
||||
|
||||
x, okx := n1.Int()
|
||||
y, oky := n2.Int()
|
||||
|
||||
if okx && oky && inSmallIntRange(x) && inSmallIntRange(y) {
|
||||
if y == 0 {
|
||||
return fmt.Errorf("modulo by zero")
|
||||
}
|
||||
|
||||
return iter(ast.IntNumberTerm(x % y))
|
||||
}
|
||||
|
||||
op1, err1 := builtins.NumberToInt(n1)
|
||||
op2, err2 := builtins.NumberToInt(n2)
|
||||
|
||||
@@ -171,14 +237,18 @@ func builtinRem(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) err
|
||||
return builtins.NewOperandTypeErr(2, operands[1].Value, "number")
|
||||
}
|
||||
|
||||
func inSmallIntRange(num int) bool {
|
||||
return -1000 < num && num < 1000
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.Abs.Name, builtinArithArity1(arithAbs))
|
||||
RegisterBuiltinFunc(ast.Round.Name, builtinArithArity1(arithRound))
|
||||
RegisterBuiltinFunc(ast.Ceil.Name, builtinArithArity1(arithCeil))
|
||||
RegisterBuiltinFunc(ast.Floor.Name, builtinArithArity1(arithFloor))
|
||||
RegisterBuiltinFunc(ast.Plus.Name, builtinArithArity2(arithPlus))
|
||||
RegisterBuiltinFunc(ast.Plus.Name, builtinPlus)
|
||||
RegisterBuiltinFunc(ast.Minus.Name, builtinMinus)
|
||||
RegisterBuiltinFunc(ast.Multiply.Name, builtinArithArity2(arithMultiply))
|
||||
RegisterBuiltinFunc(ast.Multiply.Name, builtinMultiply)
|
||||
RegisterBuiltinFunc(ast.Divide.Name, builtinArithArity2(arithDivide))
|
||||
RegisterBuiltinFunc(ast.Rem.Name, builtinRem)
|
||||
}
|
||||
|
||||
+4
-2
@@ -182,17 +182,19 @@ func handleBuiltinErr(name string, loc *ast.Location, err error) error {
|
||||
case *Error, Halt:
|
||||
return err
|
||||
case builtins.ErrOperand:
|
||||
return &Error{
|
||||
e := &Error{
|
||||
Code: TypeErr,
|
||||
Message: fmt.Sprintf("%v: %v", name, err.Error()),
|
||||
Location: loc,
|
||||
}
|
||||
return e.Wrap(err)
|
||||
default:
|
||||
return &Error{
|
||||
e := &Error{
|
||||
Code: BuiltinErr,
|
||||
Message: fmt.Sprintf("%v: %v", name, err.Error()),
|
||||
Location: loc,
|
||||
}
|
||||
return e.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -249,7 +249,11 @@ func NumberToFloat(n ast.Number) *big.Float {
|
||||
|
||||
// FloatToNumber converts f to a number.
|
||||
func FloatToNumber(f *big.Float) ast.Number {
|
||||
return ast.Number(f.Text('g', -1))
|
||||
var format byte = 'g'
|
||||
if f.IsInt() {
|
||||
format = 'f'
|
||||
}
|
||||
return ast.Number(f.Text(format, -1))
|
||||
}
|
||||
|
||||
// NumberToInt converts n to a big int.
|
||||
|
||||
+13
-2
@@ -7,11 +7,10 @@ package cache
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
|
||||
"sync"
|
||||
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
|
||||
@@ -62,6 +61,7 @@ func (c *Config) validateAndInjectDefaults() error {
|
||||
// InterQueryCacheValue defines the interface for the data that the inter-query cache holds.
|
||||
type InterQueryCacheValue interface {
|
||||
SizeInBytes() int64
|
||||
Clone() (InterQueryCacheValue, error)
|
||||
}
|
||||
|
||||
// InterQueryCache defines the interface for the inter-query cache.
|
||||
@@ -70,6 +70,7 @@ type InterQueryCache interface {
|
||||
Insert(key ast.Value, value InterQueryCacheValue) int
|
||||
Delete(key ast.Value)
|
||||
UpdateConfig(config *Config)
|
||||
Clone(value InterQueryCacheValue) (InterQueryCacheValue, error)
|
||||
}
|
||||
|
||||
// NewInterQueryCache returns a new inter-query cache.
|
||||
@@ -130,6 +131,12 @@ func (c *cache) UpdateConfig(config *Config) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *cache) Clone(value InterQueryCacheValue) (InterQueryCacheValue, error) {
|
||||
c.mtx.Lock()
|
||||
defer c.mtx.Unlock()
|
||||
return c.unsafeClone(value)
|
||||
}
|
||||
|
||||
func (c *cache) unsafeInsert(k ast.Value, v InterQueryCacheValue) (dropped int) {
|
||||
size := v.SizeInBytes()
|
||||
limit := c.maxSizeBytes()
|
||||
@@ -174,6 +181,10 @@ func (c *cache) unsafeDelete(k ast.Value) {
|
||||
c.l.Remove(cacheItem.keyElement)
|
||||
}
|
||||
|
||||
func (c *cache) unsafeClone(value InterQueryCacheValue) (InterQueryCacheValue, error) {
|
||||
return value.Clone()
|
||||
}
|
||||
|
||||
func (c *cache) maxSizeBytes() int64 {
|
||||
if c.config == nil {
|
||||
return defaultMaxSizeBytes
|
||||
|
||||
+243
-45
@@ -6,11 +6,13 @@ package topdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -20,8 +22,9 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/internal/jwx/jwk"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
"github.com/open-policy-agent/opa/util"
|
||||
)
|
||||
@@ -38,7 +41,8 @@ const (
|
||||
blockTypeRSAPrivateKey = "RSA PRIVATE KEY"
|
||||
// blockTypeRSAPrivateKey indicates this PEM block contains a RSA private key.
|
||||
// Exported for tests.
|
||||
blockTypePrivateKey = "PRIVATE KEY"
|
||||
blockTypePrivateKey = "PRIVATE KEY"
|
||||
blockTypeEcPrivateKey = "EC PRIVATE KEY"
|
||||
)
|
||||
|
||||
func builtinCryptoX509ParseCertificates(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
@@ -52,7 +56,7 @@ func builtinCryptoX509ParseCertificates(_ BuiltinContext, operands []*ast.Term,
|
||||
return err
|
||||
}
|
||||
|
||||
v, err := ast.InterfaceToValue(certs)
|
||||
v, err := ast.InterfaceToValue(extendCertificates(certs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,6 +64,28 @@ func builtinCryptoX509ParseCertificates(_ BuiltinContext, operands []*ast.Term,
|
||||
return iter(ast.NewTerm(v))
|
||||
}
|
||||
|
||||
// extendedCert is a wrapper around x509.Certificate that adds additional fields for JSON serialization.
|
||||
type extendedCert struct {
|
||||
x509.Certificate
|
||||
URIStrings []string
|
||||
}
|
||||
|
||||
func extendCertificates(certs []*x509.Certificate) []extendedCert {
|
||||
// add a field to certs containing the URIs as strings
|
||||
processedCerts := make([]extendedCert, len(certs))
|
||||
|
||||
for i, cert := range certs {
|
||||
processedCerts[i].Certificate = *cert
|
||||
if cert.URIs != nil {
|
||||
processedCerts[i].URIStrings = make([]string, len(cert.URIs))
|
||||
for j, uri := range cert.URIs {
|
||||
processedCerts[i].URIStrings[j] = uri.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return processedCerts
|
||||
}
|
||||
|
||||
func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
|
||||
a := operands[0].Value
|
||||
@@ -83,7 +109,7 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a
|
||||
return iter(invalid)
|
||||
}
|
||||
|
||||
value, err := ast.InterfaceToValue(verified)
|
||||
value, err := ast.InterfaceToValue(extendCertificates(verified))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,6 +122,29 @@ func builtinCryptoX509ParseAndVerifyCertificates(_ BuiltinContext, operands []*a
|
||||
return iter(valid)
|
||||
}
|
||||
|
||||
func builtinCryptoX509ParseKeyPair(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
certificate, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := builtins.StringOperand(operands[1].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certs, err := getTLSx509KeyPairFromString([]byte(certificate), []byte(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := ast.InterfaceToValue(certs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast.NewTerm(v))
|
||||
}
|
||||
|
||||
func builtinCryptoX509ParseCertificateRequest(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
input, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
@@ -144,7 +193,8 @@ func builtinCryptoX509ParseCertificateRequest(_ BuiltinContext, operands []*ast.
|
||||
return iter(ast.NewTerm(v))
|
||||
}
|
||||
|
||||
func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
func builtinCryptoJWKFromPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
var x interface{}
|
||||
|
||||
a := operands[0].Value
|
||||
input, err := builtins.StringOperand(a, 1)
|
||||
@@ -153,23 +203,83 @@ func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term,
|
||||
}
|
||||
|
||||
// get the raw private key
|
||||
rawKey, err := getRSAPrivateKeyFromString(string(input))
|
||||
pemDataString := string(input)
|
||||
|
||||
if pemDataString == "" {
|
||||
return fmt.Errorf("input PEM data was empty")
|
||||
}
|
||||
|
||||
// This built in must be supplied a valid PEM or base64 encoded string.
|
||||
// If the input is not a PEM string, attempt to decode b64.
|
||||
// If the base64 decode fails - this is an error
|
||||
if !strings.HasPrefix(pemDataString, "-----BEGIN") {
|
||||
bs, err := base64.StdEncoding.DecodeString(pemDataString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pemDataString = string(bs)
|
||||
}
|
||||
|
||||
rawKeys, err := getPrivateKeysFromPEMData(pemDataString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rsaPrivateKey, err := jwk.New(rawKey)
|
||||
if len(rawKeys) == 0 {
|
||||
return iter(ast.NullTerm())
|
||||
}
|
||||
|
||||
key, err := jwk.New(rawKeys[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
jsonKey, err := json.Marshal(rsaPrivateKey)
|
||||
jsonKey, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := util.UnmarshalJSON(jsonKey, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
value, err := ast.InterfaceToValue(x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast.NewTerm(value))
|
||||
}
|
||||
|
||||
func builtinCryptoParsePrivateKeys(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
|
||||
a := operands[0].Value
|
||||
input, err := builtins.StringOperand(a, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if string(input) == "" {
|
||||
return iter(ast.NullTerm())
|
||||
}
|
||||
|
||||
// get the raw private key
|
||||
rawKeys, err := getPrivateKeysFromPEMData(string(input))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(rawKeys) == 0 {
|
||||
return iter(ast.NewTerm(ast.NewArray()))
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(rawKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var x interface{}
|
||||
if err := util.UnmarshalJSON(jsonKey, &x); err != nil {
|
||||
if err := util.UnmarshalJSON(bs, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -249,6 +359,24 @@ func builtinCryptoHmacSha512(_ BuiltinContext, operands []*ast.Term, iter func(*
|
||||
return hmacHelper(operands, iter, sha512.New)
|
||||
}
|
||||
|
||||
func builtinCryptoHmacEqual(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
a1 := operands[0].Value
|
||||
mac1, err := builtins.StringOperand(a1, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a2 := operands[1].Value
|
||||
mac2, err := builtins.StringOperand(a2, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res := hmac.Equal([]byte(mac1), []byte(mac2))
|
||||
|
||||
return iter(ast.BooleanTerm(res))
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseCertificates.Name, builtinCryptoX509ParseCertificates)
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseAndVerifyCertificates.Name, builtinCryptoX509ParseAndVerifyCertificates)
|
||||
@@ -256,11 +384,14 @@ func init() {
|
||||
RegisterBuiltinFunc(ast.CryptoSha1.Name, builtinCryptoSha1)
|
||||
RegisterBuiltinFunc(ast.CryptoSha256.Name, builtinCryptoSha256)
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseCertificateRequest.Name, builtinCryptoX509ParseCertificateRequest)
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoX509ParseRSAPrivateKey)
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoJWKFromPrivateKey)
|
||||
RegisterBuiltinFunc(ast.CryptoParsePrivateKeys.Name, builtinCryptoParsePrivateKeys)
|
||||
RegisterBuiltinFunc(ast.CryptoX509ParseKeyPair.Name, builtinCryptoX509ParseKeyPair)
|
||||
RegisterBuiltinFunc(ast.CryptoHmacMd5.Name, builtinCryptoHmacMd5)
|
||||
RegisterBuiltinFunc(ast.CryptoHmacSha1.Name, builtinCryptoHmacSha1)
|
||||
RegisterBuiltinFunc(ast.CryptoHmacSha256.Name, builtinCryptoHmacSha256)
|
||||
RegisterBuiltinFunc(ast.CryptoHmacSha512.Name, builtinCryptoHmacSha512)
|
||||
RegisterBuiltinFunc(ast.CryptoHmacEqual.Name, builtinCryptoHmacEqual)
|
||||
}
|
||||
|
||||
func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) {
|
||||
@@ -334,43 +465,56 @@ func getX509CertsFromPem(pemBlocks []byte) ([]*x509.Certificate, error) {
|
||||
return x509.ParseCertificates(decodedCerts)
|
||||
}
|
||||
|
||||
func getRSAPrivateKeyFromString(key string) (interface{}, error) {
|
||||
// if the input is PEM handle that
|
||||
if strings.HasPrefix(key, "-----BEGIN") {
|
||||
return getRSAPrivateKeyFromPEM([]byte(key))
|
||||
func getPrivateKeysFromPEMData(pemData string) ([]crypto.PrivateKey, error) {
|
||||
pemBlockString := pemData
|
||||
|
||||
var validPrivateKeys []crypto.PrivateKey
|
||||
|
||||
// if the input is base64, decode it
|
||||
bs, err := base64.StdEncoding.DecodeString(pemBlockString)
|
||||
if err == nil {
|
||||
pemBlockString = string(bs)
|
||||
}
|
||||
bs = []byte(pemBlockString)
|
||||
|
||||
// assume input is base64 if not PEM
|
||||
b64, err := base64.StdEncoding.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for len(bs) > 0 {
|
||||
inputLen := len(bs)
|
||||
var block *pem.Block
|
||||
block, bs = pem.Decode(bs)
|
||||
if block == nil && len(bs) == 0 {
|
||||
break
|
||||
}
|
||||
// should only happen if end of input is not a valid PEM block. See TestParseRSAPrivateKeyVariedPemInput.
|
||||
if inputLen == len(bs) {
|
||||
break
|
||||
}
|
||||
|
||||
if block == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
switch block.Type {
|
||||
case blockTypeRSAPrivateKey:
|
||||
parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validPrivateKeys = append(validPrivateKeys, parsedKey)
|
||||
case blockTypePrivateKey:
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validPrivateKeys = append(validPrivateKeys, parsedKey)
|
||||
case blockTypeEcPrivateKey:
|
||||
parsedKey, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
validPrivateKeys = append(validPrivateKeys, parsedKey)
|
||||
}
|
||||
}
|
||||
|
||||
return getRSAPrivateKeyFromPEM(b64)
|
||||
}
|
||||
|
||||
func getRSAPrivateKeyFromPEM(pemBlocks []byte) (interface{}, error) {
|
||||
|
||||
// decode the pem into the Block struct
|
||||
p, _ := pem.Decode(pemBlocks)
|
||||
if p == nil {
|
||||
return nil, fmt.Errorf("failed to parse PEM block containing the key")
|
||||
}
|
||||
|
||||
// if the key is in PKCS1 format
|
||||
if p.Type == blockTypeRSAPrivateKey {
|
||||
return x509.ParsePKCS1PrivateKey(p.Bytes)
|
||||
}
|
||||
|
||||
// if the key is in PKCS8 format
|
||||
if p.Type == blockTypePrivateKey {
|
||||
return x509.ParsePKCS8PrivateKey(p.Bytes)
|
||||
}
|
||||
|
||||
// unsupported key format
|
||||
return nil, fmt.Errorf("PEM block type is '%s', expected %s or %s", p.Type, blockTypeRSAPrivateKey,
|
||||
blockTypePrivateKey)
|
||||
|
||||
return validPrivateKeys, nil
|
||||
}
|
||||
|
||||
// addCACertsFromFile adds CA certificates from filePath into the given pool.
|
||||
@@ -406,7 +550,7 @@ func addCACertsFromBytes(pool *x509.CertPool, pemBytes []byte) (*x509.CertPool,
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// addCACertsFromBytes adds CA certificates from the environment variable named
|
||||
// addCACertsFromEnv adds CA certificates from the environment variable named
|
||||
// by envName into the given pool. If pool is nil, it creates a new x509.CertPool.
|
||||
// pool is returned.
|
||||
func addCACertsFromEnv(pool *x509.CertPool, envName string) (*x509.CertPool, error) {
|
||||
@@ -428,6 +572,60 @@ func readCertFromFile(localCertFile string) ([]byte, error) {
|
||||
return certPEM, nil
|
||||
}
|
||||
|
||||
func getTLSx509KeyPairFromString(certPemBlock []byte, keyPemBlock []byte) (*tls.Certificate, error) {
|
||||
|
||||
if !strings.HasPrefix(string(certPemBlock), "-----BEGIN") {
|
||||
s, err := base64.StdEncoding.DecodeString(string(certPemBlock))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPemBlock = s
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(string(keyPemBlock), "-----BEGIN") {
|
||||
s, err := base64.StdEncoding.DecodeString(string(keyPemBlock))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPemBlock = s
|
||||
}
|
||||
|
||||
// we assume it a DER certificate and try to convert it to a PEM.
|
||||
if !bytes.HasPrefix(certPemBlock, []byte("-----BEGIN")) {
|
||||
|
||||
pemBlock := &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certPemBlock,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, pemBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certPemBlock = buf.Bytes()
|
||||
|
||||
}
|
||||
// we assume it a DER key and try to convert it to a PEM.
|
||||
if !bytes.HasPrefix(keyPemBlock, []byte("-----BEGIN")) {
|
||||
pemBlock := &pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: keyPemBlock,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, pemBlock); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keyPemBlock = buf.Bytes()
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(certPemBlock, keyPemBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ReadKeyFromFile reads a key from file
|
||||
func readKeyFromFile(localKeyFile string) ([]byte, error) {
|
||||
// Read in the cert file
|
||||
|
||||
+4
-4
@@ -13,7 +13,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
ghodss "github.com/ghodss/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
@@ -232,7 +232,7 @@ func builtinYAMLMarshal(_ BuiltinContext, operands []*ast.Term, iter func(*ast.T
|
||||
return err
|
||||
}
|
||||
|
||||
bs, err := ghodss.JSONToYAML(buf.Bytes())
|
||||
bs, err := yaml.JSONToYAML(buf.Bytes())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func builtinYAMLUnmarshal(_ BuiltinContext, operands []*ast.Term, iter func(*ast
|
||||
return err
|
||||
}
|
||||
|
||||
bs, err := ghodss.YAMLToJSON([]byte(str))
|
||||
bs, err := yaml.YAMLToJSON([]byte(str))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func builtinYAMLIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.T
|
||||
}
|
||||
|
||||
var x interface{}
|
||||
err = ghodss.Unmarshal([]byte(str), &x)
|
||||
err = yaml.Unmarshal([]byte(str), &x)
|
||||
return iter(ast.BooleanTerm(err == nil))
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -29,6 +29,7 @@ type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Location *ast.Location `json:"location,omitempty"`
|
||||
err error `json:"-"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -90,6 +91,15 @@ func (e *Error) Error() string {
|
||||
return msg
|
||||
}
|
||||
|
||||
func (e *Error) Wrap(err error) *Error {
|
||||
e.err = err
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func functionConflictErr(loc *ast.Location) error {
|
||||
return &Error{
|
||||
Code: ConflictErr,
|
||||
|
||||
+265
-74
@@ -23,6 +23,8 @@ type evalIterator func(*eval) error
|
||||
|
||||
type unifyIterator func() error
|
||||
|
||||
type unifyRefIterator func(pos int) error
|
||||
|
||||
type queryIDFactory struct {
|
||||
curr uint64
|
||||
}
|
||||
@@ -1795,13 +1797,16 @@ type evalFunc struct {
|
||||
|
||||
func (e evalFunc) eval(iter unifyIterator) error {
|
||||
|
||||
// default functions aren't supported:
|
||||
// https://github.com/open-policy-agent/opa/issues/2445
|
||||
if len(e.ir.Rules) == 0 {
|
||||
if e.ir.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
argCount := len(e.ir.Rules[0].Head.Args)
|
||||
var argCount int
|
||||
if len(e.ir.Rules) > 0 {
|
||||
argCount = len(e.ir.Rules[0].Head.Args)
|
||||
} else if e.ir.Default != nil {
|
||||
argCount = len(e.ir.Default.Head.Args)
|
||||
}
|
||||
|
||||
if len(e.ir.Else) > 0 && e.e.unknown(e.e.query[e.e.index], e.e.bindings) {
|
||||
// Partial evaluation of ordered rules is not supported currently. Save the
|
||||
@@ -1820,6 +1825,7 @@ func (e evalFunc) eval(iter unifyIterator) error {
|
||||
return e.partialEvalSupport(argCount, iter)
|
||||
}
|
||||
}
|
||||
|
||||
return suppressEarlyExit(e.evalValue(iter, argCount, e.ir.EarlyExit))
|
||||
}
|
||||
|
||||
@@ -1859,6 +1865,11 @@ func (e evalFunc) evalValue(iter unifyIterator, argCount int, findOne bool) erro
|
||||
}
|
||||
}
|
||||
|
||||
if e.ir.Default != nil && prev == nil {
|
||||
_, err := e.evalOneRule(iter, e.ir.Default, cacheKey, prev, findOne)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2269,6 +2280,14 @@ func (e evalVirtual) eval(iter unifyIterator) error {
|
||||
|
||||
switch ir.Kind {
|
||||
case ast.MultiValue:
|
||||
var empty *ast.Term
|
||||
if ir.OnlyGroundRefs {
|
||||
// rule ref contains no vars, so we're building a set
|
||||
empty = ast.SetTerm()
|
||||
} else {
|
||||
// rule ref contains vars, so we're building an object containing a set leaf
|
||||
empty = ast.ObjectTerm()
|
||||
}
|
||||
eval := evalVirtualPartial{
|
||||
e: e.e,
|
||||
ref: e.ref,
|
||||
@@ -2278,12 +2297,10 @@ func (e evalVirtual) eval(iter unifyIterator) error {
|
||||
bindings: e.bindings,
|
||||
rterm: e.rterm,
|
||||
rbindings: e.rbindings,
|
||||
empty: ast.SetTerm(),
|
||||
empty: empty,
|
||||
}
|
||||
return eval.eval(iter)
|
||||
case ast.SingleValue:
|
||||
// NOTE(sr): If we allow vars in others than the last position of a ref, we need
|
||||
// to start reworking things here
|
||||
if ir.OnlyGroundRefs {
|
||||
eval := evalVirtualComplete{
|
||||
e: e.e,
|
||||
@@ -2350,9 +2367,31 @@ func (e evalVirtualPartial) eval(iter unifyIterator) error {
|
||||
return e.evalEachRule(iter, unknown)
|
||||
}
|
||||
|
||||
// returns the maximum length a ref can be without being longer than the longest rule ref in rules.
|
||||
func maxRefLength(rules []*ast.Rule, ceil int) int {
|
||||
var l int
|
||||
for _, r := range rules {
|
||||
rl := len(r.Ref())
|
||||
if r.Head.RuleKind() == ast.MultiValue {
|
||||
rl = rl + 1
|
||||
}
|
||||
if rl >= ceil {
|
||||
return ceil
|
||||
} else if rl > l {
|
||||
l = rl
|
||||
}
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) evalEachRule(iter unifyIterator, unknown bool) error {
|
||||
|
||||
if e.e.unknown(e.ref[e.pos+1], e.bindings) {
|
||||
if e.ir.Empty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := maxRefLength(e.ir.Rules, len(e.ref))
|
||||
if e.e.unknown(e.ref[e.pos+1:m], e.bindings) {
|
||||
for _, rule := range e.ir.Rules {
|
||||
if err := e.evalOneRulePostUnify(iter, rule); err != nil {
|
||||
return err
|
||||
@@ -2378,12 +2417,25 @@ func (e evalVirtualPartial) evalEachRule(iter unifyIterator, unknown bool) error
|
||||
}
|
||||
|
||||
result := e.empty
|
||||
var visitedRefs []ast.Ref
|
||||
|
||||
for _, rule := range e.ir.Rules {
|
||||
if err := e.evalOneRulePreUnify(iter, rule, hint, result, unknown); err != nil {
|
||||
result, err = e.evalOneRulePreUnify(iter, rule, result, unknown, &visitedRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if hint.key != nil {
|
||||
if v, err := result.Value.Find(hint.key[e.pos+1:]); err == nil && v != nil {
|
||||
e.e.virtualCache.Put(hint.key, ast.NewTerm(v))
|
||||
}
|
||||
}
|
||||
|
||||
if !unknown {
|
||||
return e.evalTerm(iter, e.pos+1, result, e.bindings)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2413,13 +2465,15 @@ func (e evalVirtualPartial) evalAllRules(iter unifyIterator, rules []*ast.Rule)
|
||||
func (e evalVirtualPartial) evalAllRulesNoCache(rules []*ast.Rule) (*ast.Term, error) {
|
||||
result := e.empty
|
||||
|
||||
var visitedRefs []ast.Ref
|
||||
|
||||
for _, rule := range rules {
|
||||
child := e.e.child(rule.Body)
|
||||
child.traceEnter(rule)
|
||||
err := child.eval(func(*eval) error {
|
||||
child.traceExit(rule)
|
||||
var err error
|
||||
result, _, err = e.reduce(rule.Head, child.bindings, result)
|
||||
result, _, err = e.reduce(rule, child.bindings, result, &visitedRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2436,9 +2490,18 @@ func (e evalVirtualPartial) evalAllRulesNoCache(rules []*ast.Rule) (*ast.Term, e
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) evalOneRulePreUnify(iter unifyIterator, rule *ast.Rule, hint evalVirtualPartialCacheHint, result *ast.Term, unknown bool) error {
|
||||
func wrapInObjects(leaf *ast.Term, ref ast.Ref) *ast.Term {
|
||||
// We build the nested objects leaf-to-root to preserve ground:ness
|
||||
if len(ref) == 0 {
|
||||
return leaf
|
||||
}
|
||||
key := ref[0]
|
||||
val := wrapInObjects(leaf, ref[1:])
|
||||
return ast.ObjectTerm(ast.Item(key, val))
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) evalOneRulePreUnify(iter unifyIterator, rule *ast.Rule, result *ast.Term, unknown bool, visitedRefs *[]ast.Ref) (*ast.Term, error) {
|
||||
|
||||
key := e.ref[e.pos+1]
|
||||
child := e.e.child(rule.Body)
|
||||
|
||||
child.traceEnter(rule)
|
||||
@@ -2448,63 +2511,89 @@ func (e evalVirtualPartial) evalOneRulePreUnify(iter unifyIterator, rule *ast.Ru
|
||||
if headKey == nil {
|
||||
headKey = rule.Head.Reference[len(rule.Head.Reference)-1]
|
||||
}
|
||||
err := child.biunify(headKey, key, child.bindings, e.bindings, func() error {
|
||||
|
||||
// Walk the dynamic portion of rule ref and key to unify vars
|
||||
err := child.biunifyRuleHead(e.pos+1, e.ref, rule, e.bindings, child.bindings, func(pos int) error {
|
||||
defined = true
|
||||
return child.eval(func(child *eval) error {
|
||||
|
||||
child.traceExit(rule)
|
||||
|
||||
term := rule.Head.Value
|
||||
if term == nil {
|
||||
term = headKey
|
||||
}
|
||||
|
||||
if hint.key != nil {
|
||||
result := child.bindings.Plug(term)
|
||||
e.e.virtualCache.Put(hint.key, result)
|
||||
}
|
||||
if unknown {
|
||||
term, termbindings := child.bindings.apply(term)
|
||||
|
||||
// NOTE(tsandall): if the rule set depends on any unknowns then do
|
||||
// not perform the duplicate check because evaluation of the ruleset
|
||||
// may not produce a definitive result. This is a bit strict--we
|
||||
// could improve by skipping only when saves occur.
|
||||
if !unknown {
|
||||
var dup bool
|
||||
var err error
|
||||
result, dup, err = e.reduce(rule.Head, child.bindings, result)
|
||||
if rule.Head.RuleKind() == ast.MultiValue {
|
||||
term = ast.SetTerm(term)
|
||||
}
|
||||
|
||||
objRef := rule.Ref()[e.pos+1:]
|
||||
term = wrapInObjects(term, objRef)
|
||||
|
||||
err := e.evalTerm(iter, e.pos+1, term, termbindings)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if dup {
|
||||
}
|
||||
} else {
|
||||
var dup bool
|
||||
var err error
|
||||
result, dup, err = e.reduce(rule, child.bindings, result, visitedRefs)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !unknown && dup {
|
||||
child.traceDuplicate(rule)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
child.traceExit(rule)
|
||||
term, termbindings := child.bindings.apply(term)
|
||||
err := e.evalTerm(iter, e.pos+2, term, termbindings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
child.traceRedo(rule)
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(tsandall): why are we tracing here? this looks wrong.
|
||||
if !defined {
|
||||
child.traceFail(rule)
|
||||
}
|
||||
|
||||
return nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *eval) biunifyRuleHead(pos int, ref ast.Ref, rule *ast.Rule, refBindings, ruleBindings *bindings, iter unifyRefIterator) error {
|
||||
return e.biunifyDynamicRef(pos, ref, rule.Ref(), refBindings, ruleBindings, func(pos int) error {
|
||||
// FIXME: Is there a simpler, more robust way of figuring out that we should biunify the rule key?
|
||||
if rule.Head.RuleKind() == ast.MultiValue && pos < len(ref) && len(rule.Ref()) <= len(ref) {
|
||||
headKey := rule.Head.Key
|
||||
if headKey == nil {
|
||||
headKey = rule.Head.Reference[len(rule.Head.Reference)-1]
|
||||
}
|
||||
return e.biunify(ref[pos], headKey, refBindings, ruleBindings, func() error {
|
||||
return iter(pos + 1)
|
||||
})
|
||||
}
|
||||
return iter(pos)
|
||||
})
|
||||
}
|
||||
|
||||
func (e *eval) biunifyDynamicRef(pos int, a, b ast.Ref, b1, b2 *bindings, iter unifyRefIterator) error {
|
||||
if pos >= len(a) || pos >= len(b) {
|
||||
return iter(pos)
|
||||
}
|
||||
|
||||
return e.biunify(a[pos], b[pos], b1, b2, func() error {
|
||||
return e.biunifyDynamicRef(pos+1, a, b, b1, b2, iter)
|
||||
})
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) evalOneRulePostUnify(iter unifyIterator, rule *ast.Rule) error {
|
||||
|
||||
key := e.ref[e.pos+1]
|
||||
child := e.e.child(rule.Body)
|
||||
|
||||
child.traceEnter(rule)
|
||||
@@ -2512,7 +2601,7 @@ func (e evalVirtualPartial) evalOneRulePostUnify(iter unifyIterator, rule *ast.R
|
||||
|
||||
err := child.eval(func(child *eval) error {
|
||||
defined = true
|
||||
return e.e.biunify(rule.Head.Key, key, child.bindings, e.bindings, func() error {
|
||||
return e.e.biunifyRuleHead(e.pos+1, e.ref, rule, e.bindings, child.bindings, func(pos int) error {
|
||||
return e.evalOneRuleContinue(iter, rule, child)
|
||||
})
|
||||
})
|
||||
@@ -2538,7 +2627,15 @@ func (e evalVirtualPartial) evalOneRuleContinue(iter unifyIterator, rule *ast.Ru
|
||||
}
|
||||
|
||||
term, termbindings := child.bindings.apply(term)
|
||||
err := e.evalTerm(iter, e.pos+2, term, termbindings)
|
||||
|
||||
if rule.Head.RuleKind() == ast.MultiValue {
|
||||
term = ast.SetTerm(term)
|
||||
}
|
||||
|
||||
objRef := rule.Ref()[e.pos+1:]
|
||||
term = wrapInObjects(term, objRef)
|
||||
|
||||
err := e.evalTerm(iter, e.pos+1, term, termbindings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2597,17 +2694,32 @@ func (e evalVirtualPartial) partialEvalSupportRule(rule *ast.Rule, path ast.Ref)
|
||||
// Skip this rule body if it fails to type-check.
|
||||
// Type-checking failure means the rule body will never succeed.
|
||||
if e.e.compiler.PassesTypeCheck(plugged) {
|
||||
var key, value *ast.Term
|
||||
|
||||
if rule.Head.Key != nil {
|
||||
key = child.bindings.PlugNamespaced(rule.Head.Key, e.e.caller.bindings)
|
||||
}
|
||||
var value *ast.Term
|
||||
|
||||
if rule.Head.Value != nil {
|
||||
value = child.bindings.PlugNamespaced(rule.Head.Value, e.e.caller.bindings)
|
||||
}
|
||||
|
||||
head := ast.NewHead(rule.Head.Name, key, value)
|
||||
ref := e.e.namespaceRef(rule.Ref())
|
||||
for i := 1; i < len(ref); i++ {
|
||||
ref[i] = child.bindings.plugNamespaced(ref[i], e.e.caller.bindings)
|
||||
}
|
||||
pkg, ruleRef := splitPackageAndRule(ref)
|
||||
|
||||
head := ast.RefHead(ruleRef, value)
|
||||
|
||||
// key is also part of ref in single-value rules, and can be dropped
|
||||
if rule.Head.Key != nil && rule.Head.RuleKind() == ast.MultiValue {
|
||||
head.Key = child.bindings.PlugNamespaced(rule.Head.Key, e.e.caller.bindings)
|
||||
}
|
||||
|
||||
if rule.Head.RuleKind() == ast.SingleValue && len(ruleRef) == 2 {
|
||||
head.Key = ruleRef[len(ruleRef)-1]
|
||||
}
|
||||
|
||||
if head.Name.Equal(ast.Var("")) && (len(ruleRef) == 1 || (len(ruleRef) == 2 && rule.Head.RuleKind() == ast.SingleValue)) {
|
||||
head.Name = ruleRef[0].Value.(ast.Var)
|
||||
}
|
||||
|
||||
if !e.e.inliningControl.shallow {
|
||||
cp := copypropagation.New(head.Vars()).
|
||||
@@ -2616,7 +2728,7 @@ func (e evalVirtualPartial) partialEvalSupportRule(rule *ast.Rule, path ast.Ref)
|
||||
plugged = applyCopyPropagation(cp, e.e.instr, plugged)
|
||||
}
|
||||
|
||||
e.e.saveSupport.Insert(path, &ast.Rule{
|
||||
e.e.saveSupport.InsertByPkg(pkg, &ast.Rule{
|
||||
Head: head,
|
||||
Body: plugged,
|
||||
Default: rule.Default,
|
||||
@@ -2649,6 +2761,7 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac
|
||||
var hint evalVirtualPartialCacheHint
|
||||
|
||||
if e.e.unknown(e.ref[:e.pos+1], e.bindings) {
|
||||
// FIXME: Return empty hint if unknowns in any e.ref elem overlapping with applicable rule refs?
|
||||
return hint, nil
|
||||
}
|
||||
|
||||
@@ -2660,17 +2773,29 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac
|
||||
|
||||
plugged := e.bindings.Plug(e.ref[e.pos+1])
|
||||
|
||||
if plugged.IsGround() {
|
||||
hint.key = append(e.plugged[:e.pos+1], plugged)
|
||||
if _, ok := plugged.Value.(ast.Var); ok {
|
||||
hint.full = true
|
||||
hint.key = e.plugged[:e.pos+1]
|
||||
e.e.instr.counterIncr(evalOpVirtualCacheMiss)
|
||||
return hint, nil
|
||||
}
|
||||
|
||||
m := maxRefLength(e.ir.Rules, len(e.ref))
|
||||
|
||||
for i := e.pos + 1; i < m; i++ {
|
||||
plugged = e.bindings.Plug(e.ref[i])
|
||||
|
||||
if !plugged.IsGround() {
|
||||
break
|
||||
}
|
||||
|
||||
hint.key = append(e.plugged[:i], plugged)
|
||||
|
||||
if cached, _ := e.e.virtualCache.Get(hint.key); cached != nil {
|
||||
e.e.instr.counterIncr(evalOpVirtualCacheHit)
|
||||
hint.hit = true
|
||||
return hint, e.evalTerm(iter, e.pos+2, cached, e.bindings)
|
||||
return hint, e.evalTerm(iter, i+1, cached, e.bindings)
|
||||
}
|
||||
} else if _, ok := plugged.Value.(ast.Var); ok {
|
||||
hint.full = true
|
||||
hint.key = e.plugged[:e.pos+1]
|
||||
}
|
||||
|
||||
e.e.instr.counterIncr(evalOpVirtualCacheMiss)
|
||||
@@ -2678,26 +2803,99 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac
|
||||
return hint, nil
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) reduce(head *ast.Head, b *bindings, result *ast.Term) (*ast.Term, bool, error) {
|
||||
func getNestedObject(ref ast.Ref, rootObj *ast.Object, b *bindings, l *ast.Location) (*ast.Object, error) {
|
||||
current := rootObj
|
||||
for _, term := range ref {
|
||||
key := b.Plug(term)
|
||||
if child := (*current).Get(key); child != nil {
|
||||
if val, ok := child.Value.(ast.Object); ok {
|
||||
current = &val
|
||||
} else {
|
||||
return nil, objectDocKeyConflictErr(l)
|
||||
}
|
||||
} else {
|
||||
child := ast.NewObject()
|
||||
(*current).Insert(key, ast.NewTerm(child))
|
||||
current = &child
|
||||
}
|
||||
}
|
||||
|
||||
return current, nil
|
||||
}
|
||||
|
||||
func hasCollisions(path ast.Ref, visitedRefs *[]ast.Ref, b *bindings) bool {
|
||||
collisionPathTerm := b.Plug(ast.NewTerm(path))
|
||||
collisionPath := collisionPathTerm.Value.(ast.Ref)
|
||||
for _, c := range *visitedRefs {
|
||||
if collisionPath.HasPrefix(c) && !collisionPath.Equal(c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
*visitedRefs = append(*visitedRefs, collisionPath)
|
||||
return false
|
||||
}
|
||||
|
||||
func (e evalVirtualPartial) reduce(rule *ast.Rule, b *bindings, result *ast.Term, visitedRefs *[]ast.Ref) (*ast.Term, bool, error) {
|
||||
|
||||
var exists bool
|
||||
head := rule.Head
|
||||
|
||||
switch v := result.Value.(type) {
|
||||
case ast.Set: // MultiValue
|
||||
case ast.Set:
|
||||
key := b.Plug(head.Key)
|
||||
exists = v.Contains(key)
|
||||
v.Add(key)
|
||||
case ast.Object: // SingleValue
|
||||
key := head.Reference[len(head.Reference)-1] // NOTE(sr): multiple vars in ref heads need to deal with this better
|
||||
key = b.Plug(key)
|
||||
value := b.Plug(head.Value)
|
||||
if curr := v.Get(key); curr != nil {
|
||||
if !curr.Equal(value) {
|
||||
return nil, false, objectDocKeyConflictErr(head.Location)
|
||||
case ast.Object:
|
||||
// data.p.q[r].s.t := 42 {...}
|
||||
// |----|-|
|
||||
// ^ ^
|
||||
// | leafKey
|
||||
// objPath
|
||||
fullPath := rule.Ref()
|
||||
|
||||
collisionPath := fullPath[e.pos+1:]
|
||||
if hasCollisions(collisionPath, visitedRefs, b) {
|
||||
return nil, false, objectDocKeyConflictErr(head.Location)
|
||||
}
|
||||
|
||||
objPath := fullPath[e.pos+1 : len(fullPath)-1] // the portion of the ref that generates nested objects
|
||||
leafKey := b.Plug(fullPath[len(fullPath)-1]) // the portion of the ref that is the deepest nested key for the value
|
||||
|
||||
leafObj, err := getNestedObject(objPath, &v, b, head.Location)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if kind := head.RuleKind(); kind == ast.SingleValue {
|
||||
// We're inserting into an object
|
||||
val := b.Plug(head.Value) // head.Value instance is shared between rule enumerations;but this is ok, as we don't allow rules to modify each others values.
|
||||
|
||||
if curr := (*leafObj).Get(leafKey); curr != nil {
|
||||
if !curr.Equal(val) {
|
||||
return nil, false, objectDocKeyConflictErr(head.Location)
|
||||
}
|
||||
exists = true
|
||||
} else {
|
||||
(*leafObj).Insert(leafKey, val)
|
||||
}
|
||||
exists = true
|
||||
} else {
|
||||
v.Insert(key, value)
|
||||
// We're inserting into a set
|
||||
var set *ast.Set
|
||||
if leaf := (*leafObj).Get(leafKey); leaf != nil {
|
||||
if s, ok := leaf.Value.(ast.Set); ok {
|
||||
set = &s
|
||||
} else {
|
||||
return nil, false, objectDocKeyConflictErr(head.Location)
|
||||
}
|
||||
} else {
|
||||
s := ast.NewSet()
|
||||
(*leafObj).Insert(leafKey, ast.NewTerm(s))
|
||||
set = &s
|
||||
}
|
||||
|
||||
key := b.Plug(head.Key)
|
||||
exists = (*set).Contains(key)
|
||||
(*set).Add(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2916,15 +3114,8 @@ func (e evalVirtualComplete) partialEvalSupportRule(rule *ast.Rule, path ast.Ref
|
||||
// Skip this rule body if it fails to type-check.
|
||||
// Type-checking failure means the rule body will never succeed.
|
||||
if e.e.compiler.PassesTypeCheck(plugged) {
|
||||
var name ast.Var
|
||||
switch ref := rule.Head.Ref().GroundPrefix(); len(ref) {
|
||||
case 1:
|
||||
name = ref[0].Value.(ast.Var)
|
||||
default:
|
||||
s := ref[len(ref)-1].Value.(ast.String)
|
||||
name = ast.Var(s)
|
||||
}
|
||||
head := ast.NewHead(name, nil, child.bindings.PlugNamespaced(rule.Head.Value, e.e.caller.bindings))
|
||||
pkg, ruleRef := splitPackageAndRule(path)
|
||||
head := ast.RefHead(ruleRef, child.bindings.PlugNamespaced(rule.Head.Value, e.e.caller.bindings))
|
||||
|
||||
if !e.e.inliningControl.shallow {
|
||||
cp := copypropagation.New(head.Vars()).
|
||||
@@ -2933,7 +3124,7 @@ func (e evalVirtualComplete) partialEvalSupportRule(rule *ast.Rule, path ast.Ref
|
||||
plugged = applyCopyPropagation(cp, e.e.instr, plugged)
|
||||
}
|
||||
|
||||
e.e.saveSupport.Insert(path, &ast.Rule{
|
||||
e.e.saveSupport.InsertByPkg(pkg, &ast.Rule{
|
||||
Head: head,
|
||||
Body: plugged,
|
||||
Default: rule.Default,
|
||||
|
||||
+102
-39
@@ -67,6 +67,7 @@ var allowedKeyNames = [...]string{
|
||||
"force_cache_duration_seconds",
|
||||
"raise_error",
|
||||
"caching_mode",
|
||||
"max_retry_attempts",
|
||||
}
|
||||
|
||||
// ref: https://www.rfc-editor.org/rfc/rfc7231#section-6.1
|
||||
@@ -104,6 +105,12 @@ const (
|
||||
|
||||
// HTTPSendNetworkErr represents a network error.
|
||||
HTTPSendNetworkErr string = "eval_http_send_network_error"
|
||||
|
||||
// minRetryDelay is amount of time to backoff after the first failure.
|
||||
minRetryDelay = time.Millisecond * 100
|
||||
|
||||
// maxRetryDelay is the upper bound of backoff delay.
|
||||
maxRetryDelay = time.Second * 60
|
||||
)
|
||||
|
||||
func builtinHTTPSend(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
@@ -461,7 +468,7 @@ func createHTTPRequest(bctx BuiltinContext, obj ast.Object) (*http.Request, *htt
|
||||
case "cache", "caching_mode",
|
||||
"force_cache", "force_cache_duration_seconds",
|
||||
"force_json_decode", "force_yaml_decode",
|
||||
"raise_error": // no-op
|
||||
"raise_error", "max_retry_attempts": // no-op
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("invalid parameter %q", key)
|
||||
}
|
||||
@@ -647,8 +654,39 @@ func createHTTPRequest(bctx BuiltinContext, obj ast.Object) (*http.Request, *htt
|
||||
return req, client, nil
|
||||
}
|
||||
|
||||
func executeHTTPRequest(req *http.Request, client *http.Client) (*http.Response, error) {
|
||||
return client.Do(req)
|
||||
func executeHTTPRequest(req *http.Request, client *http.Client, inputReqObj ast.Object) (*http.Response, error) {
|
||||
var err error
|
||||
var retry int
|
||||
|
||||
retry, err = getNumberValFromReqObj(inputReqObj, ast.StringTerm("max_retry_attempts"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := 0; true; i++ {
|
||||
|
||||
var resp *http.Response
|
||||
resp, err = client.Do(req)
|
||||
if err == nil {
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// final attempt
|
||||
if i == retry {
|
||||
break
|
||||
}
|
||||
|
||||
if err == context.Canceled {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(util.DefaultBackoff(float64(minRetryDelay), float64(maxRetryDelay), i)):
|
||||
case <-req.Context().Done():
|
||||
return nil, context.Canceled
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func isContentType(header http.Header, typ ...string) bool {
|
||||
@@ -765,10 +803,16 @@ func insertErrorIntoHTTPSendCache(bctx BuiltinContext, key ast.Object, err error
|
||||
func (c *interQueryCache) checkHTTPSendInterQueryCache() (ast.Value, error) {
|
||||
requestCache := c.bctx.InterQueryBuiltinCache
|
||||
|
||||
value, found := requestCache.Get(c.key)
|
||||
cachedValue, found := requestCache.Get(c.key)
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
value, cerr := requestCache.Clone(cachedValue)
|
||||
if cerr != nil {
|
||||
return nil, handleHTTPSendErr(c.bctx, cerr)
|
||||
}
|
||||
|
||||
c.bctx.Metrics.Counter(httpSendInterQueryCacheHits).Incr()
|
||||
var cachedRespData *interQueryCacheData
|
||||
|
||||
@@ -795,15 +839,12 @@ func (c *interQueryCache) checkHTTPSendInterQueryCache() (ast.Value, error) {
|
||||
return nil, handleHTTPSendErr(c.bctx, err)
|
||||
}
|
||||
|
||||
headers, err := parseResponseHeaders(cachedRespData.Headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers := parseResponseHeaders(cachedRespData.Headers)
|
||||
|
||||
// check with the server if the stale response is still up-to-date.
|
||||
// If server returns a new response (ie. status_code=200), update the cache with the new response
|
||||
// If server returns an unmodified response (ie. status_code=304), update the headers for the existing response
|
||||
result, modified, err := revalidateCachedResponse(c.httpReq, c.httpClient, headers)
|
||||
result, modified, err := revalidateCachedResponse(c.httpReq, c.httpClient, c.key, headers)
|
||||
requestCache.Delete(c.key)
|
||||
if err != nil || result == nil {
|
||||
return nil, err
|
||||
@@ -820,11 +861,16 @@ func (c *interQueryCache) checkHTTPSendInterQueryCache() (ast.Value, error) {
|
||||
}
|
||||
}
|
||||
|
||||
expiresAt, err := expiryFromHeaders(result.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if forceCaching(c.forceCacheParams) {
|
||||
createdAt := getCurrentTime(c.bctx)
|
||||
cachedRespData.ExpiresAt = createdAt.Add(time.Second * time.Duration(c.forceCacheParams.forceCacheDurationSeconds))
|
||||
} else {
|
||||
expiresAt, err := expiryFromHeaders(result.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cachedRespData.ExpiresAt = expiresAt
|
||||
}
|
||||
cachedRespData.ExpiresAt = expiresAt
|
||||
|
||||
cachingMode, err := getCachingMode(c.key)
|
||||
if err != nil {
|
||||
@@ -944,6 +990,23 @@ func getBoolValFromReqObj(req ast.Object, key *ast.Term) (bool, error) {
|
||||
return bool(b), nil
|
||||
}
|
||||
|
||||
func getNumberValFromReqObj(req ast.Object, key *ast.Term) (int, error) {
|
||||
term := req.Get(key)
|
||||
if term == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if t, ok := term.Value.(ast.Number); ok {
|
||||
num, ok := t.Int()
|
||||
if !ok || num < 0 {
|
||||
return 0, fmt.Errorf("invalid value %v for field %v", t.String(), key.String())
|
||||
}
|
||||
return num, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("invalid value %v for field %v", term.String(), key.String())
|
||||
}
|
||||
|
||||
func getCachingMode(req ast.Object) (cachingMode, error) {
|
||||
key := ast.StringTerm("caching_mode")
|
||||
var s ast.String
|
||||
@@ -980,6 +1043,12 @@ func newInterQueryCacheValue(bctx BuiltinContext, resp *http.Response, respBody
|
||||
return &interQueryCacheValue{Data: b}, nil
|
||||
}
|
||||
|
||||
func (cb interQueryCacheValue) Clone() (cache.InterQueryCacheValue, error) {
|
||||
dup := make([]byte, len(cb.Data))
|
||||
copy(dup, cb.Data)
|
||||
return &interQueryCacheValue{Data: dup}, nil
|
||||
}
|
||||
|
||||
func (cb interQueryCacheValue) SizeInBytes() int64 {
|
||||
return int64(len(cb.Data))
|
||||
}
|
||||
@@ -1063,44 +1132,38 @@ func (c *interQueryCacheData) SizeInBytes() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *interQueryCacheData) Clone() (cache.InterQueryCacheValue, error) {
|
||||
dup := make([]byte, len(c.RespBody))
|
||||
copy(dup, c.RespBody)
|
||||
|
||||
return &interQueryCacheData{
|
||||
ExpiresAt: c.ExpiresAt,
|
||||
RespBody: dup,
|
||||
Status: c.Status,
|
||||
StatusCode: c.StatusCode,
|
||||
Headers: c.Headers.Clone()}, nil
|
||||
}
|
||||
|
||||
type responseHeaders struct {
|
||||
date time.Time // origination date and time of response
|
||||
cacheControl map[string]string // response cache-control header
|
||||
maxAge deltaSeconds // max-age cache control directive
|
||||
expires time.Time // date/time after which the response is considered stale
|
||||
etag string // identifier for a specific version of the response
|
||||
lastModified string // date and time response was last modified as per origin server
|
||||
etag string // identifier for a specific version of the response
|
||||
lastModified string // date and time response was last modified as per origin server
|
||||
}
|
||||
|
||||
// deltaSeconds specifies a non-negative integer, representing
|
||||
// time in seconds: http://tools.ietf.org/html/rfc7234#section-1.2.1
|
||||
type deltaSeconds int32
|
||||
|
||||
func parseResponseHeaders(headers http.Header) (*responseHeaders, error) {
|
||||
var err error
|
||||
func parseResponseHeaders(headers http.Header) *responseHeaders {
|
||||
result := responseHeaders{}
|
||||
|
||||
result.date, err = getResponseHeaderDate(headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.cacheControl = parseCacheControlHeader(headers)
|
||||
result.maxAge, err = parseMaxAgeCacheDirective(result.cacheControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.expires = getResponseHeaderExpires(headers)
|
||||
|
||||
result.etag = headers.Get("etag")
|
||||
|
||||
result.lastModified = headers.Get("last-modified")
|
||||
|
||||
return &result, nil
|
||||
return &result
|
||||
}
|
||||
|
||||
func revalidateCachedResponse(req *http.Request, client *http.Client, headers *responseHeaders) (*http.Response, bool, error) {
|
||||
func revalidateCachedResponse(req *http.Request, client *http.Client, inputReqObj ast.Object, headers *responseHeaders) (*http.Response, bool, error) {
|
||||
etag := headers.etag
|
||||
lastModified := headers.lastModified
|
||||
|
||||
@@ -1118,7 +1181,7 @@ func revalidateCachedResponse(req *http.Request, client *http.Client, headers *r
|
||||
cloneReq.Header.Set("if-modified-since", lastModified)
|
||||
}
|
||||
|
||||
response, err := client.Do(cloneReq)
|
||||
response, err := executeHTTPRequest(cloneReq, client, inputReqObj)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -1391,7 +1454,7 @@ func (c *interQueryCache) ExecuteHTTPRequest() (*http.Response, error) {
|
||||
return nil, handleHTTPSendErr(c.bctx, err)
|
||||
}
|
||||
|
||||
return executeHTTPRequest(c.httpReq, c.httpClient)
|
||||
return executeHTTPRequest(c.httpReq, c.httpClient, c.key)
|
||||
}
|
||||
|
||||
type intraQueryCache struct {
|
||||
@@ -1441,7 +1504,7 @@ func (c *intraQueryCache) ExecuteHTTPRequest() (*http.Response, error) {
|
||||
if err != nil {
|
||||
return nil, handleHTTPSendErr(c.bctx, err)
|
||||
}
|
||||
return executeHTTPRequest(httpReq, httpClient)
|
||||
return executeHTTPRequest(httpReq, httpClient, c.key)
|
||||
}
|
||||
|
||||
func useInterQueryCache(req ast.Object) (bool, *forceCacheParams, error) {
|
||||
|
||||
+56
-16
@@ -28,32 +28,71 @@ func builtinNumbersRange(bctx BuiltinContext, operands []*ast.Term, iter func(*a
|
||||
return err
|
||||
}
|
||||
|
||||
result := ast.NewArray()
|
||||
ast, err := generateRange(bctx, x, y, one, "numbers.range")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast)
|
||||
}
|
||||
|
||||
func builtinNumbersRangeStep(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
|
||||
x, err := builtins.BigIntOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
y, err := builtins.BigIntOperand(operands[1].Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
step, err := builtins.BigIntOperand(operands[2].Value, 3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if step.Cmp(big.NewInt(0)) <= 0 {
|
||||
return fmt.Errorf("numbers.range_step: step must be a positive number above zero")
|
||||
}
|
||||
|
||||
ast, err := generateRange(bctx, x, y, step, "numbers.range_step")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast)
|
||||
}
|
||||
|
||||
func generateRange(bctx BuiltinContext, x *big.Int, y *big.Int, step *big.Int, funcName string) (*ast.Term, error) {
|
||||
|
||||
cmp := x.Cmp(y)
|
||||
|
||||
comp := func(i *big.Int, y *big.Int) bool { return i.Cmp(y) <= 0 }
|
||||
iter := func(i *big.Int) *big.Int { return i.Add(i, step) }
|
||||
|
||||
if cmp > 0 {
|
||||
comp = func(i *big.Int, y *big.Int) bool { return i.Cmp(y) >= 0 }
|
||||
iter = func(i *big.Int) *big.Int { return i.Sub(i, step) }
|
||||
}
|
||||
|
||||
result := ast.NewArray()
|
||||
haltErr := Halt{
|
||||
Err: &Error{
|
||||
Code: CancelErr,
|
||||
Message: "numbers.range: timed out before generating all numbers in range",
|
||||
Message: fmt.Sprintf("%s: timed out before generating all numbers in range", funcName),
|
||||
},
|
||||
}
|
||||
|
||||
if cmp <= 0 {
|
||||
for i := new(big.Int).Set(x); i.Cmp(y) <= 0; i = i.Add(i, one) {
|
||||
if bctx.Cancel != nil && bctx.Cancel.Cancelled() {
|
||||
return haltErr
|
||||
}
|
||||
result = result.Append(ast.NewTerm(builtins.IntToNumber(i)))
|
||||
}
|
||||
} else {
|
||||
for i := new(big.Int).Set(x); i.Cmp(y) >= 0; i = i.Sub(i, one) {
|
||||
if bctx.Cancel != nil && bctx.Cancel.Cancelled() {
|
||||
return haltErr
|
||||
}
|
||||
result = result.Append(ast.NewTerm(builtins.IntToNumber(i)))
|
||||
for i := new(big.Int).Set(x); comp(i, y); i = iter(i) {
|
||||
if bctx.Cancel != nil && bctx.Cancel.Cancelled() {
|
||||
return nil, haltErr
|
||||
}
|
||||
result = result.Append(ast.NewTerm(builtins.IntToNumber(i)))
|
||||
}
|
||||
|
||||
return iter(ast.NewTerm(result))
|
||||
return ast.NewTerm(result), nil
|
||||
}
|
||||
|
||||
func builtinRandIntn(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
@@ -95,5 +134,6 @@ func builtinRandIntn(bctx BuiltinContext, operands []*ast.Term, iter func(*ast.T
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.NumbersRange.Name, builtinNumbersRange)
|
||||
RegisterBuiltinFunc(ast.NumbersRangeStep.Name, builtinNumbersRangeStep)
|
||||
RegisterBuiltinFunc(ast.RandIntn.Name, builtinRandIntn)
|
||||
}
|
||||
|
||||
+10
-1
@@ -200,7 +200,16 @@ func mergewithOverwriteInPlace(obj, other ast.Object, frozenKeys map[*ast.Term]s
|
||||
v2 := obj.Get(k)
|
||||
// The key didn't exist in other, keep the original value.
|
||||
if v2 == nil {
|
||||
obj.Insert(k, v)
|
||||
nestedObj, ok := v.Value.(ast.Object)
|
||||
if !ok {
|
||||
// v is not an object
|
||||
obj.Insert(k, v)
|
||||
} else {
|
||||
// Copy the nested object so the original object would not be modified
|
||||
nestedObjCopy := nestedObj.Copy()
|
||||
obj.Insert(k, ast.NewTerm(nestedObjCopy))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// The key exists in both. Merge or reject change.
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ func getReqBodyBytes(body, rawBody *ast.Term) ([]byte, error) {
|
||||
}
|
||||
|
||||
func objectToMap(o ast.Object) map[string][]string {
|
||||
var out map[string][]string
|
||||
out := make(map[string][]string, o.Len())
|
||||
o.Foreach(func(k, v *ast.Term) {
|
||||
ks := stringFromTerm(k)
|
||||
vs := stringFromTerm(v)
|
||||
|
||||
+8
@@ -471,6 +471,14 @@ func (q *Query) Run(ctx context.Context) (QueryResultSet, error) {
|
||||
// Iter executes the query and invokes the iter function with query results
|
||||
// produced by evaluating the query.
|
||||
func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error {
|
||||
// Query evaluation must not be allowed if the compiler has errors and is in an undefined, possibly inconsistent state
|
||||
if q.compiler != nil && len(q.compiler.Errors) > 0 {
|
||||
return &Error{
|
||||
Code: InternalErr,
|
||||
Message: "compiler has errors",
|
||||
}
|
||||
}
|
||||
|
||||
if q.seed == nil {
|
||||
q.seed = rand.Reader
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ func builtinRegexMatch(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Te
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return iter(ast.BooleanTerm(re.Match([]byte(s2))))
|
||||
return iter(ast.BooleanTerm(re.MatchString(string(s2))))
|
||||
}
|
||||
|
||||
func builtinRegexMatchTemplate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
|
||||
+39
-5
@@ -287,22 +287,37 @@ func (s *saveSupport) List() []*ast.Module {
|
||||
}
|
||||
|
||||
func (s *saveSupport) Exists(path ast.Ref) bool {
|
||||
k := path[:len(path)-1].String()
|
||||
module, ok := s.modules[k]
|
||||
pkg, ruleRef := splitPackageAndRule(path)
|
||||
module, ok := s.modules[pkg.String()]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
name := ast.Var(path[len(path)-1].Value.(ast.String))
|
||||
|
||||
if len(ruleRef) == 1 {
|
||||
name := ruleRef[0].Value.(ast.Var)
|
||||
for _, rule := range module.Rules {
|
||||
if rule.Head.Name.Equal(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, rule := range module.Rules {
|
||||
if rule.Head.Name.Equal(name) {
|
||||
if rule.Head.Ref().HasPrefix(ruleRef) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *saveSupport) Insert(path ast.Ref, rule *ast.Rule) {
|
||||
pkg := path[:len(path)-1]
|
||||
pkg, _ := splitPackageAndRule(path)
|
||||
s.InsertByPkg(pkg, rule)
|
||||
}
|
||||
|
||||
func (s *saveSupport) InsertByPkg(pkg ast.Ref, rule *ast.Rule) {
|
||||
k := pkg.String()
|
||||
module, ok := s.modules[k]
|
||||
if !ok {
|
||||
@@ -317,6 +332,25 @@ func (s *saveSupport) Insert(path ast.Ref, rule *ast.Rule) {
|
||||
module.Rules = append(module.Rules, rule)
|
||||
}
|
||||
|
||||
func splitPackageAndRule(path ast.Ref) (ast.Ref, ast.Ref) {
|
||||
p := path.Copy()
|
||||
|
||||
ruleRefStart := 2 // path always contains at least 3 terms (data. + one term in package + rule name)
|
||||
for i := ruleRefStart; i < len(p.StringPrefix()); i++ {
|
||||
t := p[i]
|
||||
if str, ok := t.Value.(ast.String); ok && ast.IsVarCompatibleString(string(str)) {
|
||||
ruleRefStart = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pkg := p[:ruleRefStart]
|
||||
rule := p[ruleRefStart:]
|
||||
rule[0].Value = ast.Var(rule[0].Value.(ast.String))
|
||||
return pkg, rule
|
||||
}
|
||||
|
||||
// saveRequired returns true if the statement x will result in some expressions
|
||||
// being saved. This check allows the evaluator to evaluate statements
|
||||
// completely during partial evaluation as long as they do not depend on any
|
||||
|
||||
+2
-2
@@ -193,12 +193,12 @@ func arraySubset(super, sub *ast.Array) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if superCursor == super.Len() {
|
||||
if superCursor+subCursor == super.Len() {
|
||||
return false
|
||||
}
|
||||
|
||||
subElem := sub.Elem(subCursor)
|
||||
superElem := sub.Elem(superCursor + subCursor)
|
||||
superElem := super.Elem(superCursor + subCursor)
|
||||
if superElem == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
)
|
||||
|
||||
func renderTemplate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
preContentTerm, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
templateVariablesTerm, err := builtins.ObjectOperand(operands[1].Value, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var templateVariables map[string]interface{}
|
||||
|
||||
if err := ast.As(templateVariablesTerm, &templateVariables); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := template.New("template").Parse(string(preContentTerm))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Do not attempt to render if template variable keys are missing
|
||||
tmpl.Option("missingkey=error")
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, templateVariables); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast.StringTerm(buf.String()))
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.RenderTemplate.Name, renderTemplate)
|
||||
}
|
||||
+27
-1
@@ -12,6 +12,7 @@ import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
_ "time/tzdata" // this is needed to have LoadLocation when no filesystem tzdata is available
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
@@ -49,7 +50,13 @@ func builtinTimeParseNanos(_ BuiltinContext, operands []*ast.Term, iter func(*as
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := time.Parse(string(format), string(value))
|
||||
formatStr := string(format)
|
||||
// look for the formatStr in our acceptedTimeFormats and
|
||||
// use the constant instead if it matches
|
||||
if f, ok := acceptedTimeFormats[formatStr]; ok {
|
||||
formatStr = f
|
||||
}
|
||||
result, err := time.Parse(formatStr, string(value))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,6 +89,20 @@ func builtinParseDurationNanos(_ BuiltinContext, operands []*ast.Term, iter func
|
||||
return iter(ast.NumberTerm(int64ToJSONNumber(int64(value))))
|
||||
}
|
||||
|
||||
// Represent exposed constants for formatting from the stdlib time pkg
|
||||
var acceptedTimeFormats = map[string]string{
|
||||
"ANSIC": time.ANSIC,
|
||||
"UnixDate": time.UnixDate,
|
||||
"RubyDate": time.RubyDate,
|
||||
"RFC822": time.RFC822,
|
||||
"RFC822Z": time.RFC822Z,
|
||||
"RFC850": time.RFC850,
|
||||
"RFC1123": time.RFC1123,
|
||||
"RFC1123Z": time.RFC1123Z,
|
||||
"RFC3339": time.RFC3339,
|
||||
"RFC3339Nano": time.RFC3339Nano,
|
||||
}
|
||||
|
||||
func builtinFormat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
t, layout, err := tzTime(operands[0].Value)
|
||||
if err != nil {
|
||||
@@ -90,7 +111,12 @@ func builtinFormat(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term)
|
||||
// Using RFC3339Nano time formatting as default
|
||||
if layout == "" {
|
||||
layout = time.RFC3339Nano
|
||||
} else if layoutStr, ok := acceptedTimeFormats[layout]; ok {
|
||||
// if we can find a constant specified, use the constant
|
||||
layout = layoutStr
|
||||
}
|
||||
// otherwise try to treat the fmt string as a datetime fmt string
|
||||
|
||||
timestamp := t.Format(layout)
|
||||
return iter(ast.StringTerm(timestamp))
|
||||
}
|
||||
|
||||
+2
@@ -1065,6 +1065,8 @@ func builtinJWTDecodeVerify(bctx BuiltinContext, operands []*ast.Term, iter func
|
||||
if constraints.iss != issVal {
|
||||
return iter(unverified)
|
||||
}
|
||||
} else {
|
||||
return iter(unverified)
|
||||
}
|
||||
}
|
||||
// RFC7159 4.1.3 aud
|
||||
|
||||
+20
@@ -7,6 +7,7 @@ package topdown
|
||||
import (
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/internal/uuid"
|
||||
"github.com/open-policy-agent/opa/topdown/builtins"
|
||||
)
|
||||
|
||||
type uuidCachingKey string
|
||||
@@ -31,6 +32,25 @@ func builtinUUIDRFC4122(bctx BuiltinContext, operands []*ast.Term, iter func(*as
|
||||
return iter(result)
|
||||
}
|
||||
|
||||
func builtinUUIDParse(_ BuiltinContext, operands []*ast.Term, iter func(term *ast.Term) error) error {
|
||||
str, err := builtins.StringOperand(operands[0].Value, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsed, err := uuid.Parse(string(str))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
val, err := ast.InterfaceToValue(parsed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return iter(ast.NewTerm(val))
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.UUIDRFC4122.Name, builtinUUIDRFC4122)
|
||||
RegisterBuiltinFunc(ast.UUIDParse.Name, builtinUUIDParse)
|
||||
}
|
||||
|
||||
+50
-5
@@ -10,6 +10,15 @@ import (
|
||||
|
||||
func evalWalk(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
|
||||
input := operands[0]
|
||||
|
||||
if pathIsWildcard(operands) {
|
||||
// When the path assignment is a wildcard: walk(input, [_, value])
|
||||
// we may skip the path construction entirely, and simply return
|
||||
// same pointer in each iteration. This is a much more efficient
|
||||
// path when only the values are needed.
|
||||
return walkNoPath(input, iter)
|
||||
}
|
||||
|
||||
filter := getOutputPath(operands)
|
||||
return walk(filter, nil, input, iter)
|
||||
}
|
||||
@@ -70,6 +79,33 @@ func walk(filter, path *ast.Array, input *ast.Term, iter func(*ast.Term) error)
|
||||
return nil
|
||||
}
|
||||
|
||||
var emptyArr = ast.ArrayTerm()
|
||||
|
||||
func walkNoPath(input *ast.Term, iter func(*ast.Term) error) error {
|
||||
if err := iter(ast.ArrayTerm(emptyArr, input)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch v := input.Value.(type) {
|
||||
case ast.Object:
|
||||
return v.Iter(func(_, v *ast.Term) error {
|
||||
return walkNoPath(v, iter)
|
||||
})
|
||||
case *ast.Array:
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if err := walkNoPath(v.Elem(i), iter); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
case ast.Set:
|
||||
return v.Iter(func(elem *ast.Term) error {
|
||||
return walkNoPath(elem, iter)
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathAppend(path *ast.Array, key *ast.Term) *ast.Array {
|
||||
if path == nil {
|
||||
return ast.NewArray(key)
|
||||
@@ -80,17 +116,26 @@ func pathAppend(path *ast.Array, key *ast.Term) *ast.Array {
|
||||
|
||||
func getOutputPath(operands []*ast.Term) *ast.Array {
|
||||
if len(operands) == 2 {
|
||||
if arr, ok := operands[1].Value.(*ast.Array); ok {
|
||||
if arr.Len() == 2 {
|
||||
if path, ok := arr.Elem(0).Value.(*ast.Array); ok {
|
||||
return path
|
||||
}
|
||||
if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 {
|
||||
if path, ok := arr.Elem(0).Value.(*ast.Array); ok {
|
||||
return path
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathIsWildcard(operands []*ast.Term) bool {
|
||||
if len(operands) == 2 {
|
||||
if arr, ok := operands[1].Value.(*ast.Array); ok && arr.Len() == 2 {
|
||||
if v, ok := arr.Elem(0).Value.(ast.Var); ok {
|
||||
return v.IsWildcard()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func init() {
|
||||
RegisterBuiltinFunc(ast.WalkBuiltin.Name, evalWalk)
|
||||
}
|
||||
|
||||
+137
-13
@@ -214,7 +214,7 @@ func (t *Array) String() string {
|
||||
for _, tpe := range t.static {
|
||||
buf = append(buf, Sprint(tpe))
|
||||
}
|
||||
var repr = prefix
|
||||
repr := prefix
|
||||
if len(buf) > 0 {
|
||||
repr += "<" + strings.Join(buf, ", ") + ">"
|
||||
}
|
||||
@@ -259,6 +259,10 @@ func NewSet(of Type) *Set {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Set) Of() Type {
|
||||
return t.of
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of t.
|
||||
func (t *Set) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.toMap())
|
||||
@@ -351,7 +355,7 @@ func (t *Object) String() string {
|
||||
for _, p := range t.static {
|
||||
buf = append(buf, fmt.Sprintf("%v: %v", p.Key, Sprint(p.Value)))
|
||||
}
|
||||
var repr = prefix
|
||||
repr := prefix
|
||||
if len(buf) > 0 {
|
||||
repr += "<" + strings.Join(buf, ", ") + ">"
|
||||
}
|
||||
@@ -408,7 +412,6 @@ func (t *Object) toMap() map[string]interface{} {
|
||||
|
||||
// Select returns the type of the named property.
|
||||
func (t *Object) Select(name interface{}) Type {
|
||||
|
||||
pos := sort.Search(len(t.static), func(x int) bool {
|
||||
return util.Compare(t.static[x].Key, name) >= 0
|
||||
})
|
||||
@@ -426,6 +429,77 @@ func (t *Object) Select(name interface{}) Type {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Object) Merge(other Type) *Object {
|
||||
if otherObj, ok := other.(*Object); ok {
|
||||
return mergeObjects(t, otherObj)
|
||||
}
|
||||
|
||||
var typeK Type
|
||||
var typeV Type
|
||||
dynProps := t.DynamicProperties()
|
||||
if dynProps != nil {
|
||||
typeK = Or(Keys(other), dynProps.Key)
|
||||
typeV = Or(Values(other), dynProps.Value)
|
||||
dynProps = NewDynamicProperty(typeK, typeV)
|
||||
} else {
|
||||
typeK = Keys(other)
|
||||
typeV = Values(other)
|
||||
if typeK != nil && typeV != nil {
|
||||
dynProps = NewDynamicProperty(typeK, typeV)
|
||||
}
|
||||
}
|
||||
|
||||
return NewObject(t.StaticProperties(), dynProps)
|
||||
}
|
||||
|
||||
func mergeObjects(a, b *Object) *Object {
|
||||
var dynamicProps *DynamicProperty
|
||||
if a.dynamic != nil && b.dynamic != nil {
|
||||
typeK := Or(a.dynamic.Key, b.dynamic.Key)
|
||||
var typeV Type
|
||||
aObj, aIsObj := a.dynamic.Value.(*Object)
|
||||
bObj, bIsObj := b.dynamic.Value.(*Object)
|
||||
if aIsObj && bIsObj {
|
||||
typeV = mergeObjects(aObj, bObj)
|
||||
} else {
|
||||
typeV = Or(a.dynamic.Value, b.dynamic.Value)
|
||||
}
|
||||
dynamicProps = NewDynamicProperty(typeK, typeV)
|
||||
} else if a.dynamic != nil {
|
||||
dynamicProps = a.dynamic
|
||||
} else {
|
||||
dynamicProps = b.dynamic
|
||||
}
|
||||
|
||||
staticPropsMap := make(map[interface{}]Type)
|
||||
|
||||
for _, sp := range a.static {
|
||||
staticPropsMap[sp.Key] = sp.Value
|
||||
}
|
||||
|
||||
for _, sp := range b.static {
|
||||
currV := staticPropsMap[sp.Key]
|
||||
if currV != nil {
|
||||
currVObj, currVIsObj := currV.(*Object)
|
||||
spVObj, spVIsObj := sp.Value.(*Object)
|
||||
if currVIsObj && spVIsObj {
|
||||
staticPropsMap[sp.Key] = mergeObjects(currVObj, spVObj)
|
||||
} else {
|
||||
staticPropsMap[sp.Key] = Or(currV, sp.Value)
|
||||
}
|
||||
} else {
|
||||
staticPropsMap[sp.Key] = sp.Value
|
||||
}
|
||||
}
|
||||
|
||||
staticProps := make([]*StaticProperty, 0, len(staticPropsMap))
|
||||
for k, v := range staticPropsMap {
|
||||
staticProps = append(staticProps, NewStaticProperty(k, v))
|
||||
}
|
||||
|
||||
return NewObject(staticProps, dynamicProps)
|
||||
}
|
||||
|
||||
// Any represents a dynamic type.
|
||||
type Any []Type
|
||||
|
||||
@@ -491,22 +565,73 @@ func (t Any) Merge(other Type) Any {
|
||||
}
|
||||
|
||||
// Union returns a new Any type that is the union of the two Any types.
|
||||
// Note(philipc): The two Any slices MUST be sorted before running Union,
|
||||
// or else this method will fail to merge the two slices correctly.
|
||||
func (t Any) Union(other Any) Any {
|
||||
if len(t) == 0 {
|
||||
lenT := len(t)
|
||||
lenOther := len(other)
|
||||
// Return the more general (blank) Any type if present.
|
||||
if lenT == 0 {
|
||||
return t
|
||||
}
|
||||
if len(other) == 0 {
|
||||
if lenOther == 0 {
|
||||
return other
|
||||
}
|
||||
cpy := make(Any, len(t))
|
||||
copy(cpy, t)
|
||||
for i := range other {
|
||||
if !cpy.Contains(other[i]) {
|
||||
cpy = append(cpy, other[i])
|
||||
// Prealloc the output list.
|
||||
maxLen := lenT
|
||||
if lenT < lenOther {
|
||||
maxLen = lenOther
|
||||
}
|
||||
merged := make(Any, 0, maxLen)
|
||||
// Note(philipc): Create a merged slice, doing the minimum number of
|
||||
// comparisons along the way. We treat this as a problem of merging two
|
||||
// sorted lists that might have duplicates. This specifically saves us
|
||||
// from cases where one list might be *much* longer than the other.
|
||||
// Algorithm:
|
||||
// Assume:
|
||||
// - List A
|
||||
// - List B
|
||||
// - List Output
|
||||
// - Idx_a, Idx_b
|
||||
// Procedure:
|
||||
// - While Idx_a < len(A) and Idx_b < len(B)
|
||||
// - Compare head(A) and head(B)
|
||||
// - Cases:
|
||||
// - A < B: Append head(A) to Output, advance Idx_a
|
||||
// - A == B: Append head(A) to Output, advance Idx_a, Idx_b
|
||||
// - A > B: Append head(B) to Output, advance Idx_b
|
||||
// - Return output
|
||||
idxA := 0
|
||||
idxB := 0
|
||||
for idxA < lenT || idxB < lenOther {
|
||||
// Early-exit cases:
|
||||
if idxA == lenT {
|
||||
// Ran out of elements in t. Copy over what's left from other.
|
||||
merged = append(merged, other[idxB:]...)
|
||||
break
|
||||
} else if idxB == lenOther {
|
||||
// Ran out of elements in other. Copy over what's left from t.
|
||||
merged = append(merged, t[idxA:]...)
|
||||
break
|
||||
}
|
||||
// Normal selection of next element to merge:
|
||||
switch Compare(t[idxA], other[idxB]) {
|
||||
// A < B:
|
||||
case -1:
|
||||
merged = append(merged, t[idxA])
|
||||
idxA++
|
||||
// A == B:
|
||||
case 0:
|
||||
merged = append(merged, t[idxA])
|
||||
idxA++
|
||||
idxB++
|
||||
// A > B:
|
||||
case 1:
|
||||
merged = append(merged, other[idxB])
|
||||
idxB++
|
||||
}
|
||||
}
|
||||
sort.Sort(typeSlice(cpy))
|
||||
return cpy
|
||||
return merged
|
||||
}
|
||||
|
||||
func (t Any) String() string {
|
||||
@@ -631,7 +756,6 @@ func (t *Function) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// UnmarshalJSON decodes the JSON serialized function declaration.
|
||||
func (t *Function) UnmarshalJSON(bs []byte) error {
|
||||
|
||||
tpe, err := Unmarshal(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
+21
-8
@@ -11,7 +11,9 @@ import (
|
||||
"io"
|
||||
"reflect"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/open-policy-agent/opa/loader/extension"
|
||||
)
|
||||
|
||||
// UnmarshalJSON parses the JSON encoded data and stores the result in the value
|
||||
@@ -19,10 +21,17 @@ import (
|
||||
//
|
||||
// This function is intended to be used in place of the standard json.Marshal
|
||||
// function when json.Number is required.
|
||||
func UnmarshalJSON(bs []byte, x interface{}) (err error) {
|
||||
func UnmarshalJSON(bs []byte, x interface{}) error {
|
||||
return unmarshalJSON(bs, x, true)
|
||||
}
|
||||
|
||||
func unmarshalJSON(bs []byte, x interface{}, ext bool) error {
|
||||
buf := bytes.NewBuffer(bs)
|
||||
decoder := NewJSONDecoder(buf)
|
||||
if err := decoder.Decode(x); err != nil {
|
||||
if handler := extension.FindExtension(".json"); handler != nil && ext {
|
||||
return handler(bs, x)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -103,14 +112,18 @@ func Reference(x interface{}) *interface{} {
|
||||
return &x
|
||||
}
|
||||
|
||||
// Unmarshal decodes a YAML or JSON value into the specified type.
|
||||
// Unmarshal decodes a YAML, JSON or JSON extension value into the specified type.
|
||||
func Unmarshal(bs []byte, v interface{}) error {
|
||||
if json.Valid(bs) {
|
||||
return UnmarshalJSON(bs, v)
|
||||
return unmarshalJSON(bs, v, false)
|
||||
}
|
||||
bs, err := yaml.YAMLToJSON(bs)
|
||||
if err != nil {
|
||||
return err
|
||||
nbs, err := yaml.YAMLToJSON(bs)
|
||||
if err == nil {
|
||||
return unmarshalJSON(nbs, v, false)
|
||||
}
|
||||
return UnmarshalJSON(bs, v)
|
||||
// not json or yaml: try extensions
|
||||
if handler := extension.FindExtension(".json"); handler != nil {
|
||||
return handler(bs, v)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
+23
-1
@@ -7,10 +7,11 @@ package version
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Version is the canonical version of OPA.
|
||||
var Version = "0.51.0"
|
||||
var Version = "0.59.0"
|
||||
|
||||
// GoVersion is the version of Go this was built with
|
||||
var GoVersion = runtime.Version()
|
||||
@@ -25,3 +26,24 @@ var (
|
||||
Timestamp = ""
|
||||
Hostname = ""
|
||||
)
|
||||
|
||||
func init() {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
dirty := false
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.time":
|
||||
Timestamp = s.Value
|
||||
case "vcs.revision":
|
||||
Vcs = s.Value
|
||||
case "vcs.modified":
|
||||
dirty = s.Value == "true"
|
||||
}
|
||||
}
|
||||
if dirty {
|
||||
Vcs = Vcs + "-dirty"
|
||||
}
|
||||
}
|
||||
|
||||
-25
@@ -6,28 +6,3 @@
|
||||
// +build go1.18
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
dirty := false
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.time":
|
||||
Timestamp = s.Value
|
||||
case "vcs.revision":
|
||||
Vcs = s.Value
|
||||
case "vcs.modified":
|
||||
dirty = s.Value == "true"
|
||||
}
|
||||
}
|
||||
if dirty {
|
||||
Vcs = Vcs + "-dirty"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user