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:
dependabot[bot]
2023-12-05 09:47:11 +01:00
committed by Ralf Haferkamp
parent a6a6c22c14
commit 1f069c7c00
197 changed files with 73803 additions and 3024 deletions
+15 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+187
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -76,7 +76,7 @@ var strings = [...]string{
EOF: "eof",
Whitespace: "whitespace",
Comment: "comment",
Ident: "ident",
Ident: "identifier",
Package: "package",
Import: "import",
As: "as",
+36
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
File diff suppressed because it is too large Load Diff
+27 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+258
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
}
@@ -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
1 opa_agg_count opa_value_type
344 opa_json_writer_write opa_free
345 opa_json_dump opa_json_writer_write
346 opa_value_dump opa_json_writer_write
347 move_freelists opa_abort
348 opa_heap_blocks_stash move_freelists
349 opa_heap_blocks_restore move_freelists
350 opa_malloc opa_free_bulk_commit
351 opa_malloc opa_abort
352 opa_free_bulk_commit merge_sort_blocks
353 opa_realloc opa_malloc
354 opa_realloc memcpy
355 opa_realloc opa_free
356 opa_builtin_cache_get opa_abort
357 opa_builtin_cache_set opa_abort
358 merge_sort_blocks merge_sort_blocks
359 opa_memoize_init opa_malloc
360 opa_memoize_init opa_object
361 opa_memoize_push opa_malloc
575 opa_sets_intersection opa_set
576 opa_sets_intersection opa_set_union
577 opa_sets_intersection opa_set_intersection
578 opa_sets_intersection opa_value_free opa_value_free_shallow
579 opa_set_union opa_value_type
580 opa_set_union opa_set
581 opa_set_union opa_set_add
582 opa_sets_union opa_value_type
583 opa_sets_union opa_set
584 opa_sets_union opa_set_add
585 opa_sets_union opa_value_free opa_value_free_shallow
586 opa_strings_any_prefix_match opa_value_type
587 opa_strings_any_prefix_match opa_value_iter
588 opa_strings_any_prefix_match opa_value_get
791 opa_object_keys opa_value_compare_object
792 opa_object_keys opa_abort
793 opa_object_keys opa_value_compare_set
794 opa_value_free opa_array_free opa_free __opa_value_free
795 opa_array_free opa_free
796 opa_array_free opa_free_bulk
797 __opa_value_free opa_free_bulk
798 __opa_value_free opa_free
799 __opa_value_free opa_array_free
800 __opa_value_free __opa_object_buckets_free
801 __opa_value_free __opa_set_buckets_free
802 __opa_object_buckets_free opa_free
803 __opa_object_buckets_free opa_array_free
804 __opa_object_buckets_free __opa_object_buckets_free
805 __opa_object_buckets_free __opa_set_buckets_free
806 __opa_object_buckets_free __opa_value_free
807 __opa_object_buckets_free opa_free_bulk
808 __opa_set_buckets_free opa_free
809 __opa_set_buckets_free opa_array_free
810 __opa_set_buckets_free __opa_object_buckets_free
811 __opa_set_buckets_free __opa_set_buckets_free
812 __opa_set_buckets_free __opa_value_free
813 __opa_set_buckets_free opa_free_bulk
814 opa_value_free __opa_value_free
815 opa_value_free_shallow __opa_value_free
816 opa_value_merge opa_malloc
817 opa_value_merge opa_value_get
818 opa_value_merge opa_object_insert __opa_object_insert
819 opa_value_merge opa_value_merge
820 opa_value_merge opa_abort
821 opa_value_merge opa_atoi64
825 opa_value_merge opa_value_compare
826 opa_value_merge opa_value_compare_object
827 opa_value_merge opa_value_compare_set
828 opa_object_insert __opa_object_insert opa_value_hash
829 opa_object_insert __opa_object_insert opa_value_compare
830 opa_object_insert __opa_object_insert __opa_object_grow __opa_value_free
831 opa_object_insert __opa_object_insert opa_malloc __opa_object_grow
832 __opa_object_grow __opa_object_insert opa_malloc
833 __opa_object_grow opa_object_insert opa_value_hash __opa_object_insert
__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
834 opa_boolean opa_malloc
835 opa_number_ref opa_malloc
836 opa_number_int opa_malloc
838 opa_value_shallow_copy_object opa_malloc
839 opa_value_shallow_copy_object opa_value_iter
840 opa_value_shallow_copy_object opa_value_get
841 opa_value_shallow_copy_object opa_object_insert __opa_object_insert
842 opa_value_shallow_copy_set opa_malloc
843 opa_value_shallow_copy_set opa_value_iter
844 opa_value_shallow_copy_set opa_set_add
881 opa_object opa_malloc
882 opa_set opa_malloc
883 opa_set_with_cap opa_malloc
884 __opa_object_grow opa_malloc
885 __opa_object_grow opa_value_hash
886 __opa_object_grow opa_value_compare_number
887 __opa_object_grow opa_strncmp
888 __opa_object_grow opa_value_compare
889 __opa_object_grow opa_value_compare_object
890 __opa_object_grow opa_value_compare_set
891 __opa_object_grow opa_abort
892 __opa_object_grow opa_free
893 opa_object_remove opa_value_hash
894 opa_object_remove opa_value_compare
895 opa_object_remove __opa_value_free
896 opa_object_remove opa_free_bulk
897 opa_object_remove opa_free
898 opa_string_copy opa_malloc
899 opa_value_add_path opa_value_get
900 opa_value_add_path opa_malloc
901 opa_value_add_path opa_object_insert __opa_object_insert
902 opa_value_add_path opa_value_free __opa_value_free
903 opa_value_remove_path opa_value_get
904 opa_value_remove_path opa_value_hash opa_object_remove
opa_value_remove_path opa_value_compare
opa_value_remove_path opa_value_free
opa_value_remove_path opa_free
905 opa_lookup opa_value_get
906 opa_lookup opa_value_iter
907 opa_lookup opa_atoi64
1068 opa_regex_find_all_string_submatch compile\28char\20const*\29
1069 opa_regex_find_all_string_submatch opa_array
1070 opa_regex_find_all_string_submatch memset
opa_regex_find_all_string_submatch strlen
1071 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
1072 opa_regex_find_all_string_submatch fullrune
1073 opa_regex_find_all_string_submatch chartorune
Binary file not shown.
+5 -21
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -0,0 +1,7 @@
//go:build go1.20
package errors
import "errors"
var Join = errors.Join
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}