diff --git a/.golangci.yaml b/.golangci.yaml index 2e99a3915f..8aeb515bd3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -3,6 +3,139 @@ run: issues: max-same-issues: 0 # don't hide issues in CI runs because they are the same type + exclude-rules: + - path: ast/ + linters: + - staticcheck + text: "SA1019" + - path: bundle/ + linters: + - staticcheck + text: "SA1019" + - path: capabilities/ + linters: + - staticcheck + text: "SA1019" + - path: compile/ + linters: + - staticcheck + text: "SA1019" + - path: config/ + linters: + - staticcheck + text: "SA1019" + - path: cover/ + linters: + - staticcheck + text: "SA1019" + - path: debug/ + linters: + - staticcheck + text: "SA1019" + - path: dependencies/ + linters: + - staticcheck + text: "SA1019" + - path: download/ + linters: + - staticcheck + text: "SA1019" + - path: format/ + linters: + - staticcheck + text: "SA1019" + - path: hooks/ + linters: + - staticcheck + text: "SA1019" + - path: ir/ + linters: + - staticcheck + text: "SA1019" + - path: keys/ + linters: + - staticcheck + text: "SA1019" + - path: loader/ + linters: + - staticcheck + text: "SA1019" + - path: logging/ + linters: + - staticcheck + text: "SA1019" + - path: metrics/ + linters: + - staticcheck + text: "SA1019" + - path: plugins/ + linters: + - staticcheck + text: "SA1019" + - path: profiler/ + linters: + - staticcheck + text: "SA1019" + - path: refactor/ + linters: + - staticcheck + text: "SA1019" + - path: repl/ + linters: + - staticcheck + text: "SA1019" + - path: rego/ + linters: + - staticcheck + text: "SA1019" + - path: resolver/ + linters: + - staticcheck + text: "SA1019" + - path: runtime/ + linters: + - staticcheck + text: "SA1019" + - path: schemas/ + linters: + - staticcheck + text: "SA1019" + - path: sdk/ + linters: + - staticcheck + text: "SA1019" + - path: server/ + linters: + - staticcheck + text: "SA1019" + - path: storage/ + linters: + - staticcheck + text: "SA1019" + - path: tester/ + linters: + - staticcheck + text: "SA1019" + - path: topdown/ + linters: + - staticcheck + text: "SA1019" + - path: tracing/ + linters: + - staticcheck + text: "SA1019" + - path: types/ + linters: + - staticcheck + text: "SA1019" + - path: util/ + linters: + - staticcheck + text: "SA1019" + - path: version/ + linters: + - staticcheck + text: "SA1019" linter-settings: lll: diff --git a/Makefile b/Makefile index ee6da35423..8c80a8bfb1 100644 --- a/Makefile +++ b/Makefile @@ -479,7 +479,7 @@ check-go-module: .PHONY: check-yaml-tests check-yaml-tests: ifeq ($(DOCKER_RUNNING), 1) - docker run --rm -v $(shell pwd):/data:ro,Z -w /data pipelinecomponents/yamllint:${YAML_LINT_VERSION} yamllint -f $(YAML_LINT_FORMAT) test/cases/testdata + docker run --rm -v $(shell pwd):/data:ro,Z -w /data pipelinecomponents/yamllint:${YAML_LINT_VERSION} yamllint -f $(YAML_LINT_FORMAT) v1/test/cases/testdata else @echo "Docker not installed or running. Skipping yamllint run." endif diff --git a/ast/annotations.go b/ast/annotations.go new file mode 100644 index 0000000000..533290d323 --- /dev/null +++ b/ast/annotations.go @@ -0,0 +1,33 @@ +// Copyright 2022 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +type ( + // Annotations represents metadata attached to other AST nodes such as rules. + Annotations = v1.Annotations + + // SchemaAnnotation contains a schema declaration for the document identified by the path. + SchemaAnnotation = v1.SchemaAnnotation + + AuthorAnnotation = v1.AuthorAnnotation + + RelatedResourceAnnotation = v1.RelatedResourceAnnotation + + AnnotationSet = v1.AnnotationSet + + AnnotationsRef = v1.AnnotationsRef + + AnnotationsRefSet = v1.AnnotationsRefSet + + FlatAnnotationsRefSet = v1.FlatAnnotationsRefSet +) + +func NewAnnotationsRef(a *Annotations) *AnnotationsRef { + return v1.NewAnnotationsRef(a) +} diff --git a/ast/builtins.go b/ast/builtins.go new file mode 100644 index 0000000000..d0ab69a163 --- /dev/null +++ b/ast/builtins.go @@ -0,0 +1,634 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Builtins is the registry of built-in functions supported by OPA. +// Call RegisterBuiltin to add a new built-in. +var Builtins = v1.Builtins + +// RegisterBuiltin adds a new built-in function to the registry. +func RegisterBuiltin(b *Builtin) { + v1.RegisterBuiltin(b) +} + +// DefaultBuiltins is the registry of built-in functions supported in OPA +// by default. When adding a new built-in function to OPA, update this +// list. +var DefaultBuiltins = v1.DefaultBuiltins + +// BuiltinMap provides a convenient mapping of built-in names to +// built-in definitions. +var BuiltinMap = v1.BuiltinMap + +// Deprecated: Builtins can now be directly annotated with the +// Nondeterministic property, and when set to true, will be ignored +// for partial evaluation. +var IgnoreDuringPartialEval = v1.IgnoreDuringPartialEval + +/** + * Unification + */ + +// Equality represents the "=" operator. +var Equality = v1.Equality + +/** + * Assignment + */ + +// Assign represents the assignment (":=") operator. +var Assign = v1.Assign + +// Member represents the `in` (infix) operator. +var Member = v1.Member + +// MemberWithKey represents the `in` (infix) operator when used +// with two terms on the lhs, i.e., `k, v in obj`. +var MemberWithKey = v1.MemberWithKey + +var GreaterThan = v1.GreaterThan + +var GreaterThanEq = v1.GreaterThanEq + +// LessThan represents the "<" comparison operator. +var LessThan = v1.LessThan + +var LessThanEq = v1.LessThanEq + +var NotEqual = v1.NotEqual + +// Equal represents the "==" comparison operator. +var Equal = v1.Equal + +var Plus = v1.Plus + +var Minus = v1.Minus + +var Multiply = v1.Multiply + +var Divide = v1.Divide + +var Round = v1.Round + +var Ceil = v1.Ceil + +var Floor = v1.Floor + +var Abs = v1.Abs + +var Rem = v1.Rem + +/** + * Bitwise + */ + +var BitsOr = v1.BitsOr + +var BitsAnd = v1.BitsAnd + +var BitsNegate = v1.BitsNegate + +var BitsXOr = v1.BitsXOr + +var BitsShiftLeft = v1.BitsShiftLeft + +var BitsShiftRight = v1.BitsShiftRight + +/** + * Sets + */ + +var And = v1.And + +// Or performs a union operation on sets. +var Or = v1.Or + +var Intersection = v1.Intersection + +var Union = v1.Union + +/** + * Aggregates + */ + +var Count = v1.Count + +var Sum = v1.Sum + +var Product = v1.Product + +var Max = v1.Max + +var Min = v1.Min + +/** + * Sorting + */ + +var Sort = v1.Sort + +/** + * Arrays + */ + +var ArrayConcat = v1.ArrayConcat + +var ArraySlice = v1.ArraySlice + +var ArrayReverse = v1.ArrayReverse + +/** + * Conversions + */ + +var ToNumber = v1.ToNumber + +/** + * Regular Expressions + */ + +var RegexMatch = v1.RegexMatch + +var RegexIsValid = v1.RegexIsValid + +var RegexFindAllStringSubmatch = v1.RegexFindAllStringSubmatch + +var RegexTemplateMatch = v1.RegexTemplateMatch + +var RegexSplit = v1.RegexSplit + +// RegexFind takes two strings and a number, the pattern, the value and number of match values to +// return, -1 means all match values. +var RegexFind = v1.RegexFind + +// GlobsMatch takes two strings regexp-style strings and evaluates to true if their +// intersection matches a non-empty set of non-empty strings. +// Examples: +// - "a.a." and ".b.b" -> true. +// - "[a-z]*" and [0-9]+" -> not true. +var GlobsMatch = v1.GlobsMatch + +/** + * Strings + */ + +var AnyPrefixMatch = v1.AnyPrefixMatch + +var AnySuffixMatch = v1.AnySuffixMatch + +var Concat = v1.Concat + +var FormatInt = v1.FormatInt + +var IndexOf = v1.IndexOf + +var IndexOfN = v1.IndexOfN + +var Substring = v1.Substring + +var Contains = v1.Contains + +var StringCount = v1.StringCount + +var StartsWith = v1.StartsWith + +var EndsWith = v1.EndsWith + +var Lower = v1.Lower + +var Upper = v1.Upper + +var Split = v1.Split + +var Replace = v1.Replace + +var ReplaceN = v1.ReplaceN + +var RegexReplace = v1.RegexReplace + +var Trim = v1.Trim + +var TrimLeft = v1.TrimLeft + +var TrimPrefix = v1.TrimPrefix + +var TrimRight = v1.TrimRight + +var TrimSuffix = v1.TrimSuffix + +var TrimSpace = v1.TrimSpace + +var Sprintf = v1.Sprintf + +var StringReverse = v1.StringReverse + +var RenderTemplate = v1.RenderTemplate + +/** + * Numbers + */ + +// RandIntn returns a random number 0 - n +// Marked non-deterministic because it relies on RNG internally. +var RandIntn = v1.RandIntn + +var NumbersRange = v1.NumbersRange + +var NumbersRangeStep = v1.NumbersRangeStep + +/** + * Units + */ + +var UnitsParse = v1.UnitsParse + +var UnitsParseBytes = v1.UnitsParseBytes + +// +/** + * Type + */ + +// UUIDRFC4122 returns a version 4 UUID string. +// Marked non-deterministic because it relies on RNG internally. +var UUIDRFC4122 = v1.UUIDRFC4122 + +var UUIDParse = v1.UUIDParse + +/** + * JSON + */ + +var JSONFilter = v1.JSONFilter + +var JSONRemove = v1.JSONRemove + +var JSONPatch = v1.JSONPatch + +var ObjectSubset = v1.ObjectSubset + +var ObjectUnion = v1.ObjectUnion + +var ObjectUnionN = v1.ObjectUnionN + +var ObjectRemove = v1.ObjectRemove + +var ObjectFilter = v1.ObjectFilter + +var ObjectGet = v1.ObjectGet + +var ObjectKeys = v1.ObjectKeys + +/* + * Encoding + */ + +var JSONMarshal = v1.JSONMarshal + +var JSONMarshalWithOptions = v1.JSONMarshalWithOptions + +var JSONUnmarshal = v1.JSONUnmarshal + +var JSONIsValid = v1.JSONIsValid + +var Base64Encode = v1.Base64Encode + +var Base64Decode = v1.Base64Decode + +var Base64IsValid = v1.Base64IsValid + +var Base64UrlEncode = v1.Base64UrlEncode + +var Base64UrlEncodeNoPad = v1.Base64UrlEncodeNoPad + +var Base64UrlDecode = v1.Base64UrlDecode + +var URLQueryDecode = v1.URLQueryDecode + +var URLQueryEncode = v1.URLQueryEncode + +var URLQueryEncodeObject = v1.URLQueryEncodeObject + +var URLQueryDecodeObject = v1.URLQueryDecodeObject + +var YAMLMarshal = v1.YAMLMarshal + +var YAMLUnmarshal = v1.YAMLUnmarshal + +// YAMLIsValid verifies the input string is a valid YAML document. +var YAMLIsValid = v1.YAMLIsValid + +var HexEncode = v1.HexEncode + +var HexDecode = v1.HexDecode + +/** + * Tokens + */ + +var JWTDecode = v1.JWTDecode + +var JWTVerifyRS256 = v1.JWTVerifyRS256 + +var JWTVerifyRS384 = v1.JWTVerifyRS384 + +var JWTVerifyRS512 = v1.JWTVerifyRS512 + +var JWTVerifyPS256 = v1.JWTVerifyPS256 + +var JWTVerifyPS384 = v1.JWTVerifyPS384 + +var JWTVerifyPS512 = v1.JWTVerifyPS512 + +var JWTVerifyES256 = v1.JWTVerifyES256 + +var JWTVerifyES384 = v1.JWTVerifyES384 + +var JWTVerifyES512 = v1.JWTVerifyES512 + +var JWTVerifyHS256 = v1.JWTVerifyHS256 + +var JWTVerifyHS384 = v1.JWTVerifyHS384 + +var JWTVerifyHS512 = v1.JWTVerifyHS512 + +// Marked non-deterministic because it relies on time internally. +var JWTDecodeVerify = v1.JWTDecodeVerify + +// Marked non-deterministic because it relies on RNG internally. +var JWTEncodeSignRaw = v1.JWTEncodeSignRaw + +// Marked non-deterministic because it relies on RNG internally. +var JWTEncodeSign = v1.JWTEncodeSign + +/** + * Time + */ + +// Marked non-deterministic because it relies on time directly. +var NowNanos = v1.NowNanos + +var ParseNanos = v1.ParseNanos + +var ParseRFC3339Nanos = v1.ParseRFC3339Nanos + +var ParseDurationNanos = v1.ParseDurationNanos + +var Format = v1.Format + +var Date = v1.Date + +var Clock = v1.Clock + +var Weekday = v1.Weekday + +var AddDate = v1.AddDate + +var Diff = v1.Diff + +/** + * Crypto. + */ + +var CryptoX509ParseCertificates = v1.CryptoX509ParseCertificates + +var CryptoX509ParseAndVerifyCertificates = v1.CryptoX509ParseAndVerifyCertificates + +var CryptoX509ParseAndVerifyCertificatesWithOptions = v1.CryptoX509ParseAndVerifyCertificatesWithOptions + +var CryptoX509ParseCertificateRequest = v1.CryptoX509ParseCertificateRequest + +var CryptoX509ParseKeyPair = v1.CryptoX509ParseKeyPair +var CryptoX509ParseRSAPrivateKey = v1.CryptoX509ParseRSAPrivateKey + +var CryptoParsePrivateKeys = v1.CryptoParsePrivateKeys + +var CryptoMd5 = v1.CryptoMd5 + +var CryptoSha1 = v1.CryptoSha1 + +var CryptoSha256 = v1.CryptoSha256 + +var CryptoHmacMd5 = v1.CryptoHmacMd5 + +var CryptoHmacSha1 = v1.CryptoHmacSha1 + +var CryptoHmacSha256 = v1.CryptoHmacSha256 + +var CryptoHmacSha512 = v1.CryptoHmacSha512 + +var CryptoHmacEqual = v1.CryptoHmacEqual + +/** + * Graphs. + */ + +var WalkBuiltin = v1.WalkBuiltin + +var ReachableBuiltin = v1.ReachableBuiltin + +var ReachablePathsBuiltin = v1.ReachablePathsBuiltin + +/** + * Type + */ + +var IsNumber = v1.IsNumber + +var IsString = v1.IsString + +var IsBoolean = v1.IsBoolean + +var IsArray = v1.IsArray + +var IsSet = v1.IsSet + +var IsObject = v1.IsObject + +var IsNull = v1.IsNull + +/** + * Type Name + */ + +// TypeNameBuiltin returns the type of the input. +var TypeNameBuiltin = v1.TypeNameBuiltin + +/** + * HTTP Request + */ + +// Marked non-deterministic because HTTP request results can be non-deterministic. +var HTTPSend = v1.HTTPSend + +/** + * GraphQL + */ + +// GraphQLParse returns a pair of AST objects from parsing/validation. +var GraphQLParse = v1.GraphQLParse + +// GraphQLParseAndVerify returns a boolean and a pair of AST object from parsing/validation. +var GraphQLParseAndVerify = v1.GraphQLParseAndVerify + +// GraphQLParseQuery parses the input GraphQL query and returns a JSON +// representation of its AST. +var GraphQLParseQuery = v1.GraphQLParseQuery + +// GraphQLParseSchema parses the input GraphQL schema and returns a JSON +// representation of its AST. +var GraphQLParseSchema = v1.GraphQLParseSchema + +// GraphQLIsValid returns true if a GraphQL query is valid with a given +// schema, and returns false for all other inputs. +var GraphQLIsValid = v1.GraphQLIsValid + +// GraphQLSchemaIsValid returns true if the input is valid GraphQL schema, +// and returns false for all other inputs. +var GraphQLSchemaIsValid = v1.GraphQLSchemaIsValid + +/** + * JSON Schema + */ + +// JSONSchemaVerify returns empty string if the input is valid JSON schema +// and returns error string for all other inputs. +var JSONSchemaVerify = v1.JSONSchemaVerify + +// JSONMatchSchema returns empty array if the document matches the JSON schema, +// and returns non-empty array with error objects otherwise. +var JSONMatchSchema = v1.JSONMatchSchema + +/** + * Cloud Provider Helper Functions + */ + +var ProvidersAWSSignReqObj = v1.ProvidersAWSSignReqObj + +/** + * Rego + */ + +var RegoParseModule = v1.RegoParseModule + +var RegoMetadataChain = v1.RegoMetadataChain + +// RegoMetadataRule returns the metadata for the active rule +var RegoMetadataRule = v1.RegoMetadataRule + +/** + * OPA + */ + +// Marked non-deterministic because of unpredictable config/environment-dependent results. +var OPARuntime = v1.OPARuntime + +/** + * Trace + */ + +var Trace = v1.Trace + +/** + * Glob + */ + +var GlobMatch = v1.GlobMatch + +var GlobQuoteMeta = v1.GlobQuoteMeta + +/** + * Networking + */ + +var NetCIDRIntersects = v1.NetCIDRIntersects + +var NetCIDRExpand = v1.NetCIDRExpand + +var NetCIDRContains = v1.NetCIDRContains + +var NetCIDRContainsMatches = v1.NetCIDRContainsMatches + +var NetCIDRMerge = v1.NetCIDRMerge + +var NetCIDRIsValid = v1.NetCIDRIsValid + +// Marked non-deterministic because DNS resolution results can be non-deterministic. +var NetLookupIPAddr = v1.NetLookupIPAddr + +/** + * Semantic Versions + */ + +var SemVerIsValid = v1.SemVerIsValid + +var SemVerCompare = v1.SemVerCompare + +/** + * Printing + */ + +// Print is a special built-in function that writes zero or more operands +// to a message buffer. The caller controls how the buffer is displayed. The +// operands may be of any type. Furthermore, unlike other built-in functions, +// undefined operands DO NOT cause the print() function to fail during +// evaluation. +var Print = v1.Print + +// InternalPrint represents the internal implementation of the print() function. +// The compiler rewrites print() calls to refer to the internal implementation. +var InternalPrint = v1.InternalPrint + +/** + * Deprecated built-ins. + */ + +// SetDiff has been replaced by the minus built-in. +var SetDiff = v1.SetDiff + +// NetCIDROverlap has been replaced by the `net.cidr_contains` built-in. +var NetCIDROverlap = v1.NetCIDROverlap + +// CastArray checks the underlying type of the input. If it is array or set, an array +// containing the values is returned. If it is not an array, an error is thrown. +var CastArray = v1.CastArray + +// CastSet checks the underlying type of the input. +// If it is a set, the set is returned. +// If it is an array, the array is returned in set form (all duplicates removed) +// If neither, an error is thrown +var CastSet = v1.CastSet + +// CastString returns input if it is a string; if not returns error. +// For formatting variables, see sprintf +var CastString = v1.CastString + +// CastBoolean returns input if it is a boolean; if not returns error. +var CastBoolean = v1.CastBoolean + +// CastNull returns null if input is null; if not returns error. +var CastNull = v1.CastNull + +// CastObject returns the given object if it is null; throws an error otherwise +var CastObject = v1.CastObject + +// RegexMatchDeprecated declares `re_match` which has been deprecated. Use `regex.match` instead. +var RegexMatchDeprecated = v1.RegexMatchDeprecated + +// All takes a list and returns true if all of the items +// are true. A collection of length 0 returns true. +var All = v1.All + +// Any takes a collection and returns true if any of the items +// is true. A collection of length 0 returns false. +var Any = v1.Any + +// Builtin represents a built-in function supported by OPA. Every built-in +// function is uniquely identified by a name. +type Builtin = v1.Builtin diff --git a/ast/capabilities.go b/ast/capabilities.go new file mode 100644 index 0000000000..7c82377ab0 --- /dev/null +++ b/ast/capabilities.go @@ -0,0 +1,57 @@ +// Copyright 2020 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "io" + + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// 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 = v1.VersionIndex + +// 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 = v1.FeatureRefHeadStringPrefixes +const FeatureRefHeads = v1.FeatureRefHeads +const FeatureRegoV1Import = v1.FeatureRegoV1Import + +// Capabilities defines a structure containing data that describes the capabilities +// or features supported by a particular version of OPA. +type Capabilities = v1.Capabilities + +// WasmABIVersion captures the Wasm ABI version. Its `Minor` version is indicating +// backwards-compatible changes. +type WasmABIVersion = v1.WasmABIVersion + +// CapabilitiesForThisVersion returns the capabilities of this version of OPA. +func CapabilitiesForThisVersion() *Capabilities { + return v1.CapabilitiesForThisVersion() +} + +// LoadCapabilitiesJSON loads a JSON serialized capabilities structure from the reader r. +func LoadCapabilitiesJSON(r io.Reader) (*Capabilities, error) { + return v1.LoadCapabilitiesJSON(r) +} + +// LoadCapabilitiesVersion loads a JSON serialized capabilities structure from the specific version. +func LoadCapabilitiesVersion(version string) (*Capabilities, error) { + return v1.LoadCapabilitiesVersion(version) +} + +// LoadCapabilitiesFile loads a JSON serialized capabilities structure from a file. +func LoadCapabilitiesFile(file string) (*Capabilities, error) { + return v1.LoadCapabilitiesFile(file) +} + +// LoadCapabilitiesVersions loads all capabilities versions +func LoadCapabilitiesVersions() ([]string, error) { + return v1.LoadCapabilitiesVersions() +} diff --git a/ast/check.go b/ast/check.go new file mode 100644 index 0000000000..4cf00436df --- /dev/null +++ b/ast/check.go @@ -0,0 +1,22 @@ +// Copyright 2017 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// UnificationErrDetail describes a type mismatch error when two values are +// unified (e.g., x = [1,2,y]). +type UnificationErrDetail = v1.UnificationErrDetail + +// RefErrUnsupportedDetail describes an undefined reference error where the +// referenced value does not support dereferencing (e.g., scalars). +type RefErrUnsupportedDetail = v1.RefErrUnsupportedDetail + +// RefErrInvalidDetail describes an undefined reference error where the referenced +// value does not support the reference operand (e.g., missing object key, +// invalid key type, etc.) +type RefErrInvalidDetail = v1.RefErrInvalidDetail diff --git a/ast/compare.go b/ast/compare.go new file mode 100644 index 0000000000..d36078e338 --- /dev/null +++ b/ast/compare.go @@ -0,0 +1,39 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Compare returns an integer indicating whether two AST values are less than, +// equal to, or greater than each other. +// +// If a is less than b, the return value is negative. If a is greater than b, +// the return value is positive. If a is equal to b, the return value is zero. +// +// Different types are never equal to each other. For comparison purposes, types +// are sorted as follows: +// +// nil < Null < Boolean < Number < String < Var < Ref < Array < Object < Set < +// ArrayComprehension < ObjectComprehension < SetComprehension < Expr < SomeDecl +// < With < Body < Rule < Import < Package < Module. +// +// Arrays and Refs are equal if and only if both a and b have the same length +// and all corresponding elements are equal. If one element is not equal, the +// return value is the same as for the first differing element. If all elements +// are equal but a and b have different lengths, the shorter is considered less +// than the other. +// +// Objects are considered equal if and only if both a and b have the same sorted +// (key, value) pairs and are of the same length. Other comparisons are +// consistent but not defined. +// +// Sets are considered equal if and only if the symmetric difference of a and b +// is empty. +// Other comparisons are consistent but not defined. +func Compare(a, b interface{}) int { + return v1.Compare(a, b) +} diff --git a/ast/compile.go b/ast/compile.go new file mode 100644 index 0000000000..5a3daa910a --- /dev/null +++ b/ast/compile.go @@ -0,0 +1,127 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// CompileErrorLimitDefault is the default number errors a compiler will allow before +// exiting. +const CompileErrorLimitDefault = 10 + +// Compiler contains the state of a compilation process. +type Compiler = v1.Compiler + +// CompilerStage defines the interface for stages in the compiler. +type CompilerStage = v1.CompilerStage + +// CompilerEvalMode allows toggling certain stages that are only +// needed for certain modes, Concretely, only "topdown" mode will +// have the compiler build comprehension and rule indices. +type CompilerEvalMode = v1.CompilerEvalMode + +const ( + // EvalModeTopdown (default) instructs the compiler to build rule + // and comprehension indices used by topdown evaluation. + EvalModeTopdown = v1.EvalModeTopdown + + // EvalModeIR makes the compiler skip the stages for comprehension + // and rule indices. + EvalModeIR = v1.EvalModeIR +) + +// CompilerStageDefinition defines a compiler stage +type CompilerStageDefinition = v1.CompilerStageDefinition + +// RulesOptions defines the options for retrieving rules by Ref from the +// compiler. +type RulesOptions = v1.RulesOptions + +// QueryContext contains contextual information for running an ad-hoc query. +// +// Ad-hoc queries can be run in the context of a package and imports may be +// included to provide concise access to data. +type QueryContext = v1.QueryContext + +// NewQueryContext returns a new QueryContext object. +func NewQueryContext() *QueryContext { + return v1.NewQueryContext() +} + +// QueryCompiler defines the interface for compiling ad-hoc queries. +type QueryCompiler = v1.QueryCompiler + +// QueryCompilerStage defines the interface for stages in the query compiler. +type QueryCompilerStage = v1.QueryCompilerStage + +// QueryCompilerStageDefinition defines a QueryCompiler stage +type QueryCompilerStageDefinition = v1.QueryCompilerStageDefinition + +// NewCompiler returns a new empty compiler. +func NewCompiler() *Compiler { + return v1.NewCompiler().WithDefaultRegoVersion(DefaultRegoVersion) +} + +// ModuleLoader defines the interface that callers can implement to enable lazy +// loading of modules during compilation. +type ModuleLoader = v1.ModuleLoader + +// SafetyCheckVisitorParams defines the AST visitor parameters to use for collecting +// variables during the safety check. This has to be exported because it's relied on +// by the copy propagation implementation in topdown. +var SafetyCheckVisitorParams = v1.SafetyCheckVisitorParams + +// ComprehensionIndex specifies how the comprehension term can be indexed. The keys +// tell the evaluator what variables to use for indexing. In the future, the index +// could be expanded with more information that would allow the evaluator to index +// a larger fragment of comprehensions (e.g., by closing over variables in the outer +// query.) +type ComprehensionIndex = v1.ComprehensionIndex + +// ModuleTreeNode represents a node in the module tree. The module +// tree is keyed by the package path. +type ModuleTreeNode = v1.ModuleTreeNode + +// TreeNode represents a node in the rule tree. The rule tree is keyed by +// rule path. +type TreeNode = v1.TreeNode + +// NewRuleTree returns a new TreeNode that represents the root +// of the rule tree populated with the given rules. +func NewRuleTree(mtree *ModuleTreeNode) *TreeNode { + return v1.NewRuleTree(mtree) +} + +// Graph represents the graph of dependencies between rules. +type Graph = v1.Graph + +// NewGraph returns a new Graph based on modules. The list function must return +// the rules referred to directly by the ref. +func NewGraph(modules map[string]*Module, list func(Ref) []*Rule) *Graph { + return v1.NewGraph(modules, list) +} + +// GraphTraversal is a Traversal that understands the dependency graph +type GraphTraversal = v1.GraphTraversal + +// NewGraphTraversal returns a Traversal for the dependency graph +func NewGraphTraversal(graph *Graph) *GraphTraversal { + return v1.NewGraphTraversal(graph) +} + +// OutputVarsFromBody returns all variables which are the "output" for +// the given body. For safety checks this means that they would be +// made safe by the body. +func OutputVarsFromBody(c *Compiler, body Body, safe VarSet) VarSet { + return v1.OutputVarsFromBody(c, body, safe) +} + +// OutputVarsFromExpr returns all variables which are the "output" for +// the given expression. For safety checks this means that they would be +// made safe by the expr. +func OutputVarsFromExpr(c *Compiler, expr *Expr, safe VarSet) VarSet { + return v1.OutputVarsFromExpr(c, expr, safe) +} diff --git a/ast/compile_test.go b/ast/compile_test.go new file mode 100644 index 0000000000..a2064c57fc --- /dev/null +++ b/ast/compile_test.go @@ -0,0 +1,99 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "strings" + "testing" +) + +func TestCompile_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]*Module + expErrs []string + }{ + { + note: "no module rego-version, no v1 violations", + modules: map[string]*Module{ + "test": { + Package: MustParsePackage(`package test`), + Imports: MustParseImports(`import data.foo + import data.bar`), + }, + }, + }, + { + note: "no module rego-version, v1 violations", // default is v0, no errors expected + modules: map[string]*Module{ + "test": { + Package: MustParsePackage(`package test`), + Imports: MustParseImports(`import data.foo + import data.bar as foo`), + }, + }, + }, + { + note: "v0 module, v1 violations", + modules: map[string]*Module{ + "test": MustParseModuleWithOpts(`package test + import data.foo + import data.bar as foo`, + ParserOptions{RegoVersion: RegoV0}), + }, + }, + { + note: "v1 module, v1 violations", + modules: map[string]*Module{ + "test": MustParseModuleWithOpts(`package test + import data.foo + import data.bar as foo`, + ParserOptions{RegoVersion: RegoV1}), + }, + expErrs: []string{ + "3:7: rego_compile_error: import must not shadow import data.foo", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + compiler := NewCompiler() + + compiler.Compile(tc.modules) + + if len(tc.expErrs) > 0 { + assertErrors(t, compiler.Errors, tc.expErrs) + } else { + if len(compiler.Errors) > 0 { + t.Fatalf("Unexpected errors: %v", compiler.Errors) + } + } + }) + } +} + +func assertErrors(t *testing.T, actual Errors, expected []string) { + t.Helper() + if len(expected) != len(actual) { + t.Fatalf("Expected %d errors, got %d:\n\n%s\n", len(expected), len(actual), actual.Error()) + } + incorrectErrs := false + for _, e := range expected { + found := false + for _, actual := range actual { + if strings.Contains(actual.Error(), e) { + found = true + break + } + } + if !found { + incorrectErrs = true + } + } + if incorrectErrs { + t.Fatalf("Expected errors:\n\n%s\n\nGot:\n\n%s\n", expected, actual.Error()) + } +} diff --git a/ast/compilehelper.go b/ast/compilehelper.go new file mode 100644 index 0000000000..37ede329ea --- /dev/null +++ b/ast/compilehelper.go @@ -0,0 +1,48 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import v1 "github.com/open-policy-agent/opa/v1/ast" + +// CompileModules takes a set of Rego modules represented as strings and +// compiles them for evaluation. The keys of the map are used as filenames. +func CompileModules(modules map[string]string) (*Compiler, error) { + return CompileModulesWithOpt(modules, CompileOpts{ + ParserOptions: ParserOptions{ + RegoVersion: DefaultRegoVersion, + }, + }) +} + +// CompileOpts defines a set of options for the compiler. +type CompileOpts = v1.CompileOpts + +// CompileModulesWithOpt takes a set of Rego modules represented as strings and +// compiles them for evaluation. The keys of the map are used as filenames. +func CompileModulesWithOpt(modules map[string]string, opts CompileOpts) (*Compiler, error) { + if opts.ParserOptions.RegoVersion == RegoUndefined { + opts.ParserOptions.RegoVersion = DefaultRegoVersion + } + + return v1.CompileModulesWithOpt(modules, opts) +} + +// MustCompileModules compiles a set of Rego modules represented as strings. If +// the compilation process fails, this function panics. +func MustCompileModules(modules map[string]string) *Compiler { + return MustCompileModulesWithOpts(modules, CompileOpts{}) +} + +// MustCompileModulesWithOpts compiles a set of Rego modules represented as strings. If +// the compilation process fails, this function panics. +func MustCompileModulesWithOpts(modules map[string]string, opts CompileOpts) *Compiler { + + compiler, err := CompileModulesWithOpt(modules, opts) + if err != nil { + panic(err) + } + + return compiler +} diff --git a/ast/compilehelper_test.go b/ast/compilehelper_test.go new file mode 100644 index 0000000000..4ffc4f213a --- /dev/null +++ b/ast/compilehelper_test.go @@ -0,0 +1,226 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "strings" + "testing" +) + +func TestCompileModules_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]string + expErrs []string + }{ + // default rego-version + { + note: "v0 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p[x] { + x = "a" + }`, + }, + }, + { + note: "v0 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p[x] { + x = "a" + }`, + }, + }, + + // cross-rego-version + { + note: "rego.v1 import, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + p contains x if { + x = "a" + }`, + }, + }, + { + note: "rego.v1 import, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_compile_error: import must not shadow import data.foo", + }, + }, + + // NOT default rego-version + { + note: "v1 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:2: rego_parse_error: var cannot be used for rule name", + }, + }, + { + note: "v1 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + _, err := CompileModules(tc.modules) + + if len(tc.expErrs) > 0 { + for _, expErr := range tc.expErrs { + if err := err.Error(); !strings.Contains(err, expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestCompileModulesWithOpt_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]string + expErrs []string + }{ + // default rego-version + { + note: "v0 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p[x] { + x = "a" + }`, + }, + }, + { + note: "v0 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p[x] { + x = "a" + }`, + }, + }, + + // cross-rego-version + { + note: "rego.v1 import, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + p contains x if { + x = "a" + }`, + }, + }, + { + note: "rego.v1 import, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_compile_error: import must not shadow import data.foo", + }, + }, + + // NOT default rego-version + { + note: "v1 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:2: rego_parse_error: var cannot be used for rule name", + }, + }, + { + note: "v1 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + _, err := CompileModulesWithOpt(tc.modules, CompileOpts{EnablePrintStatements: true}) + + if len(tc.expErrs) > 0 { + for _, expErr := range tc.expErrs { + if err := err.Error(); !strings.Contains(err, expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} diff --git a/ast/conflicts.go b/ast/conflicts.go new file mode 100644 index 0000000000..10edce382c --- /dev/null +++ b/ast/conflicts.go @@ -0,0 +1,15 @@ +// 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 ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// CheckPathConflicts returns a set of errors indicating paths that +// are in conflict with the result of the provided callable. +func CheckPathConflicts(c *Compiler, exists func([]string) (bool, error)) Errors { + return v1.CheckPathConflicts(c, exists) +} diff --git a/ast/doc.go b/ast/doc.go new file mode 100644 index 0000000000..ba974e5ba6 --- /dev/null +++ b/ast/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package ast diff --git a/ast/env.go b/ast/env.go new file mode 100644 index 0000000000..ef0ccf89ce --- /dev/null +++ b/ast/env.go @@ -0,0 +1,12 @@ +// Copyright 2017 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// TypeEnv contains type info for static analysis such as type checking. +type TypeEnv = v1.TypeEnv diff --git a/ast/errors.go b/ast/errors.go new file mode 100644 index 0000000000..0cb8ee28f7 --- /dev/null +++ b/ast/errors.go @@ -0,0 +1,46 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Errors represents a series of errors encountered during parsing, compiling, +// etc. +type Errors = v1.Errors + +const ( + // ParseErr indicates an unclassified parse error occurred. + ParseErr = v1.ParseErr + + // CompileErr indicates an unclassified compile error occurred. + CompileErr = v1.CompileErr + + // TypeErr indicates a type error was caught. + TypeErr = v1.TypeErr + + // UnsafeVarErr indicates an unsafe variable was found during compilation. + UnsafeVarErr = v1.UnsafeVarErr + + // RecursionErr indicates recursion was found during compilation. + RecursionErr = v1.RecursionErr +) + +// IsError returns true if err is an AST error with code. +func IsError(code string, err error) bool { + return v1.IsError(code, err) +} + +// ErrorDetails defines the interface for detailed error messages. +type ErrorDetails = v1.ErrorDetails + +// Error represents a single error caught during parsing, compiling, etc. +type Error = v1.Error + +// NewError returns a new Error object. +func NewError(code string, loc *Location, f string, a ...interface{}) *Error { + return v1.NewError(code, loc, f, a...) +} diff --git a/ast/index.go b/ast/index.go new file mode 100644 index 0000000000..7e80bb7716 --- /dev/null +++ b/ast/index.go @@ -0,0 +1,20 @@ +// Copyright 2017 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// RuleIndex defines the interface for rule indices. +type RuleIndex v1.RuleIndex + +// IndexResult contains the result of an index lookup. +type IndexResult = v1.IndexResult + +// NewIndexResult returns a new IndexResult object. +func NewIndexResult(kind RuleKind) *IndexResult { + return v1.NewIndexResult(kind) +} diff --git a/ast/interning.go b/ast/interning.go new file mode 100644 index 0000000000..239293664b --- /dev/null +++ b/ast/interning.go @@ -0,0 +1,24 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +func InternedBooleanTerm(b bool) *Term { + return v1.InternedBooleanTerm(b) +} + +// InternedIntNumberTerm returns a term with the given integer value. The term is +// cached between -1 to 512, and for values outside of that range, this function +// is equivalent to ast.IntNumberTerm. +func InternedIntNumberTerm(i int) *Term { + return v1.InternedIntNumberTerm(i) +} + +func HasInternedIntNumberTerm(i int) bool { + return v1.HasInternedIntNumberTerm(i) +} diff --git a/ast/json/doc.go b/ast/json/doc.go new file mode 100644 index 0000000000..26aee9b994 --- /dev/null +++ b/ast/json/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package json diff --git a/ast/json/json.go b/ast/json/json.go new file mode 100644 index 0000000000..8a3a36bb9b --- /dev/null +++ b/ast/json/json.go @@ -0,0 +1,15 @@ +package json + +import v1 "github.com/open-policy-agent/opa/v1/ast/json" + +// Options defines the options for JSON operations, +// currently only marshaling can be configured +type Options = v1.Options + +// MarshalOptions defines the options for JSON marshaling, +// currently only toggling the marshaling of location information is supported +type MarshalOptions = v1.MarshalOptions + +// NodeToggle is a generic struct to allow the toggling of +// settings for different ast node types +type NodeToggle = v1.NodeToggle diff --git a/ast/location/doc.go b/ast/location/doc.go new file mode 100644 index 0000000000..b559963a52 --- /dev/null +++ b/ast/location/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package location diff --git a/ast/location/location.go b/ast/location/location.go new file mode 100644 index 0000000000..f746bb93b3 --- /dev/null +++ b/ast/location/location.go @@ -0,0 +1,14 @@ +// Package location defines locations in Rego source code. +package location + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Location records a position in source code +type Location = v1.Location + +// NewLocation returns a new Location object. +func NewLocation(text []byte, file string, row int, col int) *Location { + return v1.NewLocation(text, file, row, col) +} diff --git a/ast/map.go b/ast/map.go new file mode 100644 index 0000000000..070ad3e5de --- /dev/null +++ b/ast/map.go @@ -0,0 +1,18 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// ValueMap represents a key/value map between AST term values. Any type of term +// can be used as a key in the map. +type ValueMap = v1.ValueMap + +// NewValueMap returns a new ValueMap. +func NewValueMap() *ValueMap { + return v1.NewValueMap() +} diff --git a/ast/parser.go b/ast/parser.go new file mode 100644 index 0000000000..8954618a05 --- /dev/null +++ b/ast/parser.go @@ -0,0 +1,45 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +var RegoV1CompatibleRef = v1.RegoV1CompatibleRef + +// RegoVersion defines the Rego syntax requirements for a module. +type RegoVersion = v1.RegoVersion + +const DefaultRegoVersion = RegoV0 + +const ( + RegoUndefined = v1.RegoUndefined + // RegoV0 is the default, original Rego syntax. + RegoV0 = v1.RegoV0 + // RegoV0CompatV1 requires modules to comply with both the RegoV0 and RegoV1 syntax (as when 'rego.v1' is imported in a module). + // Shortly, RegoV1 compatibility is required, but 'rego.v1' or 'future.keywords' must also be imported. + RegoV0CompatV1 = v1.RegoV0CompatV1 + // RegoV1 is the Rego syntax enforced by OPA 1.0; e.g.: + // future.keywords part of default keyword set, and don't require imports; + // 'if' and 'contains' required in rule heads; + // (some) strict checks on by default. + RegoV1 = v1.RegoV1 +) + +func RegoVersionFromInt(i int) RegoVersion { + return v1.RegoVersionFromInt(i) +} + +// Parser is used to parse Rego statements. +type Parser = v1.Parser + +// ParserOptions defines the options for parsing Rego statements. +type ParserOptions = v1.ParserOptions + +// NewParser creates and initializes a Parser. +func NewParser() *Parser { + return v1.NewParser().WithRegoVersion(DefaultRegoVersion) +} diff --git a/ast/parser_ext.go b/ast/parser_ext.go new file mode 100644 index 0000000000..3b8b406825 --- /dev/null +++ b/ast/parser_ext.go @@ -0,0 +1,310 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "fmt" + + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// MustParseBody returns a parsed body. +// If an error occurs during parsing, panic. +func MustParseBody(input string) Body { + return MustParseBodyWithOpts(input, ParserOptions{}) +} + +// MustParseBodyWithOpts returns a parsed body. +// If an error occurs during parsing, panic. +func MustParseBodyWithOpts(input string, opts ParserOptions) Body { + return v1.MustParseBodyWithOpts(input, setDefaultRegoVersion(opts)) +} + +// MustParseExpr returns a parsed expression. +// If an error occurs during parsing, panic. +func MustParseExpr(input string) *Expr { + parsed, err := ParseExpr(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseImports returns a slice of imports. +// If an error occurs during parsing, panic. +func MustParseImports(input string) []*Import { + parsed, err := ParseImports(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseModule returns a parsed module. +// If an error occurs during parsing, panic. +func MustParseModule(input string) *Module { + return MustParseModuleWithOpts(input, ParserOptions{}) +} + +// MustParseModuleWithOpts returns a parsed module. +// If an error occurs during parsing, panic. +func MustParseModuleWithOpts(input string, opts ParserOptions) *Module { + return v1.MustParseModuleWithOpts(input, setDefaultRegoVersion(opts)) +} + +// MustParsePackage returns a Package. +// If an error occurs during parsing, panic. +func MustParsePackage(input string) *Package { + parsed, err := ParsePackage(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseStatements returns a slice of parsed statements. +// If an error occurs during parsing, panic. +func MustParseStatements(input string) []Statement { + parsed, _, err := ParseStatements("", input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseStatement returns exactly one statement. +// If an error occurs during parsing, panic. +func MustParseStatement(input string) Statement { + parsed, err := ParseStatement(input) + if err != nil { + panic(err) + } + return parsed +} + +func MustParseStatementWithOpts(input string, popts ParserOptions) Statement { + return v1.MustParseStatementWithOpts(input, setDefaultRegoVersion(popts)) +} + +// MustParseRef returns a parsed reference. +// If an error occurs during parsing, panic. +func MustParseRef(input string) Ref { + parsed, err := ParseRef(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseRule returns a parsed rule. +// If an error occurs during parsing, panic. +func MustParseRule(input string) *Rule { + parsed, err := ParseRule(input) + if err != nil { + panic(err) + } + return parsed +} + +// MustParseRuleWithOpts returns a parsed rule. +// If an error occurs during parsing, panic. +func MustParseRuleWithOpts(input string, opts ParserOptions) *Rule { + return v1.MustParseRuleWithOpts(input, setDefaultRegoVersion(opts)) +} + +// MustParseTerm returns a parsed term. +// If an error occurs during parsing, panic. +func MustParseTerm(input string) *Term { + parsed, err := ParseTerm(input) + if err != nil { + panic(err) + } + return parsed +} + +// ParseRuleFromBody returns a rule if the body can be interpreted as a rule +// definition. Otherwise, an error is returned. +func ParseRuleFromBody(module *Module, body Body) (*Rule, error) { + return v1.ParseRuleFromBody(module, body) +} + +// ParseRuleFromExpr returns a rule if the expression can be interpreted as a +// rule definition. +func ParseRuleFromExpr(module *Module, expr *Expr) (*Rule, error) { + return v1.ParseRuleFromExpr(module, expr) +} + +// ParseCompleteDocRuleFromAssignmentExpr returns a rule if the expression can +// be interpreted as a complete document definition declared with the assignment +// operator. +func ParseCompleteDocRuleFromAssignmentExpr(module *Module, lhs, rhs *Term) (*Rule, error) { + return v1.ParseCompleteDocRuleFromAssignmentExpr(module, lhs, rhs) +} + +// ParseCompleteDocRuleFromEqExpr returns a rule if the expression can be +// interpreted as a complete document definition. +func ParseCompleteDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, error) { + return v1.ParseCompleteDocRuleFromEqExpr(module, lhs, rhs) +} + +func ParseCompleteDocRuleWithDotsFromTerm(module *Module, term *Term) (*Rule, error) { + return v1.ParseCompleteDocRuleWithDotsFromTerm(module, term) +} + +// ParsePartialObjectDocRuleFromEqExpr returns a rule if the expression can be +// interpreted as a partial object document definition. +func ParsePartialObjectDocRuleFromEqExpr(module *Module, lhs, rhs *Term) (*Rule, error) { + return v1.ParsePartialObjectDocRuleFromEqExpr(module, lhs, rhs) +} + +// ParsePartialSetDocRuleFromTerm returns a rule if the term can be interpreted +// as a partial set document definition. +func ParsePartialSetDocRuleFromTerm(module *Module, term *Term) (*Rule, error) { + return v1.ParsePartialSetDocRuleFromTerm(module, term) +} + +// ParseRuleFromCallEqExpr returns a rule if the term can be interpreted as a +// function definition (e.g., f(x) = y => f(x) = y { true }). +func ParseRuleFromCallEqExpr(module *Module, lhs, rhs *Term) (*Rule, error) { + return v1.ParseRuleFromCallEqExpr(module, lhs, rhs) +} + +// ParseRuleFromCallExpr returns a rule if the terms can be interpreted as a +// function returning true or some value (e.g., f(x) => f(x) = true { true }). +func ParseRuleFromCallExpr(module *Module, terms []*Term) (*Rule, error) { + return v1.ParseRuleFromCallExpr(module, terms) +} + +// ParseImports returns a slice of Import objects. +func ParseImports(input string) ([]*Import, error) { + return v1.ParseImports(input) +} + +// ParseModule returns a parsed Module object. +// For details on Module objects and their fields, see policy.go. +// Empty input will return nil, nil. +func ParseModule(filename, input string) (*Module, error) { + return ParseModuleWithOpts(filename, input, ParserOptions{}) +} + +// ParseModuleWithOpts returns a parsed Module object, and has an additional input ParserOptions +// For details on Module objects and their fields, see policy.go. +// Empty input will return nil, nil. +func ParseModuleWithOpts(filename, input string, popts ParserOptions) (*Module, error) { + return v1.ParseModuleWithOpts(filename, input, setDefaultRegoVersion(popts)) +} + +// ParseBody returns exactly one body. +// If multiple bodies are parsed, an error is returned. +func ParseBody(input string) (Body, error) { + return ParseBodyWithOpts(input, ParserOptions{SkipRules: true}) +} + +// ParseBodyWithOpts returns exactly one body. It does _not_ set SkipRules: true on its own, +// but respects whatever ParserOptions it's been given. +func ParseBodyWithOpts(input string, popts ParserOptions) (Body, error) { + return v1.ParseBodyWithOpts(input, setDefaultRegoVersion(popts)) +} + +// ParseExpr returns exactly one expression. +// If multiple expressions are parsed, an error is returned. +func ParseExpr(input string) (*Expr, error) { + body, err := ParseBody(input) + if err != nil { + return nil, fmt.Errorf("failed to parse expression: %w", err) + } + if len(body) != 1 { + return nil, fmt.Errorf("expected exactly one expression but got: %v", body) + } + return body[0], nil +} + +// ParsePackage returns exactly one Package. +// If multiple statements are parsed, an error is returned. +func ParsePackage(input string) (*Package, error) { + return v1.ParsePackage(input) +} + +// ParseTerm returns exactly one term. +// If multiple terms are parsed, an error is returned. +func ParseTerm(input string) (*Term, error) { + body, err := ParseBody(input) + if err != nil { + return nil, fmt.Errorf("failed to parse term: %w", err) + } + if len(body) != 1 { + return nil, fmt.Errorf("expected exactly one term but got: %v", body) + } + term, ok := body[0].Terms.(*Term) + if !ok { + return nil, fmt.Errorf("expected term but got %v", body[0].Terms) + } + return term, nil +} + +// ParseRef returns exactly one reference. +func ParseRef(input string) (Ref, error) { + term, err := ParseTerm(input) + if err != nil { + return nil, fmt.Errorf("failed to parse ref: %w", err) + } + ref, ok := term.Value.(Ref) + if !ok { + return nil, fmt.Errorf("expected ref but got %v", term) + } + return ref, nil +} + +// ParseRuleWithOpts returns exactly one rule. +// If multiple rules are parsed, an error is returned. +func ParseRuleWithOpts(input string, opts ParserOptions) (*Rule, error) { + return v1.ParseRuleWithOpts(input, setDefaultRegoVersion(opts)) +} + +// ParseRule returns exactly one rule. +// If multiple rules are parsed, an error is returned. +func ParseRule(input string) (*Rule, error) { + return ParseRuleWithOpts(input, ParserOptions{}) +} + +// ParseStatement returns exactly one statement. +// A statement might be a term, expression, rule, etc. Regardless, +// this function expects *exactly* one statement. If multiple +// statements are parsed, an error is returned. +func ParseStatement(input string) (Statement, error) { + stmts, _, err := ParseStatements("", input) + if err != nil { + return nil, err + } + if len(stmts) != 1 { + return nil, fmt.Errorf("expected exactly one statement") + } + return stmts[0], nil +} + +func ParseStatementWithOpts(input string, popts ParserOptions) (Statement, error) { + return v1.ParseStatementWithOpts(input, setDefaultRegoVersion(popts)) +} + +// ParseStatements is deprecated. Use ParseStatementWithOpts instead. +func ParseStatements(filename, input string) ([]Statement, []*Comment, error) { + return ParseStatementsWithOpts(filename, input, ParserOptions{}) +} + +// ParseStatementsWithOpts returns a slice of parsed statements. This is the +// default return value from the parser. +func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Statement, []*Comment, error) { + return v1.ParseStatementsWithOpts(filename, input, setDefaultRegoVersion(popts)) +} + +// ParserErrorDetail holds additional details for parser errors. +type ParserErrorDetail = v1.ParserErrorDetail + +func setDefaultRegoVersion(opts ParserOptions) ParserOptions { + if opts.RegoVersion == RegoUndefined { + opts.RegoVersion = DefaultRegoVersion + } + return opts +} diff --git a/ast/parser_ext_test.go b/ast/parser_ext_test.go new file mode 100644 index 0000000000..02ca80eefc --- /dev/null +++ b/ast/parser_ext_test.go @@ -0,0 +1,127 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast_test + +import ( + "strings" + "testing" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/format" +) + +func TestParseModule_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + mod string + expRules []string + expErrs []string + }{ + { + note: "v0", // default rego-version + mod: `package test +p[x] { + x = "a" +}`, + expRules: []string{"p"}, + }, + { + note: "import rego.v1", + mod: `package test +import rego.v1 + +p contains x if { + x = "a" +}`, + expRules: []string{"p"}, + }, + { + note: "v1", // NOT default rego-version + mod: `package test +p contains x if { + x = "a" +}`, + expErrs: []string{ + "test.rego:2: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + m, err := ast.ParseModule("test.rego", tc.mod) + + if len(tc.expErrs) > 0 { + for i, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error %d to contain %q, got %q", i, expErr, err.Error()) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(m.Rules) != len(tc.expRules) { + t.Fatalf("Expected %d rules, got %d", len(tc.expRules), len(m.Rules)) + } + for i, r := range m.Rules { + if r.Head.Name.String() != tc.expRules[i] { + t.Fatalf("Expected rule %q, got %q", tc.expRules[i], r.Head.Name.String()) + } + } + } + }) + } +} + +func TestParseBody_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + body string + expStmts int + assertSame bool + }{ + { + note: "v0", // default rego-version + body: `x := ["a", "b", "c"][i] +`, + expStmts: 1, + assertSame: true, + }, + { + note: "v1", // NOT default rego-version + body: `some x, i in ["a", "b", "c"] +`, + expStmts: 3, + assertSame: false, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + body, err := ast.ParseBody(tc.body) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(body) != tc.expStmts { + t.Fatalf("Expected %d statements, got %d:%q\n\n", tc.expStmts, len(body), body) + } + + if tc.assertSame { + formatted, err := format.AstWithOpts(body, format.Opts{RegoVersion: ast.RegoV1}) // every body is v1-compatible + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if strings.Compare(string(formatted), tc.body) != 0 { + t.Fatalf("Expected body to be %q, got %q", tc.body, string(formatted)) + } + } + }) + } +} diff --git a/ast/parser_test.go b/ast/parser_test.go new file mode 100644 index 0000000000..c494c536a7 --- /dev/null +++ b/ast/parser_test.go @@ -0,0 +1,52 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "bytes" + "testing" +) + +func TestParser_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + input string + expStmtCount int + }{ + { + note: "v0", + input: `package test +p[x] { + c = ["a", "b", "c"][i] +}`, + expStmtCount: 2, //package, p + }, + { + note: "v1", + input: `package test +p contains x if { + c = ["a", "b", "c"][i] +}`, + // v1 Keywords are not recognized, and interpreted as individual statements + expStmtCount: 5, //package, p, contains, x, if + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + parser := NewParser(). + WithFilename("test.rego"). + WithReader(bytes.NewBufferString(tc.input)) + stmts, _, err := parser.Parse() + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(stmts) != tc.expStmtCount { + t.Fatalf("Expected %d statements but got %d:\n\n%v", tc.expStmtCount, len(stmts), stmts) + } + }) + } +} diff --git a/ast/policy.go b/ast/policy.go new file mode 100644 index 0000000000..a29f0dcc75 --- /dev/null +++ b/ast/policy.go @@ -0,0 +1,235 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + astJSON "github.com/open-policy-agent/opa/ast/json" + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// DefaultRootDocument is the default root document. +// +// All package directives inside source files are implicitly prefixed with the +// DefaultRootDocument value. +var DefaultRootDocument = v1.DefaultRootDocument + +// InputRootDocument names the document containing query arguments. +var InputRootDocument = v1.InputRootDocument + +// SchemaRootDocument names the document containing external data schemas. +var SchemaRootDocument = v1.SchemaRootDocument + +// FunctionArgRootDocument names the document containing function arguments. +// It's only for internal usage, for referencing function arguments between +// the index and topdown. +var FunctionArgRootDocument = v1.FunctionArgRootDocument + +// FutureRootDocument names the document containing new, to-become-default, +// features. +var FutureRootDocument = v1.FutureRootDocument + +// RegoRootDocument names the document containing new, to-become-default, +// features in a future versioned release. +var RegoRootDocument = v1.RegoRootDocument + +// RootDocumentNames contains the names of top-level documents that can be +// referred to in modules and queries. +// +// Note, the schema document is not currently implemented in the evaluator so it +// is not registered as a root document name (yet). +var RootDocumentNames = v1.RootDocumentNames + +// DefaultRootRef is a reference to the root of the default document. +// +// All refs to data in the policy engine's storage layer are prefixed with this ref. +var DefaultRootRef = v1.DefaultRootRef + +// InputRootRef is a reference to the root of the input document. +// +// All refs to query arguments are prefixed with this ref. +var InputRootRef = v1.InputRootRef + +// SchemaRootRef is a reference to the root of the schema document. +// +// All refs to schema documents are prefixed with this ref. Note, the schema +// document is not currently implemented in the evaluator so it is not +// registered as a root document ref (yet). +var SchemaRootRef = v1.SchemaRootRef + +// RootDocumentRefs contains the prefixes of top-level documents that all +// non-local references start with. +var RootDocumentRefs = v1.RootDocumentRefs + +// SystemDocumentKey is the name of the top-level key that identifies the system +// document. +const SystemDocumentKey = v1.SystemDocumentKey + +// ReservedVars is the set of names that refer to implicitly ground vars. +var ReservedVars = v1.ReservedVars + +// Wildcard represents the wildcard variable as defined in the language. +var Wildcard = v1.Wildcard + +// WildcardPrefix is the special character that all wildcard variables are +// prefixed with when the statement they are contained in is parsed. +const WildcardPrefix = v1.WildcardPrefix + +// Keywords contains strings that map to language keywords. +var Keywords = v1.Keywords + +var KeywordsV0 = v1.KeywordsV0 + +var KeywordsV1 = v1.KeywordsV1 + +func KeywordsForRegoVersion(v RegoVersion) []string { + return v1.KeywordsForRegoVersion(v) +} + +// IsKeyword returns true if s is a language keyword. +func IsKeyword(s string) bool { + return v1.IsKeyword(s) +} + +func IsInKeywords(s string, keywords []string) bool { + return v1.IsInKeywords(s, keywords) +} + +// IsKeywordInRegoVersion returns true if s is a language keyword. +func IsKeywordInRegoVersion(s string, regoVersion RegoVersion) bool { + return v1.IsKeywordInRegoVersion(s, regoVersion) +} + +type ( + // Node represents a node in an AST. Nodes may be statements in a policy module + // or elements of an ad-hoc query, expression, etc. + Node = v1.Node + + // Statement represents a single statement in a policy module. + Statement = v1.Statement +) + +type ( + + // Module represents a collection of policies (defined by rules) + // within a namespace (defined by the package) and optional + // dependencies on external documents (defined by imports). + Module = v1.Module + + // Comment contains the raw text from the comment in the definition. + Comment = v1.Comment + + // Package represents the namespace of the documents produced + // by rules inside the module. + Package = v1.Package + + // Import represents a dependency on a document outside of the policy + // namespace. Imports are optional. + Import = v1.Import + + // Rule represents a rule as defined in the language. Rules define the + // content of documents that represent policy decisions. + Rule = v1.Rule + + // Head represents the head of a rule. + Head = v1.Head + + // Args represents zero or more arguments to a rule. + Args = v1.Args + + // Body represents one or more expressions contained inside a rule or user + // function. + Body = v1.Body + + // Expr represents a single expression contained inside the body of a rule. + Expr = v1.Expr + + // SomeDecl represents a variable declaration statement. The symbols are variables. + SomeDecl = v1.SomeDecl + + Every = v1.Every + + // With represents a modifier on an expression. + With = v1.With +) + +// NewComment returns a new Comment object. +func NewComment(text []byte) *Comment { + return v1.NewComment(text) +} + +// IsValidImportPath returns an error indicating if the import path is invalid. +// If the import path is valid, err is nil. +func IsValidImportPath(v Value) (err error) { + return v1.IsValidImportPath(v) +} + +// NewHead returns a new Head object. If args are provided, the first will be +// used for the key and the second will be used for the value. +func NewHead(name Var, args ...*Term) *Head { + return v1.NewHead(name, args...) +} + +// 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 { + return v1.VarHead(name, location, jsonOpts) +} + +// 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 { + return v1.RefHead(ref, args...) +} + +// DocKind represents the collection of document types that can be produced by rules. +type DocKind int + +const ( + // CompleteDoc represents a document that is completely defined by the rule. + CompleteDoc = v1.CompleteDoc + + // PartialSetDoc represents a set document that is partially defined by the rule. + PartialSetDoc = v1.PartialSetDoc + + // PartialObjectDoc represents an object document that is partially defined by the rule. + PartialObjectDoc = v1.PartialObjectDoc +) + +type RuleKind = v1.RuleKind + +const ( + SingleValue = v1.SingleValue + MultiValue = v1.MultiValue +) + +// NewBody returns a new Body containing the given expressions. The indices of +// the immediate expressions will be reset. +func NewBody(exprs ...*Expr) Body { + return v1.NewBody(exprs...) +} + +// NewExpr returns a new Expr object. +func NewExpr(terms interface{}) *Expr { + return v1.NewExpr(terms) +} + +// NewBuiltinExpr creates a new Expr object with the supplied terms. +// The builtin operator must be the first term. +func NewBuiltinExpr(terms ...*Term) *Expr { + return v1.NewBuiltinExpr(terms...) +} + +// Copy returns a deep copy of the AST node x. If x is not an AST node, x is returned unmodified. +func Copy(x interface{}) interface{} { + return v1.Copy(x) +} + +// RuleSet represents a collection of rules that produce a virtual document. +type RuleSet = v1.RuleSet + +// NewRuleSet returns a new RuleSet containing the given rules. +func NewRuleSet(rules ...*Rule) RuleSet { + return v1.NewRuleSet(rules...) +} diff --git a/ast/policy_test.go b/ast/policy_test.go new file mode 100644 index 0000000000..3e35ba7695 --- /dev/null +++ b/ast/policy_test.go @@ -0,0 +1,85 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import "testing" + +func TestRuleString_DefaultRegoVersion(t *testing.T) { + // ast.Rule.String() will respect the rego-version of the ast.Module it is part of. + + tests := []struct { + note string + module string + regoVersion RegoVersion + exp string + }{ + { + note: "v0", + regoVersion: RegoV0, + module: `package a.b.c + +p[x] { x = "a" }`, + exp: `p[x] { x = "a" }`, + }, + { + note: "v1", + regoVersion: RegoV1, + module: `package a.b.c + +p contains x if { x = "a" }`, + exp: `p contains x if { x = "a" }`, + }, + { + note: "default rego-version", + module: `package a.b.c + +p[x] { x = "a" }`, + exp: `p[x] { x = "a" }`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + var mod *Module + + if tc.regoVersion == RegoUndefined { + mod = MustParseModule(tc.module) + } else { + mod = MustParseModuleWithOpts(tc.module, ParserOptions{RegoVersion: tc.regoVersion}) + } + + rule := mod.Rules[0] + act := rule.String() + + if act != tc.exp { + t.Fatalf("Expected:\n\n%s\n\nbut got:\n\n%s", tc.exp, act) + } + }) + } +} + +func TestModuleString(t *testing.T) { + + // v0 module + input := `package a.b.c + +import data.foo.bar +import input.xyz + +p = true { not bar } +q = true { xyz.abc = 2 } +wildcard = true { bar[_] = 1 }` + + mod := MustParseModule(input) + + roundtrip, err := ParseModule("", mod.String()) + if err != nil { + t.Fatalf("Unexpected error while parsing roundtripped module: %v", err) + } + + if !roundtrip.Equal(mod) { + t.Fatalf("Expected roundtripped to equal original but:\n\nExpected:\n\n%v\n\nDoes not equal result:\n\n%v", mod, roundtrip) + } +} diff --git a/ast/pretty.go b/ast/pretty.go new file mode 100644 index 0000000000..f2b8104e0a --- /dev/null +++ b/ast/pretty.go @@ -0,0 +1,18 @@ +// 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 ast + +import ( + "io" + + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Pretty writes a pretty representation of the AST rooted at x to w. +// +// This is function is intended for debug purposes when inspecting ASTs. +func Pretty(w io.Writer, x interface{}) { + v1.Pretty(w, x) +} diff --git a/ast/schema.go b/ast/schema.go new file mode 100644 index 0000000000..979958a3c0 --- /dev/null +++ b/ast/schema.go @@ -0,0 +1,17 @@ +// Copyright 2021 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// SchemaSet holds a map from a path to a schema. +type SchemaSet = v1.SchemaSet + +// NewSchemaSet returns an empty SchemaSet. +func NewSchemaSet() *SchemaSet { + return v1.NewSchemaSet() +} diff --git a/ast/strings.go b/ast/strings.go new file mode 100644 index 0000000000..ef9354bf78 --- /dev/null +++ b/ast/strings.go @@ -0,0 +1,14 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// TypeName returns a human readable name for the AST element type. +func TypeName(x interface{}) string { + return v1.TypeName(x) +} diff --git a/ast/term.go b/ast/term.go new file mode 100644 index 0000000000..a5d146ea27 --- /dev/null +++ b/ast/term.go @@ -0,0 +1,306 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "encoding/json" + "io" + + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Location records a position in source code. +type Location = v1.Location + +// NewLocation returns a new Location object. +func NewLocation(text []byte, file string, row int, col int) *Location { + return v1.NewLocation(text, file, row, col) +} + +// Value declares the common interface for all Term values. Every kind of Term value +// in the language is represented as a type that implements this interface: +// +// - Null, Boolean, Number, String +// - Object, Array, Set +// - Variables, References +// - Array, Set, and Object Comprehensions +// - Calls +type Value = v1.Value + +// InterfaceToValue converts a native Go value x to a Value. +func InterfaceToValue(x interface{}) (Value, error) { + return v1.InterfaceToValue(x) +} + +// ValueFromReader returns an AST value from a JSON serialized value in the reader. +func ValueFromReader(r io.Reader) (Value, error) { + return v1.ValueFromReader(r) +} + +// As converts v into a Go native type referred to by x. +func As(v Value, x interface{}) error { + return v1.As(v, x) +} + +// Resolver defines the interface for resolving references to native Go values. +type Resolver = v1.Resolver + +// ValueResolver defines the interface for resolving references to AST values. +type ValueResolver = v1.ValueResolver + +// UnknownValueErr indicates a ValueResolver was unable to resolve a reference +// because the reference refers to an unknown value. +type UnknownValueErr = v1.UnknownValueErr + +// IsUnknownValueErr returns true if the err is an UnknownValueErr. +func IsUnknownValueErr(err error) bool { + return v1.IsUnknownValueErr(err) +} + +// ValueToInterface returns the Go representation of an AST value. The AST +// value should not contain any values that require evaluation (e.g., vars, +// comprehensions, etc.) +func ValueToInterface(v Value, resolver Resolver) (interface{}, error) { + return v1.ValueToInterface(v, resolver) +} + +// JSON returns the JSON representation of v. The value must not contain any +// refs or terms that require evaluation (e.g., vars, comprehensions, etc.) +func JSON(v Value) (interface{}, error) { + return v1.JSON(v) +} + +// JSONOpt defines parameters for AST to JSON conversion. +type JSONOpt = v1.JSONOpt + +// JSONWithOpt returns the JSON representation of v. The value must not contain any +// refs or terms that require evaluation (e.g., vars, comprehensions, etc.) +func JSONWithOpt(v Value, opt JSONOpt) (interface{}, error) { + return v1.JSONWithOpt(v, opt) +} + +// MustJSON returns the JSON representation of v. The value must not contain any +// refs or terms that require evaluation (e.g., vars, comprehensions, etc.) If +// the conversion fails, this function will panic. This function is mostly for +// test purposes. +func MustJSON(v Value) interface{} { + return v1.MustJSON(v) +} + +// MustInterfaceToValue converts a native Go value x to a Value. If the +// conversion fails, this function will panic. This function is mostly for test +// purposes. +func MustInterfaceToValue(x interface{}) Value { + return v1.MustInterfaceToValue(x) +} + +// Term is an argument to a function. +type Term = v1.Term + +// NewTerm returns a new Term object. +func NewTerm(v Value) *Term { + return v1.NewTerm(v) +} + +// IsConstant returns true if the AST value is constant. +func IsConstant(v Value) bool { + return v1.IsConstant(v) +} + +// IsComprehension returns true if the supplied value is a comprehension. +func IsComprehension(x Value) bool { + return v1.IsComprehension(x) +} + +// ContainsRefs returns true if the Value v contains refs. +func ContainsRefs(v interface{}) bool { + return v1.ContainsRefs(v) +} + +// ContainsComprehensions returns true if the Value v contains comprehensions. +func ContainsComprehensions(v interface{}) bool { + return v1.ContainsComprehensions(v) +} + +// ContainsClosures returns true if the Value v contains closures. +func ContainsClosures(v interface{}) bool { + return v1.ContainsClosures(v) +} + +// IsScalar returns true if the AST value is a scalar. +func IsScalar(v Value) bool { + return v1.IsScalar(v) +} + +// Null represents the null value defined by JSON. +type Null = v1.Null + +// NullTerm creates a new Term with a Null value. +func NullTerm() *Term { + return v1.NullTerm() +} + +// Boolean represents a boolean value defined by JSON. +type Boolean = v1.Boolean + +// BooleanTerm creates a new Term with a Boolean value. +func BooleanTerm(b bool) *Term { + return v1.BooleanTerm(b) +} + +// Number represents a numeric value as defined by JSON. +type Number = v1.Number + +// NumberTerm creates a new Term with a Number value. +func NumberTerm(n json.Number) *Term { + return v1.NumberTerm(n) +} + +// IntNumberTerm creates a new Term with an integer Number value. +func IntNumberTerm(i int) *Term { + return v1.IntNumberTerm(i) +} + +// UIntNumberTerm creates a new Term with an unsigned integer Number value. +func UIntNumberTerm(u uint64) *Term { + return v1.UIntNumberTerm(u) +} + +// FloatNumberTerm creates a new Term with a floating point Number value. +func FloatNumberTerm(f float64) *Term { + return v1.FloatNumberTerm(f) +} + +// String represents a string value as defined by JSON. +type String = v1.String + +// StringTerm creates a new Term with a String value. +func StringTerm(s string) *Term { + return v1.StringTerm(s) +} + +// Var represents a variable as defined by the language. +type Var = v1.Var + +// VarTerm creates a new Term with a Variable value. +func VarTerm(v string) *Term { + return v1.VarTerm(v) +} + +// Ref represents a reference as defined by the language. +type Ref = v1.Ref + +// EmptyRef returns a new, empty reference. +func EmptyRef() Ref { + return v1.EmptyRef() +} + +// PtrRef returns a new reference against the head for the pointer +// s. Path components in the pointer are unescaped. +func PtrRef(head *Term, s string) (Ref, error) { + return v1.PtrRef(head, s) +} + +// RefTerm creates a new Term with a Ref value. +func RefTerm(r ...*Term) *Term { + return v1.RefTerm(r...) +} + +func IsVarCompatibleString(s string) bool { + return v1.IsVarCompatibleString(s) +} + +// QueryIterator defines the interface for querying AST documents with references. +type QueryIterator = v1.QueryIterator + +// ArrayTerm creates a new Term with an Array value. +func ArrayTerm(a ...*Term) *Term { + return v1.ArrayTerm(a...) +} + +// NewArray creates an Array with the terms provided. The array will +// use the provided term slice. +func NewArray(a ...*Term) *Array { + return v1.NewArray(a...) +} + +// Array represents an array as defined by the language. Arrays are similar to the +// same types as defined by JSON with the exception that they can contain Vars +// and References. +type Array = v1.Array + +// Set represents a set as defined by the language. +type Set = v1.Set + +// NewSet returns a new Set containing t. +func NewSet(t ...*Term) Set { + return v1.NewSet(t...) +} + +func SetTerm(t ...*Term) *Term { + return v1.SetTerm(t...) +} + +// Object represents an object as defined by the language. +type Object = v1.Object + +// NewObject creates a new Object with t. +func NewObject(t ...[2]*Term) Object { + return v1.NewObject(t...) +} + +// ObjectTerm creates a new Term with an Object value. +func ObjectTerm(o ...[2]*Term) *Term { + return v1.ObjectTerm(o...) +} + +func LazyObject(blob map[string]interface{}) Object { + return v1.LazyObject(blob) +} + +// Item is a helper for constructing an tuple containing two Terms +// representing a key/value pair in an Object. +func Item(key, value *Term) [2]*Term { + return v1.Item(key, value) +} + +// NOTE(philipc): The only way to get an ObjectKeyIterator should be +// from an Object. This ensures that the iterator can have implementation- +// specific details internally, with no contracts except to the very +// limited interface. +type ObjectKeysIterator = v1.ObjectKeysIterator + +// ArrayComprehension represents an array comprehension as defined in the language. +type ArrayComprehension = v1.ArrayComprehension + +// ArrayComprehensionTerm creates a new Term with an ArrayComprehension value. +func ArrayComprehensionTerm(term *Term, body Body) *Term { + return v1.ArrayComprehensionTerm(term, body) +} + +// ObjectComprehension represents an object comprehension as defined in the language. +type ObjectComprehension = v1.ObjectComprehension + +// ObjectComprehensionTerm creates a new Term with an ObjectComprehension value. +func ObjectComprehensionTerm(key, value *Term, body Body) *Term { + return v1.ObjectComprehensionTerm(key, value, body) +} + +// SetComprehension represents a set comprehension as defined in the language. +type SetComprehension = v1.SetComprehension + +// SetComprehensionTerm creates a new Term with an SetComprehension value. +func SetComprehensionTerm(term *Term, body Body) *Term { + return v1.SetComprehensionTerm(term, body) +} + +// Call represents as function call in the language. +type Call = v1.Call + +// CallTerm returns a new Term with a Call value defined by terms. The first +// term is the operator and the rest are operands. +func CallTerm(terms ...*Term) *Term { + return v1.CallTerm(terms...) +} diff --git a/ast/transform.go b/ast/transform.go new file mode 100644 index 0000000000..cfb137813f --- /dev/null +++ b/ast/transform.go @@ -0,0 +1,46 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// Transformer defines the interface for transforming AST elements. If the +// transformer returns nil and does not indicate an error, the AST element will +// be set to nil and no transformations will be applied to children of the +// element. +type Transformer = v1.Transformer + +// Transform iterates the AST and calls the Transform function on the +// Transformer t for x before recursing. +func Transform(t Transformer, x interface{}) (interface{}, error) { + return v1.Transform(t, x) +} + +// TransformRefs calls the function f on all references under x. +func TransformRefs(x interface{}, f func(Ref) (Value, error)) (interface{}, error) { + return v1.TransformRefs(x, f) +} + +// TransformVars calls the function f on all vars under x. +func TransformVars(x interface{}, f func(Var) (Value, error)) (interface{}, error) { + return v1.TransformVars(x, f) +} + +// TransformComprehensions calls the functio nf on all comprehensions under x. +func TransformComprehensions(x interface{}, f func(interface{}) (Value, error)) (interface{}, error) { + return v1.TransformComprehensions(x, f) +} + +// GenericTransformer implements the Transformer interface to provide a utility +// to transform AST nodes using a closure. +type GenericTransformer = v1.GenericTransformer + +// NewGenericTransformer returns a new GenericTransformer that will transform +// AST nodes using the function f. +func NewGenericTransformer(f func(x interface{}) (interface{}, error)) *GenericTransformer { + return v1.NewGenericTransformer(f) +} diff --git a/ast/unify.go b/ast/unify.go new file mode 100644 index 0000000000..3cb260272a --- /dev/null +++ b/ast/unify.go @@ -0,0 +1,14 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import v1 "github.com/open-policy-agent/opa/v1/ast" + +// Unify returns a set of variables that will be unified when the equality expression defined by +// terms a and b is evaluated. The unifier assumes that variables in the VarSet safe are already +// unified. +func Unify(safe VarSet, a *Term, b *Term) VarSet { + return v1.Unify(safe, a, b) +} diff --git a/ast/varset.go b/ast/varset.go new file mode 100644 index 0000000000..9e7db8efda --- /dev/null +++ b/ast/varset.go @@ -0,0 +1,17 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + v1 "github.com/open-policy-agent/opa/v1/ast" +) + +// VarSet represents a set of variables. +type VarSet = v1.VarSet + +// NewVarSet returns a new VarSet containing the specified variables. +func NewVarSet(vs ...Var) VarSet { + return v1.NewVarSet(vs...) +} diff --git a/ast/visit.go b/ast/visit.go new file mode 100644 index 0000000000..94823c6cc7 --- /dev/null +++ b/ast/visit.go @@ -0,0 +1,123 @@ +// Copyright 2016 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import v1 "github.com/open-policy-agent/opa/v1/ast" + +// Visitor defines the interface for iterating AST elements. The Visit function +// can return a Visitor w which will be used to visit the children of the AST +// element v. If the Visit function returns nil, the children will not be +// visited. +// Deprecated: use GenericVisitor or another visitor implementation +type Visitor = v1.Visitor + +// BeforeAndAfterVisitor wraps Visitor to provide hooks for being called before +// and after the AST has been visited. +// Deprecated: use GenericVisitor or another visitor implementation +type BeforeAndAfterVisitor = v1.BeforeAndAfterVisitor + +// Walk iterates the AST by calling the Visit function on the Visitor +// v for x before recursing. +// Deprecated: use GenericVisitor.Walk +func Walk(v Visitor, x interface{}) { + v1.Walk(v, x) +} + +// WalkBeforeAndAfter iterates the AST by calling the Visit function on the +// Visitor v for x before recursing. +// Deprecated: use GenericVisitor.Walk +func WalkBeforeAndAfter(v BeforeAndAfterVisitor, x interface{}) { + v1.WalkBeforeAndAfter(v, x) +} + +// WalkVars calls the function f on all vars under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkVars(x interface{}, f func(Var) bool) { + v1.WalkVars(x, f) +} + +// WalkClosures calls the function f on all closures under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkClosures(x interface{}, f func(interface{}) bool) { + v1.WalkClosures(x, f) +} + +// WalkRefs calls the function f on all references under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkRefs(x interface{}, f func(Ref) bool) { + v1.WalkRefs(x, f) +} + +// WalkTerms calls the function f on all terms under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkTerms(x interface{}, f func(*Term) bool) { + v1.WalkTerms(x, f) +} + +// WalkWiths calls the function f on all with modifiers under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkWiths(x interface{}, f func(*With) bool) { + v1.WalkWiths(x, f) +} + +// WalkExprs calls the function f on all expressions under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkExprs(x interface{}, f func(*Expr) bool) { + v1.WalkExprs(x, f) +} + +// WalkBodies calls the function f on all bodies under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkBodies(x interface{}, f func(Body) bool) { + v1.WalkBodies(x, f) +} + +// WalkRules calls the function f on all rules under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkRules(x interface{}, f func(*Rule) bool) { + v1.WalkRules(x, f) +} + +// WalkNodes calls the function f on all nodes under x. If the function f +// returns true, AST nodes under the last node will not be visited. +func WalkNodes(x interface{}, f func(Node) bool) { + v1.WalkNodes(x, f) +} + +// GenericVisitor provides a utility to walk over AST nodes using a +// closure. If the closure returns true, the visitor will not walk +// over AST nodes under x. +type GenericVisitor = v1.GenericVisitor + +// NewGenericVisitor returns a new GenericVisitor that will invoke the function +// f on AST nodes. +func NewGenericVisitor(f func(x interface{}) bool) *GenericVisitor { + return v1.NewGenericVisitor(f) +} + +// BeforeAfterVisitor provides a utility to walk over AST nodes using +// closures. If the before closure returns true, the visitor will not +// walk over AST nodes under x. The after closure is invoked always +// after visiting a node. +type BeforeAfterVisitor = v1.BeforeAfterVisitor + +// NewBeforeAfterVisitor returns a new BeforeAndAfterVisitor that +// will invoke the functions before and after AST nodes. +func NewBeforeAfterVisitor(before func(x interface{}) bool, after func(x interface{})) *BeforeAfterVisitor { + return v1.NewBeforeAfterVisitor(before, after) +} + +// VarVisitor walks AST nodes under a given node and collects all encountered +// variables. The collected variables can be controlled by specifying +// VarVisitorParams when creating the visitor. +type VarVisitor = v1.VarVisitor + +// VarVisitorParams contains settings for a VarVisitor. +type VarVisitorParams = v1.VarVisitorParams + +// NewVarVisitor returns a new VarVisitor object. +func NewVarVisitor() *VarVisitor { + return v1.NewVarVisitor() +} diff --git a/build/binary-smoke-test.sh b/build/binary-smoke-test.sh index f06c3b6afd..827a2fb72c 100755 --- a/build/binary-smoke-test.sh +++ b/build/binary-smoke-test.sh @@ -5,11 +5,11 @@ TARGET="$2" PATH_SEPARATOR="/" BASE_PATH=$(pwd) -TEST_PATH="${BASE_PATH}/test/cli/smoke/namespace/data.json" +TEST_PATH="${BASE_PATH}/v1/test/cli/smoke/namespace/data.json" if [[ $OPA_EXEC == *".exe" ]]; then PATH_SEPARATOR="\\" BASE_PATH=$(pwd -W) - TEST_PATH="$(echo ${BASE_PATH}/test/cli/smoke/namespace/data.json | sed 's/^\///' | sed 's/\//\\\\/g')" + TEST_PATH="$(echo ${BASE_PATH}/v1/test/cli/smoke/namespace/data.json | sed 's/^\///' | sed 's/\//\\\\/g')" BASE_PATH=$(echo ${BASE_PATH} | sed 's/^\///' | sed 's/\//\\/g') fi @@ -47,26 +47,26 @@ assert_not_contains() { opa version opa eval -t $TARGET 'time.now_ns()' -opa eval --format pretty --bundle test/cli/smoke/golden-bundle.tar.gz --input test/cli/smoke/input.json data.test.result --fail -opa exec --bundle test/cli/smoke/golden-bundle.tar.gz --decision test/result test/cli/smoke/input.json -opa build --output o0.tar.gz test/cli/smoke/data.yaml test/cli/smoke/test.rego +opa eval --format pretty --bundle v1/test/cli/smoke/golden-bundle.tar.gz --input v1/test/cli/smoke/input.json data.test.result --fail +opa exec --bundle v1/test/cli/smoke/golden-bundle.tar.gz --decision test/result v1/test/cli/smoke/input.json +opa build --output o0.tar.gz v1/test/cli/smoke/data.yaml v1/test/cli/smoke/test.rego echo '{"yay": "bar"}' | opa eval --format pretty --bundle o0.tar.gz -I data.test.result --fail -opa build --optimize 1 --output o1.tar.gz test/cli/smoke/data.yaml test/cli/smoke/test.rego +opa build --optimize 1 --output o1.tar.gz v1/test/cli/smoke/data.yaml v1/test/cli/smoke/test.rego echo '{"yay": "bar"}' | opa eval --format pretty --bundle o1.tar.gz -I data.test.result --fail -opa build --optimize 2 --output o2.tar.gz test/cli/smoke/data.yaml test/cli/smoke/test.rego +opa build --optimize 2 --output o2.tar.gz v1/test/cli/smoke/data.yaml v1/test/cli/smoke/test.rego echo '{"yay": "bar"}' | opa eval --format pretty --bundle o2.tar.gz -I data.test.result --fail # Tar paths -opa build --output o3.tar.gz test/cli/smoke -github_actions_group assert_contains '/test/cli/smoke/test.rego' "$(tar -tf o3.tar.gz /test/cli/smoke/test.rego)" +opa build --output o3.tar.gz v1/test/cli/smoke +github_actions_group assert_contains '/v1/test/cli/smoke/test.rego' "$(tar -tf o3.tar.gz /v1/test/cli/smoke/test.rego)" # Data files - correct namespaces echo "::group:: Data files - correct namespaces" -assert_contains "data.namespace | test${PATH_SEPARATOR}cli${PATH_SEPARATOR}smoke${PATH_SEPARATOR}namespace${PATH_SEPARATOR}data.json" "$(opa inspect test/cli/smoke)" +assert_contains "data.namespace | v1${PATH_SEPARATOR}test${PATH_SEPARATOR}cli${PATH_SEPARATOR}smoke${PATH_SEPARATOR}namespace${PATH_SEPARATOR}data.json" "$(opa inspect v1/test/cli/smoke)" echo "::endgroup::" # Data files - correct root path echo "::group:: Data files - correct root path" -assert_contains "${TEST_PATH}" "$(opa inspect ${BASE_PATH}/test/cli/smoke -f json)" -assert_not_contains "\\\\${TEST_PATH}" "$(opa inspect ${BASE_PATH}/test/cli/smoke -f json)" +assert_contains "${TEST_PATH}" "$(opa inspect ${BASE_PATH}/v1/test/cli/smoke -f json)" +assert_not_contains "\\\\${TEST_PATH}" "$(opa inspect ${BASE_PATH}/v1/test/cli/smoke -f json)" echo "::endgroup::" \ No newline at end of file diff --git a/build/get-build-version.sh b/build/get-build-version.sh index 435a11ac65..50b9d73d32 100755 --- a/build/get-build-version.sh +++ b/build/get-build-version.sh @@ -1,3 +1,3 @@ #!/usr/bin/env bash -awk -F'"' '/^var Version/{print $2}' version/version.go \ No newline at end of file +awk -F'"' '/^var Version/{print $2}' v1/version/version.go \ No newline at end of file diff --git a/build/update-version.sh b/build/update-version.sh index d5a20d73a4..0c854bd9f6 100755 --- a/build/update-version.sh +++ b/build/update-version.sh @@ -3,4 +3,4 @@ set -e # NOTE(sr): This was the only way I've found to replace the string # reliably on OSX and Linux. -perl -pi -e "s/Version = \".*\"$/Version = \"$1\"/" version/version.go \ No newline at end of file +perl -pi -e "s/Version = \".*\"$/Version = \"$1\"/" v1/version/version.go \ No newline at end of file diff --git a/bundle/bundle.go b/bundle/bundle.go new file mode 100644 index 0000000000..50ad97349a --- /dev/null +++ b/bundle/bundle.go @@ -0,0 +1,134 @@ +// 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 bundle implements bundle loading. +package bundle + +import ( + "io" + + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// Common file extensions and file names. +const ( + RegoExt = v1.RegoExt + WasmFile = v1.WasmFile + PlanFile = v1.PlanFile + ManifestExt = v1.ManifestExt + SignaturesFile = v1.SignaturesFile + + DefaultSizeLimitBytes = v1.DefaultSizeLimitBytes + DeltaBundleType = v1.DeltaBundleType + SnapshotBundleType = v1.SnapshotBundleType +) + +// Bundle represents a loaded bundle. The bundle can contain data and policies. +type Bundle = v1.Bundle + +// Raw contains raw bytes representing the bundle's content +type Raw = v1.Raw + +// Patch contains an array of objects wherein each object represents the patch operation to be +// applied to the bundle data. +type Patch = v1.Patch + +// PatchOperation models a single patch operation against a document. +type PatchOperation = v1.PatchOperation + +// SignaturesConfig represents an array of JWTs that encapsulate the signatures for the bundle. +type SignaturesConfig = v1.SignaturesConfig + +// DecodedSignature represents the decoded JWT payload. +type DecodedSignature = v1.DecodedSignature + +// FileInfo contains the hashing algorithm used, resulting digest etc. +type FileInfo = v1.FileInfo + +// NewFile returns a new FileInfo. +func NewFile(name, hash, alg string) FileInfo { + return v1.NewFile(name, hash, alg) +} + +// Manifest represents the manifest from a bundle. The manifest may contain +// metadata such as the bundle revision. +type Manifest = v1.Manifest + +// WasmResolver maps a wasm module to an entrypoint ref. +type WasmResolver = v1.WasmResolver + +// ModuleFile represents a single module contained in a bundle. +type ModuleFile = v1.ModuleFile + +// WasmModuleFile represents a single wasm module contained in a bundle. +type WasmModuleFile = v1.WasmModuleFile + +// PlanModuleFile represents a single plan module contained in a bundle. +// +// NOTE(tsandall): currently the plans are just opaque binary blobs. In the +// future we could inject the entrypoints so that the plans could be executed +// inside of OPA proper like we do for Wasm modules. +type PlanModuleFile = v1.PlanModuleFile + +// Reader contains the reader to load the bundle from. +type Reader = v1.Reader + +// NewReader is deprecated. Use NewCustomReader instead. +func NewReader(r io.Reader) *Reader { + return v1.NewReader(r).WithRegoVersion(ast.DefaultRegoVersion) +} + +// NewCustomReader returns a new Reader configured to use the +// specified DirectoryLoader. +func NewCustomReader(loader DirectoryLoader) *Reader { + return v1.NewCustomReader(loader).WithRegoVersion(ast.DefaultRegoVersion) +} + +// Write is deprecated. Use NewWriter instead. +func Write(w io.Writer, bundle Bundle) error { + return v1.Write(w, bundle) +} + +// Writer implements bundle serialization. +type Writer = v1.Writer + +// NewWriter returns a bundle writer that writes to w. +func NewWriter(w io.Writer) *Writer { + return v1.NewWriter(w) +} + +// Merge accepts a set of bundles and merges them into a single result bundle. If there are +// any conflicts during the merge (e.g., with roots) an error is returned. The result bundle +// will have an empty revision except in the special case where a single bundle is provided +// (and in that case the bundle is just returned unmodified.) +func Merge(bundles []*Bundle) (*Bundle, error) { + return MergeWithRegoVersion(bundles, ast.DefaultRegoVersion, false) +} + +// MergeWithRegoVersion creates a merged bundle from the provided bundles, similar to Merge. +// If more than one bundle is provided, the rego version of the result bundle is set to the provided regoVersion. +// Any Rego files in a bundle of conflicting rego version will be marked in the result's manifest with the rego version +// of its original bundle. If the Rego file already had an overriding rego version, it will be preserved. +// If a single bundle is provided, it will retain any rego version information it already had. If it has none, the +// provided regoVersion will be applied to it. +// If usePath is true, per-file rego-versions will be calculated using the file's ModuleFile.Path; otherwise, the file's +// ModuleFile.URL will be used. +func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePath bool) (*Bundle, error) { + if regoVersion == ast.RegoUndefined { + regoVersion = ast.DefaultRegoVersion + } + + return v1.MergeWithRegoVersion(bundles, regoVersion, usePath) +} + +// RootPathsOverlap takes in two bundle root paths and returns true if they overlap. +func RootPathsOverlap(pathA string, pathB string) bool { + return v1.RootPathsOverlap(pathA, pathB) +} + +// RootPathsContain takes a set of bundle root paths and returns true if the path is contained. +func RootPathsContain(roots []string, path string) bool { + return v1.RootPathsContain(roots, path) +} diff --git a/bundle/bundle_test.go b/bundle/bundle_test.go new file mode 100644 index 0000000000..6d58c3a223 --- /dev/null +++ b/bundle/bundle_test.go @@ -0,0 +1,84 @@ +// Copyright 2024 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 bundle + +import ( + "strings" + "testing" + + "github.com/open-policy-agent/opa/internal/file/archive" +) + +func TestRead_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package example + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package example +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package example + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + module := tc.module + files := [][2]string{ + {"test.rego", module}, + } + + buf := archive.MustWriteTarGz(files) + loader := NewTarballLoaderWithBaseURL(buf, "") + br := NewCustomReader(loader) + + bundle, err := br.Read() + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error(s):\n\n%v\n\nbut got nil", tc.expErrs) + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(bundle.Modules) != 1 { + t.Fatalf("expected 1 module but got %d", len(bundle.Modules)) + } + } + }) + } +} diff --git a/bundle/doc.go b/bundle/doc.go new file mode 100644 index 0000000000..7ec7c9b332 --- /dev/null +++ b/bundle/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package bundle diff --git a/bundle/file.go b/bundle/file.go new file mode 100644 index 0000000000..ccb7b23510 --- /dev/null +++ b/bundle/file.go @@ -0,0 +1,50 @@ +package bundle + +import ( + "io" + + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// Descriptor contains information about a file and +// can be used to read the file contents. +type Descriptor = v1.Descriptor + +func NewDescriptor(url, path string, reader io.Reader) *Descriptor { + return v1.NewDescriptor(url, path, reader) +} + +type PathFormat = v1.PathFormat + +const ( + Chrooted = v1.Chrooted + SlashRooted = v1.SlashRooted + Passthrough = v1.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 = v1.DirectoryLoader + +// NewDirectoryLoader returns a basic DirectoryLoader implementation +// that will load files from a given root directory path. +func NewDirectoryLoader(root string) DirectoryLoader { + return v1.NewDirectoryLoader(root) +} + +// NewTarballLoader is deprecated. Use NewTarballLoaderWithBaseURL instead. +func NewTarballLoader(r io.Reader) DirectoryLoader { + return v1.NewTarballLoader(r) +} + +// NewTarballLoaderWithBaseURL returns a new DirectoryLoader that reads +// files out of a gzipped tar archive. The file URLs will be prefixed +// with the baseURL. +func NewTarballLoaderWithBaseURL(r io.Reader, baseURL string) DirectoryLoader { + return v1.NewTarballLoaderWithBaseURL(r, baseURL) +} + +func NewIterator(raw []Raw) storage.Iterator { + return v1.NewIterator(raw) +} diff --git a/bundle/filefs.go b/bundle/filefs.go new file mode 100644 index 0000000000..16e00928da --- /dev/null +++ b/bundle/filefs.go @@ -0,0 +1,22 @@ +//go:build go1.16 +// +build go1.16 + +package bundle + +import ( + "io/fs" + + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// NewFSLoader returns a basic DirectoryLoader implementation +// that will load files from a fs.FS interface +func NewFSLoader(filesystem fs.FS) (DirectoryLoader, error) { + return v1.NewFSLoader(filesystem) +} + +// 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 { + return v1.NewFSLoaderWithRoot(filesystem, root) +} diff --git a/bundle/hash.go b/bundle/hash.go new file mode 100644 index 0000000000..d4cc601dea --- /dev/null +++ b/bundle/hash.go @@ -0,0 +1,32 @@ +// 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 bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// HashingAlgorithm represents a subset of hashing algorithms implemented in Go +type HashingAlgorithm = v1.HashingAlgorithm + +// Supported values for HashingAlgorithm +const ( + MD5 = v1.MD5 + SHA1 = v1.SHA1 + SHA224 = v1.SHA224 + SHA256 = v1.SHA256 + SHA384 = v1.SHA384 + SHA512 = v1.SHA512 + SHA512224 = v1.SHA512224 + SHA512256 = v1.SHA512256 +) + +// SignatureHasher computes a signature digest for a file with (structured or unstructured) data and policy +type SignatureHasher = v1.SignatureHasher + +// NewSignatureHasher returns a signature hasher suitable for a particular hashing algorithm +func NewSignatureHasher(alg HashingAlgorithm) (SignatureHasher, error) { + return v1.NewSignatureHasher(alg) +} diff --git a/bundle/keys.go b/bundle/keys.go new file mode 100644 index 0000000000..99f9b0f165 --- /dev/null +++ b/bundle/keys.go @@ -0,0 +1,30 @@ +// 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 bundle provide helpers that assist in creating the verification and signing key configuration +package bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// KeyConfig holds the keys used to sign or verify bundles and tokens +// Moved to own package, alias kept for backwards compatibility +type KeyConfig = v1.KeyConfig + +// VerificationConfig represents the key configuration used to verify a signed bundle +type VerificationConfig = v1.VerificationConfig + +// NewVerificationConfig return a new VerificationConfig +func NewVerificationConfig(keys map[string]*KeyConfig, id, scope string, exclude []string) *VerificationConfig { + return v1.NewVerificationConfig(keys, id, scope, exclude) +} + +// SigningConfig represents the key configuration used to generate a signed bundle +type SigningConfig = v1.SigningConfig + +// NewSigningConfig return a new SigningConfig +func NewSigningConfig(key, alg, claimsPath string) *SigningConfig { + return v1.NewSigningConfig(key, alg, claimsPath) +} diff --git a/bundle/sign.go b/bundle/sign.go new file mode 100644 index 0000000000..56e25eec9c --- /dev/null +++ b/bundle/sign.go @@ -0,0 +1,35 @@ +// 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 bundle provide helpers that assist in the creating a signed bundle +package bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// Signer is the interface expected for implementations that generate bundle signatures. +type Signer v1.Signer + +// GenerateSignedToken will retrieve the Signer implementation based on the Plugin specified +// in SigningConfig, and call its implementation of GenerateSignedToken. The signer generates +// a signed token given the list of files to be included in the payload and the bundle +// signing config. The keyID if non-empty, represents the value for the "keyid" claim in the token. +func GenerateSignedToken(files []FileInfo, sc *SigningConfig, keyID string) (string, error) { + return v1.GenerateSignedToken(files, sc, keyID) +} + +// DefaultSigner is the default bundle signing implementation. It signs bundles by generating +// a JWT and signing it using a locally-accessible private key. +type DefaultSigner v1.DefaultSigner + +// GetSigner returns the Signer registered under the given id +func GetSigner(id string) (Signer, error) { + return v1.GetSigner(id) +} + +// RegisterSigner registers a Signer under the given id +func RegisterSigner(id string, s Signer) error { + return v1.RegisterSigner(id, s) +} diff --git a/bundle/store.go b/bundle/store.go new file mode 100644 index 0000000000..d73cc77422 --- /dev/null +++ b/bundle/store.go @@ -0,0 +1,123 @@ +// 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 bundle + +import ( + "context" + + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// BundlesBasePath is the storage path used for storing bundle metadata +var BundlesBasePath = v1.BundlesBasePath + +// Note: As needed these helpers could be memoized. + +// ManifestStoragePath is the storage path used for the given named bundle manifest. +func ManifestStoragePath(name string) storage.Path { + return v1.ManifestStoragePath(name) +} + +// EtagStoragePath is the storage path used for the given named bundle etag. +func EtagStoragePath(name string) storage.Path { + return v1.EtagStoragePath(name) +} + +// ReadBundleNamesFromStore will return a list of bundle names which have had their metadata stored. +func ReadBundleNamesFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) ([]string, error) { + return v1.ReadBundleNamesFromStore(ctx, store, txn) +} + +// WriteManifestToStore will write the manifest into the storage. This function is called when +// the bundle is activated. +func WriteManifestToStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string, manifest Manifest) error { + return v1.WriteManifestToStore(ctx, store, txn, name, manifest) +} + +// WriteEtagToStore will write the bundle etag into the storage. This function is called when the bundle is activated. +func WriteEtagToStore(ctx context.Context, store storage.Store, txn storage.Transaction, name, etag string) error { + return v1.WriteEtagToStore(ctx, store, txn, name, etag) +} + +// EraseManifestFromStore will remove the manifest from storage. This function is called +// when the bundle is deactivated. +func EraseManifestFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) error { + return v1.EraseManifestFromStore(ctx, store, txn, name) +} + +// ReadWasmModulesFromStore will write Wasm module resolver metadata from the store. +func ReadWasmModulesFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (map[string][]byte, error) { + return v1.ReadWasmModulesFromStore(ctx, store, txn, name) +} + +// ReadBundleRootsFromStore returns the roots in the specified bundle. +// If the bundle is not activated, this function will return +// storage NotFound error. +func ReadBundleRootsFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) ([]string, error) { + return v1.ReadBundleRootsFromStore(ctx, store, txn, name) +} + +// ReadBundleRevisionFromStore returns the revision in the specified bundle. +// If the bundle is not activated, this function will return +// storage NotFound error. +func ReadBundleRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (string, error) { + return v1.ReadBundleRevisionFromStore(ctx, store, txn, name) +} + +// ReadBundleMetadataFromStore returns the metadata in the specified bundle. +// If the bundle is not activated, this function will return +// storage NotFound error. +func ReadBundleMetadataFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (map[string]interface{}, error) { + return v1.ReadBundleMetadataFromStore(ctx, store, txn, name) +} + +// ReadBundleEtagFromStore returns the etag for the specified bundle. +// If the bundle is not activated, this function will return +// storage NotFound error. +func ReadBundleEtagFromStore(ctx context.Context, store storage.Store, txn storage.Transaction, name string) (string, error) { + return v1.ReadBundleEtagFromStore(ctx, store, txn, name) +} + +// ActivateOpts defines options for the Activate API call. +type ActivateOpts = v1.ActivateOpts + +// Activate the bundle(s) by loading into the given Store. This will load policies, data, and record +// the manifest in storage. The compiler provided will have had the polices compiled on it. +func Activate(opts *ActivateOpts) error { + return v1.Activate(opts) +} + +// DeactivateOpts defines options for the Deactivate API call +type DeactivateOpts = v1.DeactivateOpts + +// Deactivate the bundle(s). This will erase associated data, policies, and the manifest entry from the store. +func Deactivate(opts *DeactivateOpts) error { + return v1.Deactivate(opts) +} + +// LegacyWriteManifestToStore will write the bundle manifest to the older single (unnamed) bundle manifest location. +// Deprecated: Use WriteManifestToStore and named bundles instead. +func LegacyWriteManifestToStore(ctx context.Context, store storage.Store, txn storage.Transaction, manifest Manifest) error { + return v1.LegacyWriteManifestToStore(ctx, store, txn, manifest) +} + +// LegacyEraseManifestFromStore will erase the bundle manifest from the older single (unnamed) bundle manifest location. +// Deprecated: Use WriteManifestToStore and named bundles instead. +func LegacyEraseManifestFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) error { + return v1.LegacyEraseManifestFromStore(ctx, store, txn) +} + +// LegacyReadRevisionFromStore will read the bundle manifest revision from the older single (unnamed) bundle manifest location. +// Deprecated: Use ReadBundleRevisionFromStore and named bundles instead. +func LegacyReadRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) (string, error) { + return v1.LegacyReadRevisionFromStore(ctx, store, txn) +} + +// ActivateLegacy calls Activate for the bundles but will also write their manifest to the older unnamed store location. +// Deprecated: Use Activate with named bundles instead. +func ActivateLegacy(opts *ActivateOpts) error { + return v1.ActivateLegacy(opts) +} diff --git a/bundle/store_test.go b/bundle/store_test.go new file mode 100644 index 0000000000..405779c230 --- /dev/null +++ b/bundle/store_test.go @@ -0,0 +1,104 @@ +// Copyright 2024 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 bundle + +import ( + "context" + "testing" + + "github.com/open-policy-agent/opa/internal/storage/mock" + "github.com/open-policy-agent/opa/storage" +) + +func TestHasRootsOverlap(t *testing.T) { + ctx := context.Background() + + cases := []struct { + note string + storeRoots map[string]*[]string + bundleRoots map[string]*[]string + overlaps bool + }{ + { + note: "no overlap with existing roots", + storeRoots: map[string]*[]string{"bundle1": {"a", "b"}}, + bundleRoots: map[string]*[]string{"bundle2": {"c"}}, + overlaps: false, + }, + { + note: "no overlap with existing roots multiple bundles", + storeRoots: map[string]*[]string{"bundle1": {"a", "b"}}, + bundleRoots: map[string]*[]string{"bundle2": {"c"}, "bundle3": {"d"}}, + overlaps: false, + }, + { + note: "no overlap no existing roots", + storeRoots: map[string]*[]string{}, + bundleRoots: map[string]*[]string{"bundle1": {"a", "b"}}, + overlaps: false, + }, + { + note: "no overlap without existing roots multiple bundles", + storeRoots: map[string]*[]string{}, + bundleRoots: map[string]*[]string{"bundle1": {"a", "b"}, "bundle2": {"c"}}, + overlaps: false, + }, + { + note: "overlap without existing roots multiple bundles", + storeRoots: map[string]*[]string{}, + bundleRoots: map[string]*[]string{"bundle1": {"a", "b"}, "bundle2": {"a", "c"}}, + overlaps: true, + }, + { + note: "overlap with existing roots", + storeRoots: map[string]*[]string{"bundle1": {"a", "b"}}, + bundleRoots: map[string]*[]string{"bundle2": {"c", "a"}}, + overlaps: true, + }, + { + note: "overlap with existing roots multiple bundles", + storeRoots: map[string]*[]string{"bundle1": {"a", "b"}}, + bundleRoots: map[string]*[]string{"bundle2": {"c", "a"}, "bundle3": {"a"}}, + overlaps: true, + }, + } + + for _, tc := range cases { + t.Run(tc.note, func(t *testing.T) { + mockStore := mock.New() + txn := storage.NewTransactionOrDie(ctx, mockStore, storage.WriteParams) + + for name, roots := range tc.storeRoots { + err := WriteManifestToStore(ctx, mockStore, txn, name, Manifest{Roots: roots}) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + bundles := map[string]*Bundle{} + for name, roots := range tc.bundleRoots { + bundles[name] = &Bundle{ + Manifest: Manifest{ + Roots: roots, + }, + } + } + + //err := hasRootsOverlap(ctx, mockStore, txn, bundles) + //if !tc.overlaps && err != nil { + // t.Fatalf("unepected error: %s", err) + //} else if tc.overlaps && (err == nil || !strings.Contains(err.Error(), "detected overlapping roots in bundle manifest")) { + // t.Fatalf("expected overlapping roots error, got: %s", err) + //} + + //err = mockStore.Commit(ctx, txn) + //if err != nil { + // t.Fatalf("unexpected error: %s", err) + //} + + mockStore.AssertValid(t) + }) + } +} diff --git a/bundle/verify.go b/bundle/verify.go new file mode 100644 index 0000000000..ef2e1e32db --- /dev/null +++ b/bundle/verify.go @@ -0,0 +1,36 @@ +// 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 bundle provide helpers that assist in the bundle signature verification process +package bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/bundle" +) + +// Verifier is the interface expected for implementations that verify bundle signatures. +type Verifier v1.Verifier + +// VerifyBundleSignature will retrieve the Verifier implementation based +// on the Plugin specified in SignaturesConfig, and call its implementation +// of VerifyBundleSignature. VerifyBundleSignature verifies the bundle signature +// using the given public keys or secret. If a signature is verified, it keeps +// track of the files specified in the JWT payload +func VerifyBundleSignature(sc SignaturesConfig, bvc *VerificationConfig) (map[string]FileInfo, error) { + return v1.VerifyBundleSignature(sc, bvc) +} + +// DefaultVerifier is the default bundle verification implementation. It verifies bundles by checking +// the JWT signature using a locally-accessible public key. +type DefaultVerifier = v1.DefaultVerifier + +// GetVerifier returns the Verifier registered under the given id +func GetVerifier(id string) (Verifier, error) { + return v1.GetVerifier(id) +} + +// RegisterVerifier registers a Verifier under the given id +func RegisterVerifier(id string, v Verifier) error { + return v1.RegisterVerifier(id, v) +} diff --git a/capabilities/capabilities.go b/capabilities/capabilities.go new file mode 100644 index 0000000000..a5e7254f2e --- /dev/null +++ b/capabilities/capabilities.go @@ -0,0 +1,17 @@ +// Copyright 2021 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. + +//go:build go1.16 +// +build go1.16 + +package capabilities + +import ( + v1 "github.com/open-policy-agent/opa/v1/capabilities" +) + +// FS contains the embedded capabilities/ directory of the built version, +// which has all the capabilities of previous versions: +// "v0.18.0.json" contains the capabilities JSON of version v0.18.0, etc +var FS = v1.FS diff --git a/capabilities/doc.go b/capabilities/doc.go new file mode 100644 index 0000000000..189c2e727a --- /dev/null +++ b/capabilities/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package capabilities diff --git a/cmd/bench_test.go b/cmd/bench_test.go index d6fe21e0e8..9dbb9818a7 100644 --- a/cmd/bench_test.go +++ b/cmd/bench_test.go @@ -776,6 +776,102 @@ func TestBenchMainBadQueryE2E(t *testing.T) { } } +func TestBenchMain_DefaultRegoVersion(t *testing.T) { + t.Parallel() + + tests := []struct { + note string + module string + query string + expErrs []string + }{ + // These tests are slow, so we're not being completely exhaustive here. + { + note: "v0 module", + module: `package test +a[x] { + x := 42 +}`, + query: `data.test.a`, + expErrs: []string{ + "mod.rego:2: rego_parse_error: `if` keyword is required before rule body", + "mod.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + module: `package test +a contains x if { + x := 42 +}`, + query: `data.test.a`, + }, + } + + modes := []struct { + name string + e2e bool + }{ + { + name: "run", + }, + { + name: "e2e", + e2e: true, + }, + } + + for _, mode := range modes { + for _, tc := range tests { + t.Run(fmt.Sprintf("%s, %s", tc.note, mode.name), func(t *testing.T) { + t.Parallel() + + files := map[string]string{ + "mod.rego": tc.module, + } + + test.WithTempFS(files, func(path string) { + params := testBenchParams() + _ = params.outputFormat.Set(evalPrettyOutput) + params.e2e = mode.e2e + + for n := range files { + err := params.dataPaths.Set(filepath.Join(path, n)) + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + } + + args := []string{tc.query} + + var buf bytes.Buffer + rc, err := benchMain(args, params, &buf, &goBenchRunner{}) + + if len(tc.expErrs) > 0 { + if rc == 0 { + t.Fatalf("Expected non-zero return code") + } + + output := buf.String() + for _, expErr := range tc.expErrs { + if !strings.Contains(output, expErr) { + t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, output) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + if rc != 0 { + t.Fatalf("Unexpected return code %d, expected 0", rc) + } + } + }) + }) + } + } +} + func TestBenchMainCompatibleFlags(t *testing.T) { t.Parallel() diff --git a/cmd/build_test.go b/cmd/build_test.go index fb7690b518..f6361a1f95 100644 --- a/cmd/build_test.go +++ b/cmd/build_test.go @@ -225,7 +225,7 @@ func TestBuildErrorDoesNotWriteFile(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 + p if { p } `, } @@ -252,7 +252,7 @@ func TestBuildErrorVerifyNonBundle(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 + p if { p } `, } @@ -335,7 +335,6 @@ func TestBuildPlanWithPruneUnused(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 p contains 1 @@ -404,7 +403,6 @@ func TestBuildPlanWithPrintStatements(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 p if { print("hello") } `, @@ -482,7 +480,6 @@ func TestBuildPlanWithRegoEntrypointAnnotations(t *testing.T) { # METADATA # entrypoint: true package test -import rego.v1 p contains 1 @@ -513,8 +510,6 @@ p[x] { "test.rego": ` package test -import future.keywords - # METADATA # entrypoint: true p contains x if { @@ -546,8 +541,6 @@ p[i] := x { "test.rego": ` package test -import future.keywords - # METADATA # entrypoint: true p[i] if { @@ -577,7 +570,6 @@ p.a.b if { files: map[string]string{ "test.rego": ` package test -import rego.v1 # METADATA # entrypoint: true @@ -593,7 +585,6 @@ p.a.b[i] := x if { files: map[string]string{ "test.rego": ` package test -import rego.v1 p contains 1 @@ -1729,6 +1720,123 @@ q contains 1 if { } } +func TestBuild_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + files map[string]string + expFiles map[string]string + expErrs []string + }{ + { + note: "v0 module", + files: map[string]string{ + "test.rego": `package test + p[x] { + x := 42 + }`, + }, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + files: map[string]string{ + "test.rego": `package test + + p contains x if { + x := 42 + }`, + }, + expFiles: map[string]string{ + ".manifest": `{"revision":"","roots":[""],"rego_version":1} +`, + "test.rego": `package test + +p contains x if { + x := 42 +} +`, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + test.WithTempFS(tc.files, func(root string) { + params := newBuildParams() + params.outputFile = path.Join(root, "bundle.tar.gz") + + err := dobuild(params, []string{root}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatal("expected error but got nil") + } + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error:\n\n%v\n\ngot:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatal(err) + } + + fl := loader.NewFileLoader() + _, err = fl.AsBundle(params.outputFile) + if err != nil { + t.Fatal(err) + } + + // Check that manifest is not written given no input manifest and no other flags + f, err := os.Open(params.outputFile) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + gr, err := gzip.NewReader(f) + if err != nil { + t.Fatal(err) + } + + tr := tar.NewReader(gr) + + foundFiles := map[string]struct{}{} + for { + f, err := tr.Next() + if err == io.EOF { + break + } else if err != nil { + t.Fatal(err) + } + foundFiles[path.Base(f.Name)] = struct{}{} + expectedFile := tc.expFiles[path.Base(f.Name)] + if expectedFile != "" { + data, err := io.ReadAll(tr) + if err != nil { + t.Fatal(err) + } + actualFile := string(data) + if actualFile != expectedFile { + t.Fatalf("expected file %s to be:\n\n%v\n\ngot:\n\n%v", f.Name, expectedFile, actualFile) + } + } + } + + for expectedFile := range tc.expFiles { + if _, ok := foundFiles[expectedFile]; !ok { + t.Fatalf("expected file %s not found in bundle, got: %v", expectedFile, foundFiles) + } + } + } + }) + }) + } +} + func TestBuildWithCompatibleFlags(t *testing.T) { tests := []struct { note string diff --git a/cmd/check_test.go b/cmd/check_test.go index ecd6cab1a1..d851e93240 100644 --- a/cmd/check_test.go +++ b/cmd/check_test.go @@ -407,6 +407,60 @@ p contains x if { } } +func TestCheck_DefaultRegoVersion(t *testing.T) { + cases := []struct { + note string + policy string + expErrs []string + }{ + { + note: "v0 module", + policy: `package test +a[x] { + x := 42 +}`, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + policy: `package test +a contains x if { + x := 42 +}`, + }, + } + + for _, tc := range cases { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.policy, + } + + test.WithTempFS(files, func(root string) { + params := newCheckParams() + + err := checkModules(params, []string{root}) + switch { + case err != nil && len(tc.expErrs) > 0: + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected err:\n\n%v\n\ngot:\n\n%v", expErr, err) + } + } + return // don't read back bundle below + case err != nil && len(tc.expErrs) == 0: + t.Fatalf("unexpected error: %v", err) + case err == nil && len(tc.expErrs) > 0: + t.Fatalf("expected error:\n\n%v\n\ngot: none", tc.expErrs) + } + }) + }) + } +} + func TestCheckCompatibleFlags(t *testing.T) { cases := []struct { note string diff --git a/cmd/deps_test.go b/cmd/deps_test.go index 2a276a008b..4335f34a73 100644 --- a/cmd/deps_test.go +++ b/cmd/deps_test.go @@ -16,6 +16,70 @@ import ( "github.com/open-policy-agent/opa/v1/util/test" ) +func TestDeps_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + query string + expErrs []string + }{ + { + note: "v0 module", + module: `package test +a[x] { + x := 42 +}`, + query: `data.test.p`, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + module: `package test +a contains x if { + x := 42 +}`, + query: `data.test.a`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + test.WithTempFS(files, func(rootPath string) { + params := newDepsCommandParams() + _ = params.outputFormat.Set(depsFormatPretty) + + for f := range files { + _ = params.dataPaths.Set(filepath.Join(rootPath, f)) + } + + err := deps([]string{tc.query}, params, io.Discard) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error but got nil") + } + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error()) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + }) + } +} + func TestDepsCompatibleFlags(t *testing.T) { tests := []struct { note string diff --git a/cmd/doc.go b/cmd/doc.go index 387bdff80a..34ad90adef 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -2,5 +2,4 @@ // Use of this source code is governed by an Apache2 // license that can be found in the LICENSE file. -// Package cmd contains the entry points for OPA commands. package cmd diff --git a/cmd/eval_test.go b/cmd/eval_test.go index 9200107f98..86826fb3cc 100755 --- a/cmd/eval_test.go +++ b/cmd/eval_test.go @@ -112,7 +112,6 @@ func TestEvalExitCode(t *testing.T) { func TestEvalWithShowBuiltinErrors(t *testing.T) { files := map[string]string{ "x.rego": `package x -import rego.v1 p if { 1/0 @@ -182,7 +181,6 @@ q if { func TestEvalWithProfiler(t *testing.T) { files := map[string]string{ "x.rego": `package x -import rego.v1 p if { a := 1 @@ -218,7 +216,7 @@ p if { expectedNumEval := []int{3, 1, 1, 1, 1} expectedNumRedo := []int{3, 1, 1, 1, 1} - expectedRow := []int{8, 7, 6, 5, 1} + expectedRow := []int{7, 6, 5, 4, 1} expectedNumGenExpr := []int{3, 1, 1, 1, 1} for idx, actualExprStat := range output.Profile { @@ -322,7 +320,7 @@ func TestEvalWithOptimize(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 + default p = false p if { q } q if { input.x = data.foo }`, @@ -353,7 +351,6 @@ func TestEvalIssue5368(t *testing.T) { files := map[string]string{ "test.rego": ` package system -import rego.v1 object_key_exists(object, key) if { _ = object[key] @@ -392,7 +389,7 @@ func TestEvalWithOptimizeBundleData(t *testing.T) { files := map[string]string{ "test.rego": ` package test - import rego.v1 + default p = false p if { q } q if { input.x = data.foo }`, @@ -567,7 +564,6 @@ func testEvalWithSchemasAnnotationButNoSchemaFlag(policy string) error { func TestEvalWithSchemasAnnotationButNoSchemaFlag(t *testing.T) { policyWithSchemaRef := ` package test -import rego.v1 # METADATA # schemas: @@ -584,7 +580,6 @@ p if { policyWithInlinedSchema := ` package test -import rego.v1 # METADATA # schemas: @@ -730,7 +725,6 @@ func TestEvalWithJSONSchema(t *testing.T) { policyWithSchemasAnnotation := ` package test -import rego.v1 # METADATA # schemas: @@ -745,7 +739,6 @@ p if { policyWithInlinedSchemasAnnotation := ` package test -import rego.v1 # METADATA # schemas: @@ -811,7 +804,6 @@ func TestEvalWithSchemaFileWithRemoteRef(t *testing.T) { "input.json": input, "schema.json": fmt.Sprintf(schemaFmt, ts.URL), "p.rego": `package p -import rego.v1 r if { input.metadata.clusterName == "NAME" @@ -992,7 +984,6 @@ func TestEvalWithRegoEntrypointAnnotations(t *testing.T) { files := map[string]string{ "test.rego": ` package test -import rego.v1 default p = false # METADATA @@ -1220,7 +1211,6 @@ func TestEvalDebugTraceJSONOutput(t *testing.T) { params.disableIndexing = true mod := `package x - import rego.v1 p contains a if { a := input.z @@ -1295,15 +1285,15 @@ func TestEvalDebugTraceJSONOutput(t *testing.T) { expectedEvalLocationsAndVars := []locationAndVars{ { - location: ast.NewLocation(nil, policyFile, 5, 3), // a := input.z + location: ast.NewLocation(nil, policyFile, 4, 3), // a := input.z varBindings: map[string]string{"__local0__": "a"}, }, { - location: ast.NewLocation(nil, policyFile, 6, 3), // a == 1 + location: ast.NewLocation(nil, policyFile, 5, 3), // a == 1 varBindings: map[string]string{"__local0__": "a"}, }, { - location: ast.NewLocation(nil, policyFile, 10, 3), // b := input.y + location: ast.NewLocation(nil, policyFile, 9, 3), // b := input.y varBindings: map[string]string{"__local1__": "b"}, }, } @@ -1792,7 +1782,6 @@ func TestResetExprLocations(t *testing.T) { // and exprs with no location information. pq, err := rego.New(rego.Query("data.test.p = x"), rego.Module("test.rego", ` package test - import rego.v1 default p = false @@ -2134,21 +2123,12 @@ func TestEvalDiscardProfilerOutput(t *testing.T) { func TestPolicyWithStrictFlag(t *testing.T) { testsShouldError := []struct { note string + v0Compatible bool policy string query string expectedCode string expectedMessage string }{ - { - note: "strict mode should error on duplicate imports", - policy: `package x - import data.bar - import data.bar - foo = bar`, - query: "data.foo", - expectedCode: "rego_compile_error", - expectedMessage: "import must not shadow import data.bar", - }, { note: "strict mode should error on unused imports", policy: `package x @@ -2160,10 +2140,32 @@ func TestPolicyWithStrictFlag(t *testing.T) { expectedMessage: "import data.foo unused", }, { - note: "strict mode should error when reserved vars data or input is used", + note: "v0 compat, strict mode should error on duplicate imports", + v0Compatible: true, + policy: `package x + import data.bar + import data.bar + foo = bar`, + query: "data.foo", + expectedCode: "rego_compile_error", + expectedMessage: "import must not shadow import data.bar", + }, + { + note: "v0 compat, strict mode should error on unused imports", + v0Compatible: true, policy: `package x import future.keywords.if - data if { x = 1}`, + import data.foo + foo = 2`, + query: "data.foo", + expectedCode: "rego_compile_error", + expectedMessage: "import data.foo unused", + }, + { + note: "v0 compat, strict mode should error when reserved vars data or input is used", + v0Compatible: true, + policy: `package x + data { x = 1}`, query: "data.foo", expectedCode: "rego_compile_error", expectedMessage: "rules must not shadow data (use a different rule name)", @@ -2178,26 +2180,38 @@ func TestPolicyWithStrictFlag(t *testing.T) { } test.WithTempFS(files, func(path string) { - params := newEvalCommandParams() - params.strict = true + for _, strict := range []bool{true, false} { + params := newEvalCommandParams() + params.strict = strict + params.v0Compatible = tc.v0Compatible - _ = params.dataPaths.Set(filepath.Join(path, "test.rego")) + _ = params.dataPaths.Set(filepath.Join(path, "test.rego")) - var buf bytes.Buffer - _, err := eval([]string{tc.query}, params, &buf) - if err == nil { - t.Fatal("expected error, got nil") - } - var output presentation.Output - if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { - t.Fatal(err) - } + var buf bytes.Buffer + _, err := eval([]string{tc.query}, params, &buf) - if code := output.Errors[0].Code; code != tc.expectedCode { - t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code) - } - if msg := output.Errors[0].Message; msg != tc.expectedMessage { - t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg) + if strict { + if err == nil { + t.Fatal("expected error, got nil") + } + var output presentation.Output + if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { + t.Fatal(err) + } + + if code := output.Errors[0].Code; code != tc.expectedCode { + t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code) + } + if msg := output.Errors[0].Message; msg != tc.expectedMessage { + t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg) + } + } else if err != nil { + var output presentation.Output + if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { + t.Fatal(err) + } + t.Fatal("unexpected error when non-strict:", output) + } } }) }) @@ -2248,13 +2262,24 @@ func TestPolicyWithStrictFlag(t *testing.T) { func TestBundleWithStrictFlag(t *testing.T) { testsShouldError := []struct { note string + v0Compatible bool policy string query string expectedCode string expectedMessage string }{ { - note: "strict mode should error on duplicate imports in this bundle", + note: "strict mode should error on unused imports in this bundle", + policy: `package x + import data.foo + foo = 2`, + query: "data.foo", + expectedCode: "rego_compile_error", + expectedMessage: "import data.foo unused", + }, + { + note: "v0 compat, strict mode should error on duplicate imports in this bundle", + v0Compatible: true, policy: `package x import data.bar import data.bar @@ -2264,9 +2289,9 @@ func TestBundleWithStrictFlag(t *testing.T) { expectedMessage: "import must not shadow import data.bar", }, { - note: "strict mode should error on unused imports in this bundle", + note: "v0 compat, strict mode should error on unused imports in this bundle", + v0Compatible: true, policy: `package x - import future.keywords.if import data.foo foo = 2`, query: "data.foo", @@ -2274,10 +2299,10 @@ func TestBundleWithStrictFlag(t *testing.T) { expectedMessage: "import data.foo unused", }, { - note: "strict mode should error when reserved vars data or input is used in this bundle", + note: "v0 compat, strict mode should error when reserved vars data or input is used in this bundle", + v0Compatible: true, policy: `package x - import future.keywords.if - data if { x = 1}`, + data { x = 1}`, query: "data.foo", expectedCode: "rego_compile_error", expectedMessage: "rules must not shadow data (use a different rule name)", @@ -2292,27 +2317,39 @@ func TestBundleWithStrictFlag(t *testing.T) { } test.WithTempFS(files, func(path string) { - params := newEvalCommandParams() - if err := params.bundlePaths.Set(path); err != nil { - t.Fatal(err) - } - params.strict = true + for _, strict := range []bool{true, false} { + params := newEvalCommandParams() + if err := params.bundlePaths.Set(path); err != nil { + t.Fatal(err) + } + params.strict = strict + params.v0Compatible = tc.v0Compatible - var buf bytes.Buffer - _, err := eval([]string{tc.query}, params, &buf) - if err == nil { - t.Fatal("expected error, got nil") - } - var output presentation.Output - if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { - t.Fatal(err) - } + var buf bytes.Buffer + _, err := eval([]string{tc.query}, params, &buf) - if code := output.Errors[0].Code; code != tc.expectedCode { - t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code) - } - if msg := output.Errors[0].Message; msg != tc.expectedMessage { - t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg) + if strict { + if err == nil { + t.Fatal("expected error, got nil") + } + var output presentation.Output + if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { + t.Fatal(err) + } + + if code := output.Errors[0].Code; code != tc.expectedCode { + t.Errorf("expected code '%v', got '%v'", tc.expectedCode, code) + } + if msg := output.Errors[0].Message; msg != tc.expectedMessage { + t.Errorf("expected message '%v', got '%v'", tc.expectedMessage, msg) + } + } else if err != nil { + var output presentation.Output + if err := util.NewJSONDecoder(&buf).Decode(&output); err != nil { + t.Fatal(err) + } + t.Fatal("unexpected error when non-strict:", output) + } } }) }) @@ -2366,7 +2403,7 @@ func TestBundleWithStrictFlag(t *testing.T) { func TestIfElseIfElseNoBrace(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + p if false else := 1 if false else := 2`, @@ -2391,7 +2428,7 @@ func TestIfElseIfElseNoBrace(t *testing.T) { func TestIfElseIfElseBrace(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + p if false else := 1 if { false } else := 2`, @@ -2416,7 +2453,7 @@ func TestIfElseIfElseBrace(t *testing.T) { func TestIfElse(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + p if false else := 1 `, } @@ -2468,7 +2505,7 @@ func TestElseNoIfV0(t *testing.T) { func TestElseIf(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + p if false else := x if { x=2 @@ -2525,7 +2562,7 @@ func TestElseIfElseV0(t *testing.T) { func TestUnexpectedElseIfElseErr(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + p if false else := x if { x=2 @@ -2563,7 +2600,7 @@ func TestUnexpectedElseIfElseErr(t *testing.T) { func TestUnexpectedElseIfErr(t *testing.T) { files := map[string]string{ "bug.rego": `package bug - import future.keywords.if + q := 1 if false else := 2 if `, @@ -2594,6 +2631,95 @@ func TestUnexpectedElseIfErr(t *testing.T) { }) } +func TestEval_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]string + query string + expErrs []string + }{ + { + note: "v0 module", + modules: map[string]string{ + "test.rego": `package test +a[x] { + x := 42 +}`, + }, + query: `data.test.a`, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + modules: map[string]string{ + "test.rego": `package test +a contains x if { + x := 42 +}`, + }, + query: `data.test.a`, + }, + } + + setup := []struct { + name string + commandParams func(params *evalCommandParams, path string) + }{ + { + name: "Files", + commandParams: func(params *evalCommandParams, path string) { + params.dataPaths = newrepeatedStringFlag([]string{path}) + }, + }, + { + name: "Bundle", + commandParams: func(params *evalCommandParams, path string) { + if err := params.bundlePaths.Set(path); err != nil { + t.Fatal(err) + } + }, + }, + } + + for _, s := range setup { + for _, tc := range tests { + t.Run(fmt.Sprintf("%s: %s", s.name, tc.note), func(t *testing.T) { + test.WithTempFS(tc.modules, func(path string) { + params := newEvalCommandParams() + _ = params.outputFormat.Set(evalPrettyOutput) + s.commandParams(¶ms, path) + + var buf bytes.Buffer + + defined, err := eval([]string{tc.query}, params, &buf) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatal("expected error, got none") + } + + actual := buf.String() + for _, expErr := range tc.expErrs { + if !strings.Contains(actual, expErr) { + t.Fatalf("expected error:\n\n%v\n\ngot\n\n%v", expErr, actual) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v, buf: %s", err, buf.String()) + } else if !defined { + t.Fatal("expected result to be defined") + } + } + }) + }) + } + } +} + func TestEvalPolicyWithCompatibleFlags(t *testing.T) { tests := []struct { note string diff --git a/cmd/exec_test.go b/cmd/exec_test.go index 5897634f79..d09f86441f 100644 --- a/cmd/exec_test.go +++ b/cmd/exec_test.go @@ -87,7 +87,7 @@ func TestExecDecisionOption(t *testing.T) { s := sdk_test.MustNewServer(sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{ "test.rego": ` package foo - import rego.v1 + main contains "hello" `, })) @@ -129,7 +129,7 @@ func TestExecBundleFlag(t *testing.T) { files := map[string]string{ "files/test.json": `{"foo": 7}`, "bundle/x.rego": `package system - import rego.v1 + main contains "hello"`, } @@ -160,6 +160,128 @@ func TestExecBundleFlag(t *testing.T) { }) } +func TestExec_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0, module", + module: `package system +main["hello"] { + input.foo == "bar" +}`, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + module: `package system +main contains "hello" if { + input.foo == "bar" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.json": `{"foo": "bar"}`, + } + + test.WithTempFS(files, func(dir string) { + s := sdk_test.MustNewServer( + sdk_test.MockBundle("/bundles/bundle.tar.gz", map[string]string{"test.rego": tc.module}), + sdk_test.RawBundles(true), + ) + + defer s.Stop() + + var buf bytes.Buffer + params := exec.NewParams(&buf) + _ = params.OutputFormat.Set("json") + params.ConfigOverrides = []string{ + "services.test.url=" + s.URL(), + "bundles.test.resource=/bundles/bundle.tar.gz", + } + + params.Paths = append(params.Paths, dir) + + if len(tc.expErrs) > 0 { + testLogger := loggingtest.New() + params.Logger = testLogger + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + go func(expectedErrors []string) { + err := runExecWithContext(ctx, params) + // Note(philipc): Catch the expected cancellation + // errors, allowing unexpected test failures through. + if err != context.Canceled { + var errs ast.Errors + if errors.As(err, &errs) { + for _, expErr := range expectedErrors { + found := false + for _, e := range errs { + if strings.Contains(e.Error(), expErr) { + found = true + break + } + } + if !found { + t.Errorf("Could not find expected error: %s in %v", expErr, errs) + return + } + } + } else { + t.Error(err) + return + } + } + }(tc.expErrs) + + if !test.Eventually(t, 5*time.Second, func() bool { + for _, expErr := range tc.expErrs { + found := false + for _, e := range testLogger.Entries() { + if strings.Contains(e.Message, expErr) { + found = true + break + } + } + if !found { + return false + } + } + return true + }) { + t.Fatalf("timed out waiting for logged errors:\n\n%v\n\ngot\n\n%v:", tc.expErrs, testLogger.Entries()) + } + } else { + err := runExec(params) + if err != nil { + t.Fatal(err) + } + + output := util.MustUnmarshalJSON(bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil)) + + exp := util.MustUnmarshalJSON([]byte(`{"result": [{ + "path": "/test.json", + "result": ["hello"] + }]}`)) + + if !reflect.DeepEqual(output, exp) { + t.Fatal("Expected:", exp, "Got:", output) + } + } + }) + }) + } +} + func TestExecCompatibleFlags(t *testing.T) { tests := []struct { note string diff --git a/cmd/flags.go b/cmd/flags.go index 45af2ce67f..ad34d3656d 100644 --- a/cmd/flags.go +++ b/cmd/flags.go @@ -163,6 +163,7 @@ func addV0CompatibleFlag(fs *pflag.FlagSet, v1Compatible *bool, value bool) { func addV1CompatibleFlag(fs *pflag.FlagSet, v1Compatible *bool, value bool) { fs.BoolVar(v1Compatible, "v1-compatible", value, "opt-in to OPA features and behaviors that are enabled by default in OPA v1.0") + _ = fs.MarkHidden("v1-compatible") } func addReadAstValuesFromStoreFlag(fs *pflag.FlagSet, readAstValuesFromStore *bool, value bool) { diff --git a/cmd/fmt_test.go b/cmd/fmt_test.go index 3aa595c22f..f6b67682e0 100644 --- a/cmd/fmt_test.go +++ b/cmd/fmt_test.go @@ -118,13 +118,13 @@ func TestFmtFormatFile(t *testing.T) { }, { note: "v1", - params: fmtCommandParams{v1Compatible: true}, + params: fmtCommandParams{}, unformatted: unformattedV1, formatted: formattedV1, }, { note: "comment in comprehension", - params: fmtCommandParams{v1Compatible: true}, + params: fmtCommandParams{}, unformatted: ComprehensionCommentShouldNotMoveUnformatted, formatted: ComprehensionCommentShouldNotMoveFormatted, }, @@ -198,7 +198,7 @@ func TestFmtFormatFileNoChanges(t *testing.T) { }, { note: "v1", - params: fmtCommandParams{v1Compatible: true}, + params: fmtCommandParams{}, module: formattedV1, }, } @@ -246,9 +246,8 @@ func TestFmtFailFormatFileNoChanges(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - fail: true, - diff: true, + fail: true, + diff: true, }, module: formattedV1, }, @@ -296,8 +295,7 @@ func TestFmtFormatFileDiff(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - diff: true, + diff: true, }, module: formattedV1, }, @@ -346,8 +344,7 @@ func TestFmtFormatFileFailToPrintDiff(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - diff: true, + diff: true, }, module: unformattedV1, }, @@ -397,8 +394,7 @@ func TestFmtFormatFileList(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - list: true, + list: true, }, module: formattedV1, }, @@ -448,9 +444,8 @@ func TestFmtFailFormatFileList(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - fail: true, - list: true, + fail: true, + list: true, }, module: formattedV1, }, @@ -499,9 +494,8 @@ func TestFmtFailFormatFileChangesList(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - fail: true, - list: true, + fail: true, + list: true, }, module: unformattedV1, }, @@ -549,8 +543,7 @@ func TestFmtFailFileNoChanges(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - fail: true, + fail: true, }, module: formattedV1, }, @@ -591,8 +584,7 @@ func TestFmtFailFileChanges(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - fail: true, + fail: true, }, module: unformattedV1, }, @@ -634,9 +626,8 @@ func TestFmtFailFileChangesDiff(t *testing.T) { { note: "v1", params: fmtCommandParams{ - v1Compatible: true, - diff: true, - fail: true, + diff: true, + fail: true, }, module: unformattedV1, }, @@ -894,7 +885,7 @@ q := all([true, false]) } } -func TestFmtV1Compatible(t *testing.T) { +func TestFmt_DefaultRegoVersion(t *testing.T) { tests := []struct { note string input string @@ -1006,9 +997,7 @@ q := all([true, false]) for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { - params := fmtCommandParams{ - v1Compatible: true, - } + params := fmtCommandParams{} files := map[string]string{ "policy.rego": tc.input, diff --git a/cmd/inspect.go b/cmd/inspect.go index 6859f466e9..cd5125dde3 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -31,14 +31,18 @@ const pageWidth = 80 type inspectCommandParams struct { outputFormat *util.EnumFlag listAnnotations bool + v0Compatible bool v1Compatible bool } func (p *inspectCommandParams) regoVersion() ast.RegoVersion { + if p.v0Compatible { + return ast.RegoV0 + } if p.v1Compatible { return ast.RegoV1 } - return ast.RegoV0 + return ast.DefaultRegoVersion } func newInspectCommandParams() inspectCommandParams { @@ -98,6 +102,7 @@ that file and summarize its structure and contents. addOutputFormat(inspectCommand.Flags(), params.outputFormat) addListAnnotations(inspectCommand.Flags(), ¶ms.listAnnotations) + addV0CompatibleFlag(inspectCommand.Flags(), ¶ms.v0Compatible, false) addV1CompatibleFlag(inspectCommand.Flags(), ¶ms.v1Compatible, false) RootCommand.AddCommand(inspectCommand) } diff --git a/cmd/inspect_test.go b/cmd/inspect_test.go index 51d941a466..4c23f0d5a9 100644 --- a/cmd/inspect_test.go +++ b/cmd/inspect_test.go @@ -565,22 +565,24 @@ Custom: }) } -func TestDoInspectV1Compatible(t *testing.T) { +func TestDoInspect_V0Compatible(t *testing.T) { tests := []struct { note string - v1Compatible bool + v0Compatible bool module string expErrs []string }{ { - note: "v0.x, keywords not used", + note: "v0, keywords not used", + v0Compatible: true, module: `package test p[v] { v := input.x }`, }, { - note: "v0.x, no keywords imported, but used", + note: "v0, no keywords imported, but used", + v0Compatible: true, module: `package test p contains v if { v := input.x @@ -590,7 +592,7 @@ p contains v if { }, }, { - note: "v0.x, keywords imported", + note: "v0, keywords imported", module: `package test import future.keywords p contains v if { @@ -598,7 +600,7 @@ p contains v if { }`, }, { - note: "v0.x, rego.v1 imported", + note: "v0, rego.v1 imported", module: `package test import rego.v1 p contains v if { @@ -606,8 +608,7 @@ p contains v if { }`, }, { - note: "v1.0, keywords not used", - v1Compatible: true, + note: "v1, keywords not used", module: `package test p[v] { v := input.x @@ -618,16 +619,14 @@ p[v] { }, }, { - note: "v1.0, no keywords imported", - v1Compatible: true, + note: "v1, no keywords imported", module: `package test p contains v if { v := input.x }`, }, { - note: "v1.0, keywords imported", - v1Compatible: true, + note: "v1, keywords imported", module: `package test import future.keywords p contains v if { @@ -635,8 +634,7 @@ p contains v if { }`, }, { - note: "v1.0, rego.v1 imported", - v1Compatible: true, + note: "v1, rego.v1 imported", module: `package test import rego.v1 p contains v if { @@ -664,7 +662,7 @@ p contains v if { var out bytes.Buffer params := newInspectCommandParams() - params.v1Compatible = tc.v1Compatible + params.v0Compatible = tc.v0Compatible err = params.outputFormat.Set(evalJSONOutput) if err != nil { t.Fatalf("Unexpected error: %s", err) @@ -1038,7 +1036,7 @@ func TestUnknownRefs(t *testing.T) { files: [][2]string{ { "/policy.rego", `package test -p { +p if { foo.bar(42) contains("foo", "o") }`, @@ -1424,7 +1422,7 @@ func TestCallToUnknownRegoFunction(t *testing.T) { {"/policy.rego", `package test import data.x.y -p { +p if { y(1) == true } `}, diff --git a/cmd/oracle.go b/cmd/oracle.go index f83aea427d..d9772103aa 100644 --- a/cmd/oracle.go +++ b/cmd/oracle.go @@ -162,6 +162,8 @@ func dofindDefinition(params findDefinitionParams, stdin io.Reader, stdout io.Wr } } + // FindDefinition() will instantiate a new compiler, but we don't need to set the + // default rego-version because the passed modules already have the rego-version from parsing. result, err := oracle.New().FindDefinition(oracle.DefinitionQuery{ Buffer: bs, Filename: filename, diff --git a/cmd/oracle_test.go b/cmd/oracle_test.go index 0682800353..a17e96528f 100644 --- a/cmd/oracle_test.go +++ b/cmd/oracle_test.go @@ -17,7 +17,6 @@ func TestOracleFindDefinition(t *testing.T) { cases := []struct { note string v0Compatible bool - v1Compatible bool onDiskModule string stdin string paths []string @@ -42,8 +41,7 @@ q = true`, }, }, { - note: "v1", - v1Compatible: true, + note: "v1", onDiskModule: `package test p if { r } @@ -60,27 +58,6 @@ q = true`, "test.rego:21", }, }, - // v0 takes precedence over v1 - { - note: "v0+v1", - v0Compatible: true, - v1Compatible: true, - onDiskModule: `package test - -p { r } - -r = true`, - stdin: `package test - -p { q } - -q = true`, - paths: []string{ - "test.rego:10", - "test.rego:15", - "test.rego:18", - }, - }, } for _, tc := range cases { @@ -102,7 +79,6 @@ q = true`, }, stdinBuffer: true, v0Compatible: tc.v0Compatible, - v1Compatible: tc.v1Compatible, } stdout := bytes.NewBuffer(nil) diff --git a/cmd/parse_test.go b/cmd/parse_test.go index b0b5037fbb..44a78e38b6 100644 --- a/cmd/parse_test.go +++ b/cmd/parse_test.go @@ -972,6 +972,58 @@ func TestParseJSONOutputComments(t *testing.T) { } } +func TestParse_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0 module", + module: `package test +a[x] { + x := 42 +}`, + expErrs: []string{ + "`if` keyword is required before rule body", + "`contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 module", + module: `package test +a contains x if { + x := 42 +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + _, _, stderr, _ := testParse(t, files, &parseParams{ + format: util.NewEnumFlag(parseFormatPretty, []string{parseFormatPretty, parseFormatJSON}), + }) + + if len(tc.expErrs) > 0 { + errs := string(stderr) + for _, expErr := range tc.expErrs { + if !strings.Contains(errs, expErr) { + t.Fatalf("Expected error:\n\n%q\n\ngot:\n\n%s", expErr, errs) + } + } + } else { + if len(stderr) > 0 { + t.Fatalf("Expected no stderr output, got:\n%s\n", string(stderr)) + } + } + }) + } +} + func TestParseCompatibleFlags(t *testing.T) { tests := []struct { note string diff --git a/cmd/refactor.go b/cmd/refactor.go index 76d586980b..b92621b49f 100644 --- a/cmd/refactor.go +++ b/cmd/refactor.go @@ -149,7 +149,7 @@ func doMove(params moveCommandParams, args []string, out io.Writer) error { return err } - formatted, err := format.Ast(mod) + formatted, err := format.AstWithOpts(mod, format.Opts{RegoVersion: params.regoVersion()}) if err != nil { return newError("failed to parse Rego source file: %v", err) } diff --git a/cmd/refactor_test.go b/cmd/refactor_test.go index 353e53fc44..c8e8377b71 100644 --- a/cmd/refactor_test.go +++ b/cmd/refactor_test.go @@ -16,7 +16,6 @@ func TestDoMoveRenamePackage(t *testing.T) { cases := []struct { note string v0Compatible bool - v1Compatible bool module string expected *ast.Module }{ @@ -41,8 +40,7 @@ func TestDoMoveRenamePackage(t *testing.T) { }`, ast.ParserOptions{RegoVersion: ast.RegoV0}), }, { - note: "v1", - v1Compatible: true, + note: "v1", module: `package lib.foo # this is a comment @@ -60,28 +58,6 @@ func TestDoMoveRenamePackage(t *testing.T) { input.message == "hello" # this is a comment too }`, ast.ParserOptions{RegoVersion: ast.RegoV1}), }, - // v0 takes precedence over v1 - { - note: "v0+v1", - v0Compatible: true, - v1Compatible: true, - module: `package lib.foo - - # this is a comment - default allow = false - - allow { - input.message == "hello" # this is a comment too - }`, - expected: ast.MustParseModuleWithOpts(`package baz.bar - - # this is a comment - default allow = false - - allow { - input.message == "hello" # this is a comment too - }`, ast.ParserOptions{RegoVersion: ast.RegoV0}), - }, } for _, tc := range cases { @@ -97,7 +73,6 @@ func TestDoMoveRenamePackage(t *testing.T) { params := moveCommandParams{ mapping: newrepeatedStringFlag(mappings), v0Compatible: tc.v0Compatible, - v1Compatible: tc.v1Compatible, } var buf bytes.Buffer @@ -107,7 +82,12 @@ func TestDoMoveRenamePackage(t *testing.T) { t.Fatal(err) } - formatted := format.MustAst(tc.expected) + var formatted []byte + if tc.v0Compatible { + formatted = format.MustAstWithOpts(tc.expected, format.Opts{RegoVersion: ast.RegoV0}) + } else { + formatted = format.MustAstWithOpts(tc.expected, format.Opts{RegoVersion: ast.RegoV1}) + } if !reflect.DeepEqual(formatted, buf.Bytes()) { t.Fatalf("Expected module:\n%v\n\nGot:\n%v\n", string(formatted), buf.String()) @@ -121,7 +101,6 @@ func TestDoMoveOverwriteFile(t *testing.T) { cases := []struct { note string v0Compatible bool - v1Compatible bool module string expected *ast.Module }{ @@ -149,8 +128,7 @@ func TestDoMoveOverwriteFile(t *testing.T) { }`, ast.ParserOptions{RegoVersion: ast.RegoV0}), }, { - note: "v1", - v1Compatible: true, + note: "v1", module: `package lib.foo import data.x.q @@ -171,31 +149,6 @@ func TestDoMoveOverwriteFile(t *testing.T) { input.message == "hello" }`, ast.ParserOptions{RegoVersion: ast.RegoV1}), }, - // v0 takes precedence over v1 - { - note: "v0+v1", - v0Compatible: true, - v1Compatible: true, - module: `package lib.foo - - import data.x.q - - default allow := false - - allow { - input.message == "hello" - } - `, - expected: ast.MustParseModuleWithOpts(`package baz.bar - - import data.hidden.q - - default allow := false - - allow { - input.message == "hello" - }`, ast.ParserOptions{RegoVersion: ast.RegoV0}), - }, } for _, tc := range cases { @@ -212,7 +165,6 @@ func TestDoMoveOverwriteFile(t *testing.T) { mapping: newrepeatedStringFlag(mappings), overwrite: true, v0Compatible: tc.v0Compatible, - v1Compatible: tc.v1Compatible, } var buf bytes.Buffer @@ -227,7 +179,12 @@ func TestDoMoveOverwriteFile(t *testing.T) { t.Fatal(err) } - actual := ast.MustParseModule(string(data)) + var actual *ast.Module + if tc.v0Compatible { + actual = ast.MustParseModuleWithOpts(string(data), ast.ParserOptions{RegoVersion: ast.RegoV0}) + } else { + actual = ast.MustParseModuleWithOpts(string(data), ast.ParserOptions{RegoVersion: ast.RegoV1}) + } if !tc.expected.Equal(actual) { t.Fatalf("Expected module:\n%v\n\nGot:\n%v\n", tc.expected, actual) diff --git a/cmd/run_test.go b/cmd/run_test.go index 943261b75e..9caf11b261 100644 --- a/cmd/run_test.go +++ b/cmd/run_test.go @@ -253,49 +253,31 @@ func TestRunServerUploadPolicy(t *testing.T) { tests := []struct { note string v0Compatible bool - v1Compatible bool module string expErr bool }{ { note: "v0-compatible, v0 policy", v0Compatible: true, - v1Compatible: false, module: v0Policy, }, { note: "v0-compatible, v1 policy", v0Compatible: true, - v1Compatible: false, module: v1Policy, expErr: true, }, { - note: "v1-compatible, v0 policy", + note: "v1, v0 policy", v0Compatible: false, - v1Compatible: true, module: v0Policy, expErr: true, }, { - note: "v1-compatible, v1 policy", + note: "v1, v1 policy", v0Compatible: false, - v1Compatible: true, module: v1Policy, }, - { - note: "v0-compatible, v1-compatible, v0 policy", - v0Compatible: true, - v1Compatible: true, - module: v0Policy, - }, - { - note: "v0-compatible, v1-compatible, v1 policy", - v0Compatible: true, - v1Compatible: true, - module: v1Policy, - expErr: true, - }, } for i, tc := range tests { @@ -304,7 +286,6 @@ func TestRunServerUploadPolicy(t *testing.T) { params := newTestRunParams() params.rt.V0Compatible = tc.v0Compatible - params.rt.V1Compatible = tc.v1Compatible rt, err := initRuntime(ctx, params, nil, false) if err != nil { diff --git a/cmd/test.go b/cmd/test.go index 1484e56632..c7db7eae6e 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -18,7 +18,6 @@ import ( "github.com/fsnotify/fsnotify" "github.com/open-policy-agent/opa/internal/pathwatcher" initload "github.com/open-policy-agent/opa/internal/runtime/init" - "github.com/open-policy-agent/opa/v1/loader" "github.com/spf13/cobra" "github.com/open-policy-agent/opa/cmd/internal/env" @@ -27,6 +26,7 @@ import ( "github.com/open-policy-agent/opa/v1/bundle" "github.com/open-policy-agent/opa/v1/compile" "github.com/open-policy-agent/opa/v1/cover" + "github.com/open-policy-agent/opa/v1/loader" "github.com/open-policy-agent/opa/v1/storage" "github.com/open-policy-agent/opa/v1/storage/inmem" "github.com/open-policy-agent/opa/v1/tester" @@ -287,7 +287,7 @@ func processWatcherUpdate(ctx context.Context, testParams testCommandParams, pat var loadResult *initload.LoadPathsResult - err := pathwatcher.ProcessWatcherUpdate(ctx, paths, removed, store, filter.Apply, testParams.bundleMode, + err := pathwatcher.ProcessWatcherUpdateForRegoVersion(ctx, testParams.RegoVersion(), paths, removed, store, filter.Apply, testParams.bundleMode, func(ctx context.Context, txn storage.Transaction, loaded *initload.LoadPathsResult) error { if len(loaded.Files.Documents) > 0 || removed != "" { if err := store.Write(ctx, txn, storage.AddOp, storage.Path{}, loaded.Files.Documents); err != nil { diff --git a/cmd/test_test.go b/cmd/test_test.go index 4233478a8f..ead9b48521 100644 --- a/cmd/test_test.go +++ b/cmd/test_test.go @@ -173,7 +173,6 @@ func failTrace(t *testing.T) []*topdown.Event { t.Helper() mod := ` package testing - import rego.v1 p if { x # Always true @@ -234,7 +233,6 @@ func TestPrettyTraceWithLocalVars(t *testing.T) { includeVars: false, files: map[string]string{ "test.rego": `package test -import rego.v1 test_p if { x := 1 @@ -251,17 +249,17 @@ data.test.test_p: FAIL (%.*%) query:1 %.*% Enter data.test.test_p = _ query:1 %.*% | Eval data.test.test_p = _ query:1 %.*% | Index data.test.test_p (matched 1 rule, early exit) - %.*%/test.rego:4 | Enter data.test.test_p - %.*%/test.rego:5 | | Eval x = 1 - %.*%/test.rego:6 | | Eval y = 2 - %.*%/test.rego:7 | | Eval z = 3 - %.*%/test.rego:8 | | Eval plus(z, y, __local3__) - %.*%/test.rego:8 | | Eval x = __local3__ - %.*%/test.rego:8 | | Fail x = __local3__ - %.*%/test.rego:8 | | Redo plus(z, y, __local3__) - %.*%/test.rego:7 | | Redo z = 3 - %.*%/test.rego:6 | | Redo y = 2 - %.*%/test.rego:5 | | Redo x = 1 + %.*%/test.rego:3 | Enter data.test.test_p + %.*%/test.rego:4 | | Eval x = 1 + %.*%/test.rego:5 | | Eval y = 2 + %.*%/test.rego:6 | | Eval z = 3 + %.*%/test.rego:7 | | Eval plus(z, y, __local3__) + %.*%/test.rego:7 | | Eval x = __local3__ + %.*%/test.rego:7 | | Fail x = __local3__ + %.*%/test.rego:7 | | Redo plus(z, y, __local3__) + %.*%/test.rego:6 | | Redo z = 3 + %.*%/test.rego:5 | | Redo y = 2 + %.*%/test.rego:4 | | Redo x = 1 query:1 %.*% | Fail data.test.test_p = _ SUMMARY @@ -277,7 +275,6 @@ FAIL: 1/1 includeVars: true, files: map[string]string{ "test.rego": `package test -import rego.v1 test_p if { x := 1 @@ -294,20 +291,20 @@ data.test.test_p: FAIL (%.*%) query:1 %.*% Enter data.test.test_p = _ {} query:1 %.*% | Eval data.test.test_p = _ {} query:1 %.*% | Index data.test.test_p (matched 1 rule, early exit) {} - %.*%/test.rego:4 | Enter data.test.test_p {} - %.*%/test.rego:5 | | Eval x = 1 {} - %.*%/test.rego:6 | | Eval y = 2 {} - %.*%/test.rego:7 | | Eval z = 3 {} - %.*%/test.rego:8 | | Eval plus(z, y, __local3__) {y: 2, z: 3} - %.*%/test.rego:8 | | Eval x = __local3__ {__local3__: 5, x: 1} - %.*%/test.rego:8 | | Fail x = __local3__ {__local3__: 5, x: 1} - %.*%/test.rego:8 | | Redo plus(z, y, __local3__) {__local3__: 5, y: 2, z: 3} - %.*%/test.rego:7 | | Redo z = 3 {z: 3} - %.*%/test.rego:6 | | Redo y = 2 {y: 2} - %.*%/test.rego:5 | | Redo x = 1 {x: 1} + %.*%/test.rego:3 | Enter data.test.test_p {} + %.*%/test.rego:4 | | Eval x = 1 {} + %.*%/test.rego:5 | | Eval y = 2 {} + %.*%/test.rego:6 | | Eval z = 3 {} + %.*%/test.rego:7 | | Eval plus(z, y, __local3__) {y: 2, z: 3} + %.*%/test.rego:7 | | Eval x = __local3__ {__local3__: 5, x: 1} + %.*%/test.rego:7 | | Fail x = __local3__ {__local3__: 5, x: 1} + %.*%/test.rego:7 | | Redo plus(z, y, __local3__) {__local3__: 5, y: 2, z: 3} + %.*%/test.rego:6 | | Redo z = 3 {z: 3} + %.*%/test.rego:5 | | Redo y = 2 {y: 2} + %.*%/test.rego:4 | | Redo x = 1 {x: 1} query:1 %.*% | Fail data.test.test_p = _ {} - %.*%/test.rego:8: + %.*%/test.rego:7: x == z + y | | | | | 2 @@ -362,7 +359,6 @@ func TestFailVarValues(t *testing.T) { note: "simple", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 1 @@ -376,7 +372,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: x == y + z | | | | | 3 @@ -396,7 +392,6 @@ FAIL: 1/1 note: "simple (not)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 5 @@ -410,7 +405,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: not x == y + z | | | | | 3 @@ -430,7 +425,6 @@ FAIL: 1/1 note: "array", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 1 @@ -444,7 +438,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: x == y[2] + z | | | | | 3 @@ -465,7 +459,6 @@ FAIL: 1/1 note: "array, var key", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 1 @@ -480,7 +473,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: x == y[i] + z | | | | | | | 3 @@ -502,7 +495,6 @@ FAIL: 1/1 note: "array containing vars", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 1 @@ -516,7 +508,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: [x, y, z] == [4, 5, 6] | | | | | 3 @@ -535,7 +527,6 @@ FAIL: 1/1 note: "array containing refs", files: map[string]string{ "/test.rego": `package test -import rego.v1 a := 1 @@ -551,7 +542,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: [a, data.test.b, data.c] == [4, 5, 6] | | | | | 3 @@ -570,7 +561,6 @@ FAIL: 1/1 note: "array containing refs, undefined", files: map[string]string{ "/test.rego": `package test -import rego.v1 a := 1 @@ -588,7 +578,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: [a, b, data.c] == [4, 5, 6] | | | undefined @@ -606,7 +596,6 @@ FAIL: 1/1 note: "nested collections containing vars", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { x := 1 @@ -620,7 +609,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: [x, {y, {"a": z}}] == [4, {5, {"a": 6}}] | | | | | 3 @@ -639,7 +628,6 @@ FAIL: 1/1 note: "single line expression containing tabs", files: map[string]string{ "/test.rego": `package test - import rego.v1 test_foo if { x := 1 @@ -653,7 +641,7 @@ FAIL: 1/1 -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: x == y + z | | | | | 3 @@ -673,7 +661,6 @@ FAIL: 1/1 note: "single line expression containing tabs #2", files: map[string]string{ "/test.rego": `package test - import rego.v1 test_foo if { x := 1 @@ -687,7 +674,7 @@ FAIL: 1/1 -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: x == y + z | | | | | 3 @@ -707,7 +694,6 @@ FAIL: 1/1 note: "multi-line expression containing tabs", files: map[string]string{ "/test.rego": `package test - import rego.v1 test_foo if { x := 1 @@ -731,7 +717,7 @@ FAIL: 1/1 -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:13: + %ROOT%/test.rego:12: obj == { "foo_": x, "bar__": y, @@ -757,7 +743,6 @@ FAIL: 1/1 note: "composite rule", files: map[string]string{ "/test.rego": `package test -import rego.v1 p contains v if { some v in numbers.range(1, 3) @@ -771,7 +756,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: p == {4, 5, 6} | {1, 2, 3} @@ -788,7 +773,6 @@ FAIL: 1/1 note: "composite rule with ref-head", files: map[string]string{ "/test.rego": `package test -import rego.v1 p.q contains v if { some v in numbers.range(1, 3) @@ -802,7 +786,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: p.q == {4, 5, 6} | {1, 2, 3} @@ -819,7 +803,6 @@ FAIL: 1/1 note: "composite rule with ref-head, partial ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 p.q contains v if { some v in numbers.range(1, 3) @@ -835,7 +818,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: p == { "q": {4, 5, 6} } @@ -854,7 +837,6 @@ FAIL: 1/1 note: "composite rules with ref-head, composite value", files: map[string]string{ "/test.rego": `package test -import rego.v1 p.q contains v if { some v in numbers.range(1, 3) @@ -873,7 +855,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:11: + %ROOT%/test.rego:10: p == { "q": {4, 5, 6}, "r": "bar" @@ -893,7 +875,6 @@ FAIL: 1/1 note: "refs in different compiled sub-expressions", files: map[string]string{ "/test.rego": `package test -import rego.v1 a := 1 b := 2 @@ -909,7 +890,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:10: + %ROOT%/test.rego:9: a == b + c | | | | | 3 @@ -929,7 +910,6 @@ FAIL: 1/1 note: "rule not defined", files: map[string]string{ "/test.rego": `package test -import rego.v1 p if { input.x == 1 @@ -943,7 +923,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: p with input.x as 2 | undefined @@ -960,7 +940,6 @@ FAIL: 1/1 note: "rule defined (not)", files: map[string]string{ "/test.rego": `package test -import rego.v1 p if { input.x == 1 @@ -974,7 +953,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: not p with input.x as 1 | true @@ -991,7 +970,6 @@ FAIL: 1/1 note: "data ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { y := 1 @@ -1004,7 +982,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:6: + %ROOT%/test.rego:5: data.x == y | | | 1 @@ -1022,7 +1000,6 @@ FAIL: 1/1 note: "data + virtual extent ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 foo.x := 1 @@ -1037,7 +1014,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: foo == y | | | {"x": 1, "y": 42} @@ -1055,7 +1032,6 @@ FAIL: 1/1 note: "in (array)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := ["a", "b", "c"] @@ -1068,7 +1044,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:7: + %ROOT%/test.rego:6: x in l | | | ["a", "b", "c"] @@ -1086,7 +1062,6 @@ FAIL: 1/1 note: "in (set)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := {"a", "b", "c"} @@ -1099,7 +1074,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:7: + %ROOT%/test.rego:6: x in l | | | {"a", "b", "c"} @@ -1117,7 +1092,6 @@ FAIL: 1/1 note: "comprehension (array)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := ["a", "b", "c"] @@ -1129,7 +1103,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:6: + %ROOT%/test.rego:5: [x | x := l[_]] == ["d", "e", "f"] | ["a", "b", "c"] @@ -1146,7 +1120,6 @@ FAIL: 1/1 note: "comprehension (set)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := ["a"] @@ -1158,7 +1131,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:6: + %ROOT%/test.rego:5: {x | x := l[_]} == {"b"} | {"a"} @@ -1175,7 +1148,6 @@ FAIL: 1/1 note: "comprehension (object)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := ["a", "b", "c"] @@ -1187,7 +1159,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:6: + %ROOT%/test.rego:5: {k: x | x := l[k]} == {3: "d", 4: "e", 5: "f"} | {0: "a", 1: "b", 2: "c"} @@ -1204,7 +1176,6 @@ FAIL: 1/1 note: "every", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := [1, 2, 3] @@ -1217,7 +1188,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:7: + %ROOT%/test.rego:6: x == 1 | 2 @@ -1234,7 +1205,6 @@ FAIL: 1/1 note: "comprehension inside every", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := [1, 2, 3] @@ -1247,7 +1217,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:7: + %ROOT%/test.rego:6: [v | v := x] == [42] | [1] @@ -1264,7 +1234,6 @@ FAIL: 1/1 note: "nested every", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := [[1, 2], [3, 4], [5, 6]] @@ -1279,7 +1248,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: y < 4 | 4 @@ -1296,7 +1265,6 @@ FAIL: 1/1 note: "nested every with comprehension", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { l := [[1, 2], [3, 4], [5, 6]] @@ -1311,7 +1279,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: [v | v := y] == [42] | [1] @@ -1328,7 +1296,6 @@ FAIL: 1/1 note: "ref equality", files: map[string]string{ "/test.rego": `package test -import rego.v1 a := 1 b := 2 @@ -1341,7 +1308,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: a == b | | | 2 @@ -1359,7 +1326,6 @@ FAIL: 1/1 note: "ref equality (data)", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { data.a == data.b @@ -1370,7 +1336,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:5: + %ROOT%/test.rego:4: data.a == data.b | | | 2 @@ -1388,7 +1354,6 @@ FAIL: 1/1 note: "with, containing local vars", files: map[string]string{ "/test.rego": `package test -import rego.v1 p := input.x @@ -1401,7 +1366,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:8: + %ROOT%/test.rego:7: p == 2 with input.x as a | | | 1 @@ -1419,7 +1384,6 @@ FAIL: 1/1 note: "with, containing ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 p := input.x @@ -1433,7 +1397,7 @@ test_p if { -------------------------------------------------------------------------------- data.test.test_p: FAIL (%TIME%) - %ROOT%/test.rego:9: + %ROOT%/test.rego:8: p == 2 with input as testInput | | | {"x": 1} @@ -1451,7 +1415,6 @@ FAIL: 1/1 note: "negated rule ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 a if {true} @@ -1464,7 +1427,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:7: + %ROOT%/test.rego:6: not a | true @@ -1481,7 +1444,6 @@ FAIL: 1/1 note: "negated data ref", files: map[string]string{ "/test.rego": `package test -import rego.v1 test_foo if { not data.a @@ -1496,7 +1458,7 @@ test_foo if { -------------------------------------------------------------------------------- data.test.test_foo: FAIL (%TIME%) - %ROOT%/test.rego:5: + %ROOT%/test.rego:4: not data.a SUMMARY @@ -1542,7 +1504,6 @@ FAIL: 1/1 func TestIgnoreFlag(t *testing.T) { files := map[string]string{ "/test.rego": `package test -import rego.v1 p := input.foo == 42 test_p if { @@ -1571,7 +1532,6 @@ test_p if { func TestIgnoreFlagWithBundleFlag(t *testing.T) { files := map[string]string{ "/test.rego": `package test -import rego.v1 p := input.foo == 42 test_p if { @@ -1619,7 +1579,6 @@ func testSchemasAnnotation(rego string) (int, error) { func TestSchemasAnnotation(t *testing.T) { policyWithSchemaRef := ` package test -import rego.v1 # METADATA # schemas: @@ -1641,7 +1600,6 @@ test_p if { func TestSchemasAnnotationInline(t *testing.T) { policyWithInlinedSchema := ` package test -import rego.v1 # METADATA # schemas: @@ -1688,7 +1646,6 @@ func testSchemasAnnotationWithJSONFile(rego string, schema string) (int, error) func TestJSONSchemaSuccess(t *testing.T) { regoContents := `package test -import rego.v1 # METADATA # schemas: @@ -1728,7 +1685,6 @@ test_p if { func TestJSONSchemaFail(t *testing.T) { regoContents := `package test -import rego.v1 # METADATA # schemas: @@ -1773,7 +1729,7 @@ func TestWatchMode(t *testing.T) { "/policy.rego": `package foo p := 1`, "/policy_test.rego": `package foo -import rego.v1 + test_p if { p == 1 }`, @@ -1801,6 +1757,113 @@ test_p if { } buf.Reset() + // update the test + f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + _, err := f.WriteString("package foo\n test_p if { p == 2 }") + if err != nil { + t.Fatal(err) + } + f.Close() + + r := regexp.MustCompile(`FAIL \(.*s\)`) + expected = `%ROOT%/policy_test.rego: +data.foo.test_p: FAIL (%TIME%) +-------------------------------------------------------------------------------- +FAIL: 1/1 +******************************************************************************** +Watching for changes ... +` + if !test.Eventually(t, 2*time.Second, func() bool { + actual := r.ReplaceAllString(buf.String(), "FAIL (%TIME%)") + expected := strings.ReplaceAll(expected, "%ROOT%", root) + return strings.Contains(actual, expected) + }) { + t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String()) + } + buf.Reset() + + // update policy so test passes + f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + _, err = f.WriteString("package foo\n p := 2") + if err != nil { + t.Fatal(err) + } + + f.Close() + + expected = `PASS: 1/1 +******************************************************************************** +Watching for changes ... +` + if !test.Eventually(t, 2*time.Second, func() bool { + expected := strings.ReplaceAll(expected, "%ROOT%", root) + return strings.Contains(buf.String(), expected) + }) { + t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String()) + } + buf.Reset() + + // add new policy and test + if err := os.WriteFile(path.Join(root, "policy2.rego"), []byte("package bar\n q := \"hello\""), 0644); err != nil { + t.Fatal(err) + } + + if err := os.WriteFile(path.Join(root, "policy2_test.rego"), []byte("package bar\n test_q if { q == \"hello\" }"), 0644); err != nil { + t.Fatal(err) + } + + expected = `PASS: 2/2 +******************************************************************************** +Watching for changes ... +` + if !test.Eventually(t, 2*time.Second, func() bool { + expected := strings.ReplaceAll(expected, "%ROOT%", root) + return strings.Contains(buf.String(), expected) + }) { + t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String()) + } + buf.Reset() + + testParams.stopChan <- syscall.SIGINT + done <- struct{}{} + }) +} + +func TestWatchMode_v0(t *testing.T) { + + files := map[string]string{ + "/policy.rego": `package foo +p := 1`, + "/policy_test.rego": `package foo + +test_p { + p == 1 +}`, + } + + test.WithTempFS(files, func(root string) { + buf := test.BlockingWriter{} + + testParams := newTestCommandParams() + testParams.output = &buf + testParams.watch = true + testParams.count = 1 + testParams.v0Compatible = true + + done := make(chan struct{}) + go func() { + _, _ = opaTest([]string{root}, testParams) + <-done + }() + + expected := "Watching for changes ..." + if !test.Eventually(t, 2*time.Second, func() bool { + return strings.Contains(buf.String(), expected) + }) { + t.Fatalf("expected:\n\n%q\n\ngot:\n\n%q", expected, buf.String()) + } + buf.Reset() + // update the test f, _ := os.OpenFile(path.Join(root, "policy_test.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) _, err := f.WriteString("package foo\n test_p { p == 2 }") @@ -1877,7 +1940,7 @@ func TestWatchModeWithDataFile(t *testing.T) { files := map[string]string{ "/policy.rego": `package foo -import rego.v1 + test_p if { data.y == 1 }`, @@ -1933,7 +1996,7 @@ Watching for changes ... // update policy so test passes f, _ = os.OpenFile(path.Join(root, "policy.rego"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - _, err = f.WriteString("package foo\n test_p { data.y == 2 }") + _, err = f.WriteString("package foo\n test_p if { data.y == 2 }") if err != nil { t.Fatal(err) } @@ -1961,7 +2024,7 @@ Watching for changes ... func TestWatchModeWhenDataFileRemoved(t *testing.T) { files := map[string]string{ "/policy.rego": `package foo -import rego.v1 + test_p if { data.y == 1 }`, @@ -2070,7 +2133,7 @@ Watching for changes ...`, note: "broken policy", fileName: "broken_policy.rego", brokenFile: "package foo\n bar {", - fixedFile: "package foo\n bar {true}", + fixedFile: "package foo\n bar if {true}", expectedOutput: `1 error occurred during loading: %ROOT%/broken_policy.rego:2: rego_parse_error: unexpected eof token bar { ^ @@ -2083,7 +2146,7 @@ Watching for changes ...`, "/policy.rego": `package foo p := 1`, "/policy_test.rego": `package foo -import rego.v1 + test_p if { p == 1 }`, @@ -2185,7 +2248,7 @@ func TestExitCode(t *testing.T) { }{ "pass when no failed or skipped tests": { Test: `package foo - import rego.v1 + test_pass if { true } `, ExitZeroOnSkipped: false, @@ -2193,7 +2256,7 @@ func TestExitCode(t *testing.T) { }, "fail when failed tests": { Test: `package foo - import rego.v1 + test_pass if { true } test_fail if { false } `, @@ -2202,7 +2265,7 @@ func TestExitCode(t *testing.T) { }, "fail when skipped tests": { Test: `package foo - import rego.v1 + test_pass if { true } todo_test_skip if { true } `, @@ -2211,7 +2274,7 @@ func TestExitCode(t *testing.T) { }, "fail when failed tests and skipped tests": { Test: `package foo - import rego.v1 + test_pass if { true } test_fail if { false } todo_test_skip if { true } @@ -2221,7 +2284,7 @@ func TestExitCode(t *testing.T) { }, "pass when skipped tests and exit zero on skipped": { Test: `package foo - import rego.v1 + test_pass if { true } todo_test_skip if { true } `, @@ -2230,7 +2293,7 @@ func TestExitCode(t *testing.T) { }, "fail when failed tests and exit zero on skipped": { Test: `package foo - import rego.v1 + test_pass if { true } test_fail if { false } `, @@ -2239,7 +2302,7 @@ func TestExitCode(t *testing.T) { }, "fail when failed tests, skipped tests and exit zero on skipped": { Test: `package foo - import rego.v1 + test_pass if { true } test_fail if { false } todo_test_skip if { true } @@ -2273,7 +2336,6 @@ func TestCoverageThreshold(t *testing.T) { note: "coverage threshold met", modules: map[string]string{ "test.rego": `package test - import rego.v1 p := 1 test_p if { p == 1 }`, @@ -2284,7 +2346,6 @@ func TestCoverageThreshold(t *testing.T) { note: "coverage threshold not met", modules: map[string]string{ "test.rego": `package test - import rego.v1 p := 1 if { 1 == 1 @@ -2301,7 +2362,6 @@ func TestCoverageThreshold(t *testing.T) { note: "coverage threshold not met (verbose)", modules: map[string]string{ "test.rego": `package test - import rego.v1 p := 1 if { 1 == 1 @@ -2315,15 +2375,14 @@ func TestCoverageThreshold(t *testing.T) { verbose: true, expectedErrOutput: `Code coverage threshold not met: got 40.00 instead of 100.00 Lines not covered: - %ROOT%/test.rego:4-5 - %ROOT%/test.rego:8 + %ROOT%/test.rego:3-4 + %ROOT%/test.rego:7 `, }, { note: "coverage threshold not met (verbose, multiple files)", modules: map[string]string{ "policy1.rego": `package test - import rego.v1 p := 1 if { 1 == 1 @@ -2331,7 +2390,6 @@ Lines not covered: q := 2 r := 3`, "policy2.rego": `package test - import rego.v1 s := 4 if { 1 == 1 @@ -2341,7 +2399,6 @@ Lines not covered: u := 6 v := 7`, "test.rego": `package test - import rego.v1 test_q if { q == 2 } test_t if { t == 5 }`, @@ -2351,10 +2408,10 @@ Lines not covered: verbose: true, expectedErrOutput: `Code coverage threshold not met: got 33.33 instead of 100.00 Lines not covered: - %ROOT%/policy1.rego:4-5 - %ROOT%/policy1.rego:8 - %ROOT%/policy2.rego:4-6 - %ROOT%/policy2.rego:9-10 + %ROOT%/policy1.rego:3-4 + %ROOT%/policy1.rego:7 + %ROOT%/policy2.rego:3-5 + %ROOT%/policy2.rego:8-9 `, }, } @@ -2400,7 +2457,128 @@ func (t loadType) String() string { return [...]string{"file", "bundle", "bundle tarball"}[t] } -func TestWithV1CompatibleFlags(t *testing.T) { +func TestRun_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + files map[string]string + expErrs []string + }{ + { + note: "v0 module", + files: map[string]string{ + "/test.rego": `package test + +l1 := {1, 3, 5} +l2[v] { + v := l1[_] +} + +test_l { + l1 == l2 +}`, + }, + expErrs: []string{ + "test.rego:4: rego_parse_error: `if` keyword is required before rule body", + "test.rego:4: rego_parse_error: `contains` keyword is required for partial set rules", + "test.rego:8: rego_parse_error: `if` keyword is required before rule body", + }, + }, + { + note: "v1 module", + files: map[string]string{ + "/test.rego": `package test + +l1 := {1, 3, 5} +l2 contains v if { + v := l1[_] +} + +test_l if { + l1 == l2 +}`, + }, + }, + } + + loadTypes := []loadType{loadFile, loadBundle, loadTarball} + + for _, tc := range tests { + for _, loadType := range loadTypes { + t.Run(fmt.Sprintf("%s (%s)", tc.note, loadType), func(t *testing.T) { + var files map[string]string + if loadType != loadTarball { + files = tc.files + } + test.WithTempFS(files, func(root string) { + if loadType == loadTarball { + f, err := os.Create(filepath.Join(root, "bundle.tar.gz")) + if err != nil { + t.Fatal(err) + } + + testBundle := bundle.Bundle{ + Data: map[string]interface{}{}, + } + for k, v := range tc.files { + testBundle.Modules = append(testBundle.Modules, bundle.ModuleFile{ + Path: k, + Raw: []byte(v), + }) + } + + if err := bundle.Write(f, testBundle); err != nil { + t.Fatal(err) + } + } + + var buf bytes.Buffer + var errBuf bytes.Buffer + + testParams := newTestCommandParams() + testParams.bundleMode = loadType == loadBundle + testParams.count = 1 + testParams.output = &buf + testParams.errOutput = &errBuf + + var paths []string + if loadType == loadTarball { + paths = []string{filepath.Join(root, "bundle.tar.gz")} + } else { + paths = []string{root} + } + + exitCode, _ := opaTest(paths, testParams) + if len(tc.expErrs) > 0 { + if exitCode == 0 { + t.Fatalf("expected non-zero exit code") + } + + for _, expErr := range tc.expErrs { + if actual := errBuf.String(); !strings.Contains(actual, expErr) { + t.Fatalf("expected error output to contain:\n\n%q\n\nbut got:\n\n%q", expErr, actual) + } + } + } else { + if exitCode != 0 { + t.Fatalf("unexpected exit code: %d", exitCode) + } + + if errBuf.Len() > 0 { + t.Fatalf("expected no error output but got:\n\n%q", buf.String()) + } + + expected := "PASS: 1/1" + if actual := buf.String(); !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain:\n\n%s\n\nbut got:\n\n%q", expected, actual) + } + } + }) + }) + } + } +} + +func TestRun_CompatibleFlags(t *testing.T) { tests := []struct { note string v0Compatible bool diff --git a/cmd/version.go b/cmd/version.go index 763bc25644..500f16e175 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -11,7 +11,8 @@ import ( "io" "os" - version2 "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/ast" + version2 "github.com/open-policy-agent/opa/v1/version" "github.com/spf13/cobra" "github.com/open-policy-agent/opa/cmd/internal/env" @@ -47,6 +48,7 @@ func generateCmdOutput(out io.Writer, check bool) { fmt.Fprintln(out, "Build Hostname: "+version2.Hostname) fmt.Fprintln(out, "Go Version: "+version2.GoVersion) fmt.Fprintln(out, "Platform: "+version2.Platform) + fmt.Fprintln(out, "Rego Version: "+ast.DefaultRegoVersion.String()) var wasmAvailable string diff --git a/cmd/version_test.go b/cmd/version_test.go index 830c9af2b5..e9c25e5556 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -29,6 +29,7 @@ func TestGenerateCmdOutputDisableCheckFlag(t *testing.T) { "Go Version", "Platform", "WebAssembly", + "Rego Version", }) } @@ -60,6 +61,7 @@ func TestGenerateCmdOutputWithCheckFlagNoError(t *testing.T) { "Latest Upstream Version", "Release Notes", "Download", + "Rego Version", }) } diff --git a/compile/compile.go b/compile/compile.go new file mode 100644 index 0000000000..decae108ef --- /dev/null +++ b/compile/compile.go @@ -0,0 +1,37 @@ +// 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 compile implements bundles compilation and linking. +package compile + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/compile" +) + +const ( + // TargetRego is the default target. The source rego is copied (potentially + // rewritten for optimization purpsoes) into the bundle. The target supports + // base documents. + TargetRego = v1.TargetRego + + // TargetWasm is an alternative target that compiles the policy into a wasm + // module instead of Rego. The target supports base documents. + TargetWasm = v1.TargetWasm + + // TargetPlan is an altertive target that compiles the policy into an + // imperative query plan that can be further transpiled or interpreted. + TargetPlan = v1.TargetPlan +) + +// Targets contains the list of targets supported by the compiler. +var Targets = v1.Targets + +// Compiler implements bundle compilation and linking. +type Compiler = v1.Compiler + +// New returns a new compiler instance that can be invoked. +func New() *Compiler { + return v1.New().WithRegoVersion(ast.DefaultRegoVersion) +} diff --git a/compile/compile_test.go b/compile/compile_test.go new file mode 100644 index 0000000000..fecb60a9a0 --- /dev/null +++ b/compile/compile_test.go @@ -0,0 +1,778 @@ +// Copyright 2024 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 compile + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/bundle" + "github.com/open-policy-agent/opa/internal/file/archive" + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/util/test" +) + +func TestCompilerDefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expRegoVersion ast.RegoVersion + expErrs []string + }{ + { + note: "v0", // Default rego-version + module: ` + package test + + p[x] { + x = "a" + }`, + expRegoVersion: ast.RegoV0, + }, + { + note: "import rego.v1", + module: ` + package test + import rego.v1 + + p contains x if { + x = "a" + }`, + expRegoVersion: ast.RegoV0, + }, + { + note: "v1", // NOT default rego-version + module: ` + package test + + p contains x if { + x = "a" + }`, + expRegoVersion: ast.RegoV1, + expErrs: []string{ + "test.rego:4: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + for _, useMemoryFS := range []bool{false, true} { + test.WithTestFS(files, useMemoryFS, func(root string, fsys fs.FS) { + + compiler := New(). + WithFS(fsys). + WithPaths(root) + + err := compiler.Build(context.Background()) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatal("expected error, got none") + } + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error to contain:\n\n%s\n\ngot:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatal(err) + } + + // Verify result is just bundle load. + exp, err := loader.NewFileLoader().WithFS(fsys).AsBundle(root) + if err != nil { + panic(err) + } + + err = exp.FormatModules(false) + if err != nil { + t.Fatal(err) + } + + if !compiler.Bundle().Equal(*exp) { + t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", compiler.Bundle(), exp) + } + } + }) + } + }) + } +} + +func TestCompilerLoadAsBundleWithBundleRegoVersion(t *testing.T) { + tests := []struct { + note string + files map[string]string + expErrs []string + }{ + { + note: "No bundle rego version (default version)", + files: map[string]string{ + ".manifest": `{}`, + "test.rego": `package test +import rego.v1 +p[1] if { + input.x == 2 +}`, + }, + }, + { + note: "v0 bundle rego version", + files: map[string]string{ + ".manifest": `{"rego_version": 0}`, + "test.rego": `package test +p[1] { + input.x == 2 +}`, + }, + }, + { + note: "v0 bundle rego version, missing keyword imports", + files: map[string]string{ + ".manifest": `{"rego_version": 0}`, + "test.rego": `package test +p contains 1 if { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_parse_error: var cannot be used for rule name", + "rego_parse_error: number cannot be used for rule name", + }, + }, + { + note: "v1 bundle rego version", + files: map[string]string{ + ".manifest": `{"rego_version": 1}`, + "test.rego": `package test +p contains 1 if { + input.x == 2 +}`, + }, + }, + { + note: "v1 bundle rego version, no keywords", + files: map[string]string{ + ".manifest": `{"rego_version": 1}`, + "test.rego": `package test +p[1] { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_parse_error: `if` keyword is required before rule body", + "rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 bundle rego version, duplicate imports", + files: map[string]string{ + ".manifest": `{"rego_version": 1}`, + "test.rego": `package test +import data.foo +import data.foo + +p contains 1 if { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_compile_error: import must not shadow import data.foo", + }, + }, + // file overrides + { + note: "v0 bundle rego version, v1 file override", + files: map[string]string{ + ".manifest": `{ + "rego_version": 0, + "file_rego_versions": { + "*/test2.rego": 1 + } +}`, + "test1.rego": `package test +p["A"] { + input.x == 1 +}`, + "test2.rego": `package test +p contains "B" if { + input.x == 2 +}`, + }, + }, + { + note: "v0 bundle rego version, v1 file override, missing file", + files: map[string]string{ + ".manifest": `{ + "rego_version": 0, + "file_rego_versions": { + "*/test2.rego": 1 + } +}`, + "test1.rego": `package test +p["A"] { + input.x == 1 +}`, + }, + }, + { + note: "v0 bundle rego version, v1 file override, no keywords", + files: map[string]string{ + ".manifest": `{ + "rego_version": 0, + "file_rego_versions": { + "*/test2.rego": 1 + } +}`, + "test1.rego": `package test +p["A"] { + input.x == 1 +}`, + "test2.rego": `package test +p["B"] { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_parse_error: `if` keyword is required before rule body", + "rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v0 bundle rego version, v1 file override, duplicate imports", + files: map[string]string{ + ".manifest": `{ + "rego_version": 0, + "file_rego_versions": { + "*/test2.rego": 1 + } +}`, + "test1.rego": `package test +p["A"] { + input.x == 1 +}`, + "test2.rego": `package test +import data.foo +import data.foo + +p contains "B" if { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_compile_error: import must not shadow import data.foo", + }, + }, + { + note: "v1 bundle rego version, v0 file override", + files: map[string]string{ + ".manifest": `{ + "rego_version": 1, + "file_rego_versions": { + "*/test1.rego": 0 + } +}`, + "test1.rego": `package test +p["A"] { + input.x == 1 +}`, + "test2.rego": `package test +p contains "B" if { + input.x == 2 +}`, + }, + }, + { + note: "v1 bundle rego version, v0 file override, no import", + files: map[string]string{ + ".manifest": `{ + "rego_version": 1, + "file_rego_versions": { + "*/test1.rego": 0 + } +}`, + "test1.rego": `package test +p contains "A" if { + input.x == 1 +}`, + "test2.rego": `package test +p contains "B" if { + input.x == 2 +}`, + }, + expErrs: []string{ + "rego_parse_error: var cannot be used for rule name", + "rego_parse_error: string cannot be used for rule name", + }, + }, + } + + bundleTypeCases := []struct { + note string + tar bool + }{ + { + "bundle dir", false, + }, + { + "bundle tar", true, + }, + } + + for _, bundleType := range bundleTypeCases { + for _, tc := range tests { + ctx := context.Background() + t.Run(fmt.Sprintf("%s, %s", bundleType.note, tc.note), func(t *testing.T) { + files := map[string]string{} + if bundleType.tar { + files["bundle.tar"] = "" + } else { + for k, v := range tc.files { + files[k] = v + } + } + + test.WithTestFS(tc.files, false, func(root string, fsys fs.FS) { + var path string + if bundleType.tar { + path = filepath.Join(root, "bundle.tar.gz") + files := make([][2]string, 0, len(tc.files)) + for k, v := range tc.files { + files = append(files, [2]string{k, v}) + } + buf := archive.MustWriteTarGz(files) + + bf, err := os.Create(path) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + _, err = bf.Write(buf.Bytes()) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } else { + path = root + } + + compiler := New(). + WithFS(fsys). + WithPaths(path). + WithAsBundle(true) + + err := compiler.Build(ctx) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatal("expected error, got none") + } + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error to contain:\n\n%s\n\ngot:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatal(err) + } + } + }) + }) + } + } +} + +func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { + regoV0 := ast.RegoV0.Int() + regoV1 := ast.RegoV1.Int() + regoDef := ast.RegoV0.Int() + + tests := []struct { + note string + bundles []*bundle.Bundle + regoVersion ast.RegoVersion + expGlobalRegoVersion *int + expFileRegoVersions map[string]int + }{ + { + note: "single bundle, no bundle rego version (default version)", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{}, + }, + }, + expGlobalRegoVersion: ®oDef, + expFileRegoVersions: map[string]int{}, + }, + { + note: "single bundle, global rego version", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV1, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{}, + }, + }, + expGlobalRegoVersion: ®oV1, + expFileRegoVersions: map[string]int{}, + }, + { + note: "no global rego versions", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{}, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{}, + }, + }, + regoVersion: ast.RegoV1, + expGlobalRegoVersion: ®oV1, + }, + { + note: "global rego versions, v1 bundles, v0 provided", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV1, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "a/test1.rego", + URL: "a/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package a"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + RegoVersion: ®oV1, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "b/test1.rego", + URL: "b/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package b"), + }, + }, + }, + }, + regoVersion: ast.RegoV0, + // global rego-version in bundles are dropped in favor of the provided rego-version + expGlobalRegoVersion: ®oV0, + expFileRegoVersions: map[string]int{ + "/a/test1.rego": 1, + "/b/test1.rego": 1, + }, + }, + { + note: "global rego versions, v0 bundles, v1 provided", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV0, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "a/test1.rego", + URL: "a/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package a"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + RegoVersion: ®oV0, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "b/test1.rego", + URL: "b/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package b"), + }, + }, + }, + }, + regoVersion: ast.RegoV1, + // global rego-version in bundles are dropped in favor of the provided rego-version + expGlobalRegoVersion: ®oV1, + expFileRegoVersions: map[string]int{ + "/a/test1.rego": 0, + "/b/test1.rego": 0, + }, + }, + { + note: "different global rego versions", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV0, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "a/test1.rego", + URL: "a/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package a"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + RegoVersion: ®oV1, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "b/test1.rego", + URL: "b/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package b"), + }, + }, + }, + }, + regoVersion: ast.RegoV0, + // global rego-version in bundles are dropped in favor of the provided rego-version + expGlobalRegoVersion: ®oV0, + expFileRegoVersions: map[string]int{ + "/b/test1.rego": 1, + }, + }, + { + note: "different global rego versions, per-file overrides", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV1, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "a/test1.rego", + URL: "a/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package a"), + }, + { + Path: "a/test2.rego", + URL: "a/test2.rego", + RelativePath: "/test2.rego", + Raw: []byte("package a"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + RegoVersion: ®oV1, + FileRegoVersions: map[string]int{ + "/test1.rego": 0, + }, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + // we don't expect this file to get an individual rego-version in the result, as + // it has the same rego-version as the global rego-version + Path: "b/test1.rego", + URL: "b/test1.rego", + RelativePath: "/test1.rego", + Raw: []byte("package b"), + }, + { + Path: "b/test2.rego", + URL: "b/test2.rego", + RelativePath: "/test2.rego", + Raw: []byte("package b"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + RegoVersion: ®oV0, + Roots: &[]string{"c"}, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + // we don't expect these files to get individual rego-versions in the result, + // as they have the same rego-version as the global rego-version + { + Path: "c/test1.rego", + URL: "c/test1.rego", + RelativePath: "test1.rego", + Raw: []byte("package c"), + }, + { + Path: "c/test2.rego", + URL: "c/test2.rego", + RelativePath: "test2.rego", + Raw: []byte("package c"), + }, + }, + }, + }, + regoVersion: ast.RegoV0, + // global rego-version in bundles are dropped in favor of the provided rego-version + expGlobalRegoVersion: ®oV0, + // rego-versions is expected for all modules with different rego-version than the global rego-version + expFileRegoVersions: map[string]int{ + "/a/test1.rego": 1, + "/a/test2.rego": 1, + "/b/test2.rego": 1, + }, + }, + { + note: "glob per-file overrides", + bundles: []*bundle.Bundle{ + { + Manifest: bundle.Manifest{ + Roots: &[]string{"a"}, + RegoVersion: ®oV0, + FileRegoVersions: map[string]int{ + "a/*": 1, + }, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "a/foo/test.rego", + URL: "a/foo/test.rego", + Raw: []byte("package a"), + }, + { + Path: "a/bar/test.rego", + URL: "a/bar/test.rego", + Raw: []byte("package a"), + }, + { + Path: "a/baz/test.rego", + URL: "a/baz/test.rego", + Raw: []byte("package a"), + }, + }, + }, + { + Manifest: bundle.Manifest{ + Roots: &[]string{"b"}, + RegoVersion: ®oV1, + FileRegoVersions: map[string]int{ + // glob should not affect files with matching path in the other bundle + "*/bar/*": 0, + }, + }, + Data: map[string]interface{}{}, + Modules: []bundle.ModuleFile{ + { + Path: "b/foo/test.rego", + URL: "b/foo/test.rego", + Raw: []byte("package b"), + }, + { + Path: "b/bar/test.rego", + URL: "b/bar/test.rego", + Raw: []byte("package b"), + }, + { + Path: "b/baz/test.rego", + URL: "b/baz/test.rego", + Raw: []byte("package b"), + }, + }, + }, + }, + regoVersion: ast.RegoV0, + expGlobalRegoVersion: ®oV0, + expFileRegoVersions: map[string]int{ + "/a/foo/test.rego": 1, + "/a/bar/test.rego": 1, + "/a/baz/test.rego": 1, + "/b/foo/test.rego": 1, + "/b/baz/test.rego": 1, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + for _, b := range tc.bundles { + b.Manifest.Init() + for i, m := range b.Modules { + b.Modules[i].Parsed = ast.MustParseModule(string(m.Raw)) + } + } + + result, err := bundle.MergeWithRegoVersion(tc.bundles, tc.regoVersion, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + compareRegoVersions(t, tc.expGlobalRegoVersion, result.Manifest.RegoVersion) + + if !reflect.DeepEqual(tc.expFileRegoVersions, result.Manifest.FileRegoVersions) { + t.Fatalf("expected file rego versions to be:\n\n%v\n\nbut got:\n\n%v", tc.expFileRegoVersions, result.Manifest.FileRegoVersions) + } + }) + } +} + +func compareRegoVersions(t *testing.T, exp, act *int) { + t.Helper() + if exp == nil { + if act != nil { + t.Errorf("expected no rego version, but got %v", *act) + } + } else { + if act == nil { + t.Errorf("expected rego version to be %v, but got none", *exp) + } else if *act != *exp { + t.Errorf("expected rego version to be %v, but got %v", *exp, *act) + } + } +} diff --git a/compile/doc.go b/compile/doc.go new file mode 100644 index 0000000000..bfe5c97dc2 --- /dev/null +++ b/compile/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package compile diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000..e612df0a00 --- /dev/null +++ b/config/config.go @@ -0,0 +1,19 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/config" +) + +// Config represents the configuration file that OPA can be started with. +type Config = v1.Config + +// 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) { + return v1.ParseConfig(raw, id) +} diff --git a/config/doc.go b/config/doc.go new file mode 100644 index 0000000000..c6dd968dc6 --- /dev/null +++ b/config/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package config diff --git a/cover/cover.go b/cover/cover.go new file mode 100644 index 0000000000..892a3a5968 --- /dev/null +++ b/cover/cover.go @@ -0,0 +1,37 @@ +// 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 cover reports coverage on modules. +package cover + +import ( + v1 "github.com/open-policy-agent/opa/v1/cover" +) + +// Cover computes and reports on coverage. +type Cover = v1.Cover + +// New returns a new Cover object. +func New() *Cover { + return v1.New() +} + +// Position represents a file location. +type Position = v1.Position + +// PositionSlice is a collection of position that can be sorted. +type PositionSlice = v1.PositionSlice + +// Range represents a range of positions in a file. +type Range = v1.Range + +// FileReport represents a coverage report for a single file. +type FileReport = v1.FileReport + +// Report represents a coverage report for a set of files. +type Report = v1.Report + +// CoverageThresholdError represents an error raised when the global +// code coverage percentage is lower than the specified threshold. +type CoverageThresholdError = v1.CoverageThresholdError diff --git a/cover/doc.go b/cover/doc.go new file mode 100644 index 0000000000..4dce6e538a --- /dev/null +++ b/cover/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package cover diff --git a/debug/breakpoint.go b/debug/breakpoint.go new file mode 100644 index 0000000000..66f3144c32 --- /dev/null +++ b/debug/breakpoint.go @@ -0,0 +1,13 @@ +// Copyright 2024 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 debug + +import ( + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +type BreakpointID = v1.BreakpointID + +type Breakpoint = v1.Breakpoint diff --git a/debug/debugger.go b/debug/debugger.go new file mode 100644 index 0000000000..33f46fb677 --- /dev/null +++ b/debug/debugger.go @@ -0,0 +1,52 @@ +// Copyright 2024 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 debug +// EXPERIMENTAL: This package is under active development and is subject to change. +package debug + +import ( + "github.com/open-policy-agent/opa/logging" + "github.com/open-policy-agent/opa/rego" + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +// Debugger is the interface for launching OPA debugger Session(s). +// This implementation is similar in structure to the Debug Adapter Protocol (DAP) +// to make such integrations easier, but is not intended to be a direct implementation. +// See: https://microsoft.github.io/debug-adapter-protocol/specification +// +// EXPERIMENTAL: These interfaces are under active development and is subject to change. +type Debugger = v1.Debugger + +type Session = v1.Session + +type DebuggerOption = v1.DebuggerOption + +func NewDebugger(options ...DebuggerOption) Debugger { + return v1.NewDebugger(options...) +} + +func SetLogger(logger logging.Logger) DebuggerOption { + return v1.SetLogger(logger) +} + +func SetEventHandler(handler EventHandler) DebuggerOption { + return v1.SetEventHandler(handler) +} + +type LaunchEvalProperties = v1.LaunchEvalProperties + +type LaunchTestProperties = v1.LaunchTestProperties + +type LaunchProperties = v1.LaunchProperties + +type LaunchOption = v1.LaunchOption + +// RegoOption adds a rego option to the internal Rego instance. +// Options may be overridden by the debugger, and it is recommended to +// use LaunchEvalProperties for commonly used options. +func RegoOption(opt func(*rego.Rego)) LaunchOption { + return v1.RegoOption(opt) +} diff --git a/debug/doc.go b/debug/doc.go new file mode 100644 index 0000000000..08931e6027 --- /dev/null +++ b/debug/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package debug diff --git a/debug/event.go b/debug/event.go new file mode 100644 index 0000000000..50d6bc2904 --- /dev/null +++ b/debug/event.go @@ -0,0 +1,23 @@ +// Copyright 2024 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 debug + +import ( + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +type EventType = v1.EventType + +const ( + ExceptionEventType = v1.ExceptionEventType + StdoutEventType = v1.StdoutEventType + StoppedEventType = v1.StoppedEventType + TerminatedEventType = v1.TerminatedEventType + ThreadEventType = v1.ThreadEventType +) + +type Event = v1.Event + +type EventHandler = v1.EventHandler diff --git a/debug/frame.go b/debug/frame.go new file mode 100644 index 0000000000..b260acad92 --- /dev/null +++ b/debug/frame.go @@ -0,0 +1,16 @@ +// Copyright 2024 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 debug + +import ( + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +type FrameID = v1.FrameID + +type StackFrame = v1.StackFrame + +// StackTrace represents a StackFrame stack. +type StackTrace = v1.StackTrace diff --git a/debug/thread.go b/debug/thread.go new file mode 100644 index 0000000000..bf7a01b297 --- /dev/null +++ b/debug/thread.go @@ -0,0 +1,17 @@ +// Copyright 2024 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 debug + +import ( + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +type ThreadID = v1.ThreadID + +// Thread represents a single thread of execution. +type Thread = v1.Thread + +// Scope represents the variable state of a StackFrame. +type Scope = v1.Scope diff --git a/debug/variable.go b/debug/variable.go new file mode 100644 index 0000000000..585d615625 --- /dev/null +++ b/debug/variable.go @@ -0,0 +1,13 @@ +// Copyright 2024 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 debug + +import ( + v1 "github.com/open-policy-agent/opa/v1/debug" +) + +type Variable = v1.Variable + +type VarRef = v1.VarRef diff --git a/dependencies/deps.go b/dependencies/deps.go new file mode 100644 index 0000000000..868edd1794 --- /dev/null +++ b/dependencies/deps.go @@ -0,0 +1,42 @@ +// Copyright 2017 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 dependencies + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/dependencies" +) + +// All returns the list of data ast.Refs that the given AST element depends on. +func All(x interface{}) (resolved []ast.Ref, err error) { + return v1.All(x) +} + +// Minimal returns the list of data ast.Refs that the given AST element depends on. +// If an AST element depends on a ast.Ref that is a prefix of another dependency, the +// ast.Ref that is the prefix of the other will be the only one in the returned list. +// +// As an example, if an element depends on data.x and data.x.y, only data.x will +// be in the returned list. +func Minimal(x interface{}) (resolved []ast.Ref, err error) { + return v1.Minimal(x) +} + +// Base returns the list of base data documents that the given AST element depends on. +// +// The returned refs are always constant and are truncated at any point where they become +// dynamic. That is, a ref like data.a.b[x] will be truncated to data.a.b. +func Base(compiler *ast.Compiler, x interface{}) ([]ast.Ref, error) { + return v1.Base(compiler, x) +} + +// Virtual returns the list of virtual data documents that the given AST element depends +// on. +// +// The returned refs are always constant and are truncated at any point where they become +// dynamic. That is, a ref like data.a.b[x] will be truncated to data.a.b. +func Virtual(compiler *ast.Compiler, x interface{}) ([]ast.Ref, error) { + return v1.Virtual(compiler, x) +} diff --git a/dependencies/doc.go b/dependencies/doc.go new file mode 100644 index 0000000000..f2a4ea5cf0 --- /dev/null +++ b/dependencies/doc.go @@ -0,0 +1,11 @@ +// Copyright 2017 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 dependencies provides functions for determining the set of ast.Refs that AST +// elements depend on. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package dependencies diff --git a/docs/content/cli.md b/docs/content/cli.md index 2a87f8462f..33e1a269b8 100755 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -64,7 +64,6 @@ opa bench [flags] -t, --target {rego,wasm} set the runtime to exercise (default rego) -u, --unknowns stringArray set paths to treat as unknown during partial evaluation (default [input]) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 ``` ____ @@ -258,7 +257,6 @@ opa build [ [...]] [flags] --signing-plugin string name of the plugin to use for signing/verification (see https://www.openpolicyagent.org/docs/latest/management-bundles/#signature-plugin -t, --target {rego,wasm,plan} set the output bundle target type (default rego) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 --verification-key string set the secret (HMAC) or path of the PEM file containing the public key (RSA and ECDSA) --verification-key-id string name assigned to the verification key used for bundle verification (default "default") --wasm-include-print enable print statements inside of WebAssembly modules compiled by the compiler @@ -360,7 +358,6 @@ opa check [path [...]] [flags] -s, --schema string set schema file path or directory path -S, --strict enable compiler strict mode --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 ``` ____ @@ -416,7 +413,6 @@ opa deps [flags] -f, --format {pretty,json} set output format (default pretty) -h, --help help for deps --ignore strings set file and directory names to ignore during loading (e.g., '.*' excludes hidden files) - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 ``` ____ @@ -576,7 +572,6 @@ opa eval [flags] --timeout duration set eval timeout (default unlimited) -u, --unknowns stringArray set paths to treat as unknown during partial evaluation (default [input]) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 --var-values show local variable values in pretty trace output ``` @@ -639,7 +634,6 @@ opa exec [ [...]] [flags] -I, --stdin-input read input document from stdin rather than a static file --timeout duration set exec timeout with a Go-style duration, such as '5m 30s'. (default unlimited) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 ``` ____ @@ -684,7 +678,6 @@ opa fmt [path [...]] [flags] -l, --list list all files who would change when formatted --rego-v1 format module(s) to be compatible with both Rego v1 and current OPA version) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 -w, --write overwrite the original source file ``` @@ -731,7 +724,7 @@ opa inspect [ [...]] [flags] -a, --annotations list annotations -f, --format {json,pretty} set output format (default pretty) -h, --help help for inspect - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 + --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible ``` ____ @@ -754,7 +747,6 @@ opa parse [flags] -f, --format {pretty,json} set output format (default pretty) -h, --help help for parse --json-include string include or exclude optional elements. By default comments are included. Current options: locations, comments. E.g. --json-include locations,-comments will include locations and exclude comments. - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 ``` ____ @@ -935,7 +927,6 @@ opa run [flags] --tls-private-key-file string set path of TLS private key file --unix-socket-perm string specify the permissions for the Unix domain socket if used to listen for incoming connections (default "755") --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 --verification-key string set the secret (HMAC) or path of the PEM file containing the public key (RSA and ECDSA) --verification-key-id string name assigned to the verification key used for bundle verification (default "default") -w, --watch watch command line files for changes @@ -1155,7 +1146,6 @@ opa test [path [...]] [flags] --threshold float set coverage threshold and exit with non-zero status if coverage is less than threshold % --timeout duration set test timeout (default 5s, 30s when benchmarking) --v0-compatible opt-in to OPA features and behaviors prior to the OPA v1.0 release. Takes precedence over --v1-compatible - --v1-compatible opt-in to OPA features and behaviors that are enabled by default in OPA v1.0 --var-values show local variable values in test output -v, --verbose set verbose reporting mode -w, --watch watch command line files for changes diff --git a/docs/website/scripts/live-blocks/src/preprocess/localEval.js b/docs/website/scripts/live-blocks/src/preprocess/localEval.js index dfa8e87783..1f5fed04fb 100644 --- a/docs/website/scripts/live-blocks/src/preprocess/localEval.js +++ b/docs/website/scripts/live-blocks/src/preprocess/localEval.js @@ -69,7 +69,7 @@ export default async function localEval(groups, groupName, opaVersion) { // Returns 1st a function that consumes a string for the output format you want and produces an array of arguments and 2nd a map of module file names to strings that should be replaced in error messages. May throw a user-friendly error. async function prepEval(groups, groupName, opaVersion) { const {module, package: pkg, query, input, included} = getGroupData(groups, groupName) - const base = ['eval', '--fail'] // Fail on undefined + const base = ['eval', '--fail', '--v0-compatible'] // Fail on undefined const rest = [] const moduleFilenameMap = {} diff --git a/download/config.go b/download/config.go new file mode 100644 index 0000000000..0e252b0589 --- /dev/null +++ b/download/config.go @@ -0,0 +1,15 @@ +// 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 download + +import ( + v1 "github.com/open-policy-agent/opa/v1/download" +) + +// PollingConfig represents polling configuration for the downloader. +type PollingConfig = v1.PollingConfig + +// Config represents the configuration for the downloader. +type Config = v1.Config diff --git a/download/doc.go b/download/doc.go new file mode 100644 index 0000000000..ad1a6cd3fa --- /dev/null +++ b/download/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package download diff --git a/download/download.go b/download/download.go new file mode 100644 index 0000000000..cfb14c0704 --- /dev/null +++ b/download/download.go @@ -0,0 +1,29 @@ +// 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 download implements low-level OPA bundle downloading. +package download + +import ( + "github.com/open-policy-agent/opa/plugins/rest" + v1 "github.com/open-policy-agent/opa/v1/download" +) + +// Update contains the result of a download. If an error occurred, the Error +// field will be non-nil. If a new bundle is available, the Bundle field will +// be non-nil. +type Update = v1.Update + +// Downloader implements low-level OPA bundle downloading. Downloader can be +// started and stopped. After starting, the downloader will request bundle +// updates from the remote HTTP endpoint that the client is configured to +// connect to. +type Downloader = v1.Downloader + +// New returns a new Downloader that can be started. +func New(config Config, client rest.Client, path string) *Downloader { + return v1.New(config, client, path) +} + +type HTTPError = v1.HTTPError diff --git a/download/oci_download.go b/download/oci_download.go new file mode 100644 index 0000000000..6df1246c0c --- /dev/null +++ b/download/oci_download.go @@ -0,0 +1,14 @@ +//go:build !opa_no_oci + +package download + +import ( + v1 "github.com/open-policy-agent/opa/v1/download" + + "github.com/open-policy-agent/opa/plugins/rest" +) + +// NewOCI returns a new Downloader that can be started. +func NewOCI(config Config, client rest.Client, path, storePath string) *OCIDownloader { + return v1.NewOCI(config, client, path, storePath) +} diff --git a/download/oci_download_unavailable.go b/download/oci_download_unavailable.go new file mode 100644 index 0000000000..e105d2bd79 --- /dev/null +++ b/download/oci_download_unavailable.go @@ -0,0 +1,59 @@ +//go:build opa_no_oci + +package download + +import ( + "context" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/bundle" + "github.com/open-policy-agent/opa/plugins/rest" +) + +func NewOCI(Config, rest.Client, string, string) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) WithCallback(f func(context.Context, Update)) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) WithLogAttrs(map[string]interface{}) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) WithBundleVerificationConfig(*bundle.VerificationConfig) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) WithSizeLimitBytes(int64) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) WithBundlePersistence(bool) *OCIDownloader { + panic("built without OCI support") +} + +func (d *OCIDownloader) ClearCache() { + panic("built without OCI support") +} + +func (d *OCIDownloader) SetCache(string) { + panic("built without OCI support") +} + +func (d *OCIDownloader) Trigger(context.Context) error { + panic("built without OCI support") +} + +func (d *OCIDownloader) Start(context.Context) { + panic("built without OCI support") +} + +func (d *OCIDownloader) Stop(context.Context) { + panic("built without OCI support") +} + +func (*OCIDownloader) WithBundleParserOpts(ast.ParserOptions) *OCIDownloader { + panic("built without OCI support") +} diff --git a/download/oci_downloader.go b/download/oci_downloader.go new file mode 100644 index 0000000000..847c456c5b --- /dev/null +++ b/download/oci_downloader.go @@ -0,0 +1,7 @@ +package download + +import ( + v1 "github.com/open-policy-agent/opa/v1/download" +) + +type OCIDownloader = v1.OCIDownloader diff --git a/features/doc.go b/features/doc.go new file mode 100644 index 0000000000..7f2bdce3d2 --- /dev/null +++ b/features/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package features diff --git a/features/tracing/doc.go b/features/tracing/doc.go new file mode 100644 index 0000000000..161a3d0cee --- /dev/null +++ b/features/tracing/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package tracing diff --git a/features/tracing/tracing.go b/features/tracing/tracing.go new file mode 100644 index 0000000000..017f79f69b --- /dev/null +++ b/features/tracing/tracing.go @@ -0,0 +1,10 @@ +// Copyright 2021 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 tracing + +import ( + // Importing v1 for side effects + _ "github.com/open-policy-agent/opa/v1/features/tracing" +) diff --git a/features/wasm/doc.go b/features/wasm/doc.go new file mode 100644 index 0000000000..165997e4a2 --- /dev/null +++ b/features/wasm/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package wasm diff --git a/features/wasm/wasm.go b/features/wasm/wasm.go new file mode 100644 index 0000000000..a2c6e8f06c --- /dev/null +++ b/features/wasm/wasm.go @@ -0,0 +1,14 @@ +// Copyright 2021 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. + +// Import this package to enable evaluation of rego code using the +// built-in wasm engine. +package wasm + +import ( + v1 "github.com/open-policy-agent/opa/v1/features/wasm" +) + +// OPA is an implementation of the OPA SDK. +type OPA = v1.OPA diff --git a/format/doc.go b/format/doc.go new file mode 100644 index 0000000000..ba514fffb9 --- /dev/null +++ b/format/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package format diff --git a/format/format.go b/format/format.go new file mode 100644 index 0000000000..ad09cea843 --- /dev/null +++ b/format/format.go @@ -0,0 +1,86 @@ +// Copyright 2017 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 format implements formatting of Rego source files. +package format + +import ( + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/types" + v1 "github.com/open-policy-agent/opa/v1/format" +) + +// Opts lets you control the code formatting via `AstWithOpts()`. +type Opts = v1.Opts + +// Source formats a Rego source file. The bytes provided must describe a complete +// 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{ + RegoVersion: ast.DefaultRegoVersion, + ParserOptions: &ast.ParserOptions{ + RegoVersion: ast.DefaultRegoVersion, + }, + }) +} + +func SourceWithOpts(filename string, src []byte, opts Opts) ([]byte, error) { + if opts.RegoVersion == ast.RegoUndefined { + opts.RegoVersion = ast.DefaultRegoVersion + } + if opts.ParserOptions == nil { + opts.ParserOptions = &ast.ParserOptions{} + } + if opts.ParserOptions.RegoVersion == ast.RegoUndefined { + opts.ParserOptions.RegoVersion = ast.DefaultRegoVersion + } + + return v1.SourceWithOpts(filename, src, opts) +} + +// MustAst is a helper function to format a Rego AST element. If any errors +// occurs this function will panic. This is mostly used for test +func MustAst(x interface{}) []byte { + bs, err := Ast(x) + if err != nil { + panic(err) + } + return bs +} + +// MustAstWithOpts is a helper function to format a Rego AST element. If any errors +// occurs this function will panic. This is mostly used for test +func MustAstWithOpts(x interface{}, opts Opts) []byte { + bs, err := AstWithOpts(x, opts) + if err != nil { + panic(err) + } + return bs +} + +// Ast formats a Rego AST element. If the passed value is not a valid AST +// element, Ast returns nil and an error. If AST nodes are missing locations +// an arbitrary location will be used. +func Ast(x interface{}) ([]byte, error) { + return AstWithOpts(x, Opts{ + RegoVersion: ast.DefaultRegoVersion, + }) +} + +func AstWithOpts(x interface{}, opts Opts) ([]byte, error) { + if opts.RegoVersion == ast.RegoUndefined { + opts.RegoVersion = ast.DefaultRegoVersion + } + + return v1.AstWithOpts(x, opts) +} + +// ArgErrDetail but for `fmt` checks since compiler has not run yet. +type ArityFormatErrDetail = v1.ArityFormatErrDetail + +// arityMismatchError but for `fmt` checks since the compiler has not run yet. +func ArityFormatMismatchError(operands []*ast.Term, operator string, loc *ast.Location, f *types.Function) *ast.Error { + return v1.ArityFormatMismatchError(operands, operator, loc, f) +} diff --git a/format/format_test.go b/format/format_test.go new file mode 100644 index 0000000000..669cac017d --- /dev/null +++ b/format/format_test.go @@ -0,0 +1,152 @@ +// Copyright 2024 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 format + +import ( + "strings" + "testing" + + "github.com/open-policy-agent/opa/ast" +) + +func TestSource_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expFormatted string + expErrs []string + }{ + { + note: "v0", // from default rego-version + module: `package test + +p[x] { + x = "a" +}`, + expFormatted: `package test + +p[x] { + x = "a" +} +`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x = "a" +}`, + expErrs: []string{ + "rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + formatted, err := Source("test.rego", []byte(tc.module)) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%q", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + formattedStr := string(formatted) + if formattedStr != tc.expFormatted { + t.Fatalf("expected %q but got %q", tc.expFormatted, formattedStr) + } + } + }) + } +} + +func TestSourceWithOpts_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + toRegoVersion ast.RegoVersion + module string + expFormatted string + expErrs []string + }{ + { + note: "v0 -> v0", // from default rego-version + toRegoVersion: ast.RegoV0, + module: `package test + +p[x] { + x = "a" +}`, + expFormatted: `package test + +p[x] { + x = "a" +} +`, + }, + { + note: "v0 -> v1", // from default rego-version + toRegoVersion: ast.RegoV1, + module: `package test + +p[x] { + x = "a" +}`, + expFormatted: `package test + +p contains x if { + x = "a" +} +`, + }, + { + note: "v1 -> v1", // from non-default rego-version + toRegoVersion: ast.RegoV1, + module: `package test + +p contains x if { + x = "a" +}`, + expErrs: []string{ + "rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + formatted, err := SourceWithOpts("test.rego", []byte(tc.module), Opts{RegoVersion: tc.toRegoVersion}) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%q", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + formattedStr := string(formatted) + if formattedStr != tc.expFormatted { + t.Fatalf("expected %q but got %q", tc.expFormatted, formattedStr) + } + } + }) + } +} diff --git a/hooks/doc.go b/hooks/doc.go new file mode 100644 index 0000000000..6c50924274 --- /dev/null +++ b/hooks/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package hooks diff --git a/hooks/hooks.go b/hooks/hooks.go new file mode 100644 index 0000000000..110398873b --- /dev/null +++ b/hooks/hooks.go @@ -0,0 +1,46 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/hooks" +) + +// 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 = v1.Hook + +// Hooks is the type used for every struct in OPA that can work with hooks. +type Hooks = v1.Hooks + +// New creates a new instance of Hooks. +func New(hs ...Hook) Hooks { + return v1.New(hs...) +} + +// 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 = v1.ConfigHook + +// ConfigHook allows inspecting or rewriting the discovered configuration when +// the discovery plugin is processing it. +type ConfigDiscoveryHook = v1.ConfigDiscoveryHook diff --git a/internal/compiler/utils.go b/internal/compiler/utils.go index dbda9b4f9c..dfb781e19b 100644 --- a/internal/compiler/utils.go +++ b/internal/compiler/utils.go @@ -32,7 +32,10 @@ func VerifyAuthorizationPolicySchema(compiler *ast.Compiler, ref ast.Ref) error schemaSet := ast.NewSchemaSet() schemaSet.Put(ast.SchemaRootRef, schemaDefinitions[AuthorizationPolicySchema]) - errs := ast.NewCompiler().WithSchemas(schemaSet).PassesTypeCheckRules(rules) + errs := ast.NewCompiler(). + WithDefaultRegoVersion(compiler.DefaultRegoVersion()). + WithSchemas(schemaSet). + PassesTypeCheckRules(rules) if len(errs) > 0 { return errs diff --git a/internal/pathwatcher/utils.go b/internal/pathwatcher/utils.go index 811c35b2be..ee7fb794cf 100644 --- a/internal/pathwatcher/utils.go +++ b/internal/pathwatcher/utils.go @@ -42,7 +42,7 @@ func CreatePathWatcher(rootPaths []string) (*fsnotify.Watcher, error) { // ProcessWatcherUpdate handles an occurrence of a watcher event func ProcessWatcherUpdate(ctx context.Context, paths []string, removed string, store storage.Store, filter loader.Filter, asBundle bool, f func(context.Context, storage.Transaction, *initload.LoadPathsResult) error) error { - return ProcessWatcherUpdateForRegoVersion(ctx, ast.RegoV0, paths, removed, store, filter, asBundle, f) + return ProcessWatcherUpdateForRegoVersion(ctx, ast.DefaultRegoVersion, paths, removed, store, filter, asBundle, f) } func ProcessWatcherUpdateForRegoVersion(ctx context.Context, regoVersion ast.RegoVersion, paths []string, removed string, store storage.Store, filter loader.Filter, asBundle bool, @@ -75,7 +75,7 @@ func ProcessWatcherUpdateForRegoVersion(ctx context.Context, regoVersion ast.Reg if err != nil { return err } - module, err := ast.ParseModule(id, string(bs)) + module, err := ast.ParseModuleWithOpts(id, string(bs), ast.ParserOptions{RegoVersion: regoVersion}) if err != nil { return err } diff --git a/internal/report/report.go b/internal/report/report.go index 5bcf5554fb..55f4cfe210 100644 --- a/internal/report/report.go +++ b/internal/report/report.go @@ -19,7 +19,7 @@ import ( "github.com/open-policy-agent/opa/v1/keys" "github.com/open-policy-agent/opa/v1/logging" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" "github.com/open-policy-agent/opa/v1/plugins/rest" "github.com/open-policy-agent/opa/v1/util" diff --git a/internal/runtime/init/init.go b/internal/runtime/init/init.go index d2ded043e4..814847a12a 100644 --- a/internal/runtime/init/init.go +++ b/internal/runtime/init/init.go @@ -53,6 +53,7 @@ func InsertAndCompile(ctx context.Context, opts InsertAndCompileOptions) (*Inser } compiler := ast.NewCompiler(). + WithDefaultRegoVersion(opts.ParserOptions.RegoVersion). SetErrorLimit(opts.MaxErrors). WithPathConflictsCheck(storage.NonEmpty(ctx, opts.Store, opts.Txn)). WithEnablePrintStatements(opts.EnablePrintStatements) diff --git a/internal/runtime/init/init_test.go b/internal/runtime/init/init_test.go index 7480d27469..9f60beb989 100644 --- a/internal/runtime/init/init_test.go +++ b/internal/runtime/init/init_test.go @@ -24,7 +24,7 @@ import ( inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test" "github.com/open-policy-agent/opa/v1/util" "github.com/open-policy-agent/opa/v1/util/test" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) func TestInit(t *testing.T) { diff --git a/internal/runtime/runtime.go b/internal/runtime/runtime.go index 8144a88771..85b49e307f 100644 --- a/internal/runtime/runtime.go +++ b/internal/runtime/runtime.go @@ -11,7 +11,7 @@ import ( "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) // Params controls the types of runtime information to return. diff --git a/internal/version/version.go b/internal/version/version.go index 371edc428b..dc52733fc2 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -11,7 +11,7 @@ import ( "runtime" "github.com/open-policy-agent/opa/v1/storage" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) var versionPath = storage.MustParsePath("/system/version") diff --git a/internal/wasm/sdk/opa/opa_test.go b/internal/wasm/sdk/opa/opa_test.go index 4a04a5177f..8a6935eb20 100644 --- a/internal/wasm/sdk/opa/opa_test.go +++ b/internal/wasm/sdk/opa/opa_test.go @@ -119,8 +119,8 @@ func TestOPA(t *testing.T) { }, { Description: "Runtime error/var assignment conflict", - Policy: `a = "b" { input > 1 } -a = "c" { input > 2 }`, + Policy: `a = "b" if { input > 1 } +a = "c" if { input > 2 }`, Query: "data.p.a = x", Evals: []Eval{ {Input: "3"}, @@ -131,10 +131,10 @@ a = "c" { input > 2 }`, Description: "Runtime error/else conflict-1", Query: `data.p.q`, Policy: ` - q { + q if { false } - else = true { + else = true if { true } q = false`, @@ -145,16 +145,16 @@ a = "c" { input > 2 }`, Description: "Runtime error/else conflict-2", Query: `data.p.q`, Policy: ` - q { + q if { false } - else = false { + else = false if { true } - q { + q if { false } - else = true { + else = true if { true }`, Evals: []Eval{{}}, @@ -167,7 +167,7 @@ a = "c" { input > 2 }`, Description: "Only input changing, regex.match", Policy: ` default hello = false - hello { + hello if { regex.match("^world$", input.message) }`, Query: "data.p.hello = x", @@ -180,7 +180,7 @@ a = "c" { input > 2 }`, Description: "Only input changing, glob.match", Policy: ` default hello = false - hello { + hello if { glob.match("world", [":"], input.message) }`, Query: "data.p.hello = x", @@ -215,7 +215,7 @@ a = "c" { input > 2 }`, { Description: "mpd init problem (#3110)", Query: `data.p.main = x`, - Policy: `main { numbers.range(1, 2)[_] == 2 }`, + Policy: `main if { numbers.range(1, 2)[_] == 2 }`, Evals: []Eval{ {Result: `{{"x": true}}`}, {Result: `{{"x": true}}`}, diff --git a/internal/wasm/sdk/test/e2e/external_test.go b/internal/wasm/sdk/test/e2e/external_test.go index 023b5b7230..cde1a0af3b 100644 --- a/internal/wasm/sdk/test/e2e/external_test.go +++ b/internal/wasm/sdk/test/e2e/external_test.go @@ -29,7 +29,7 @@ import ( const opaRootDir = "../../../../../" -var caseDir = flag.String("case-dir", filepath.Join(opaRootDir, "test/cases/testdata/"), "set directory to load test cases from") +var caseDir = flag.String("case-dir", filepath.Join(opaRootDir, "v1/test/cases/testdata/"), "set directory to load test cases from") var exceptionsFile = flag.String("exceptions", "./exceptions.yaml", "set file to load a list of test names to exclude") var exceptions map[string]string diff --git a/ir/doc.go b/ir/doc.go new file mode 100644 index 0000000000..6839297f2a --- /dev/null +++ b/ir/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package ir diff --git a/ir/encoding/doc.go b/ir/encoding/doc.go new file mode 100644 index 0000000000..bf2818e524 --- /dev/null +++ b/ir/encoding/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package encoding diff --git a/ir/encoding/encoding_test.go b/ir/encoding/encoding_test.go new file mode 100644 index 0000000000..b4c88d493c --- /dev/null +++ b/ir/encoding/encoding_test.go @@ -0,0 +1,72 @@ +package encoding + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/internal/planner" + "github.com/open-policy-agent/opa/ir" +) + +func TestRoundTrip(t *testing.T) { + + // Note: v0 module + c, err := ast.CompileModules(map[string]string{ + "test.rego": ` + package test + + p { + input.foo == 7 + } + `, + }) + + if err != nil { + t.Fatal(err) + } + + modules := []*ast.Module{} + + for _, m := range c.Modules { + modules = append(modules, m) + } + + planner := planner.New(). + WithQueries([]planner.QuerySet{ + { + Name: "main", + Queries: []ast.Body{ + ast.MustParseBody("data.test.p = true"), + }, + }, + }). + WithModules(modules). + WithBuiltinDecls(ast.BuiltinMap) + + plan, err := planner.Plan() + if err != nil { + t.Fatal(err) + } + + bs, err := json.MarshalIndent(plan, "", " ") + if err != nil { + t.Fatal(err) + } + + var cpy ir.Policy + err = json.Unmarshal(bs, &cpy) + if err != nil { + t.Fatal(err) + } + + bs2, err := json.MarshalIndent(plan, "", " ") + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(bs, bs2) { + t.Fatal("expected bytes to be equal") + } +} diff --git a/ir/ir.go b/ir/ir.go new file mode 100644 index 0000000000..d43fc56e90 --- /dev/null +++ b/ir/ir.go @@ -0,0 +1,217 @@ +// 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 ir defines an intermediate representation (IR) for Rego. +// +// The IR specifies an imperative execution model for Rego policies similar to a +// query plan in traditional databases. +package ir + +import ( + v1 "github.com/open-policy-agent/opa/v1/ir" +) + +type ( + // Policy represents a planned policy query. + Policy = v1.Policy + + // Static represents a static data segment that is indexed into by the policy. + Static = v1.Static + + // BuiltinFunc represents a built-in function that may be required by the + // policy. + BuiltinFunc = v1.BuiltinFunc + + // Plans represents a collection of named query plans to expose in the policy. + Plans = v1.Plans + + // Funcs represents a collection of planned functions to include in the + // policy. + Funcs = v1.Funcs + + // Func represents a named plan (function) that can be invoked. Functions + // accept one or more parameters and return a value. By convention, the + // input document and data documents are always passed as the first and + // second arguments (respectively). + Func = v1.Func + + // Plan represents an ordered series of blocks to execute. Plan execution + // stops when a return statement is reached. Blocks are executed in-order. + Plan = v1.Plan + + // Block represents an ordered sequence of statements to execute. Blocks are + // executed until a return statement is encountered, a statement is undefined, + // or there are no more statements. If all statements are defined but no return + // statement is encountered, the block is undefined. + Block = v1.Block + + // Stmt represents an operation (e.g., comparison, loop, dot, etc.) to execute. + Stmt = v1.Stmt + + // Local represents a plan-scoped variable. + // + // TODO(tsandall): should this be int32 for safety? + Local = v1.Local + + // StringConst represents a string value. + StringConst = v1.StringConst +) + +const ( + // Input is the local variable that refers to the global input document. + Input = v1.Input + + // Data is the local variable that refers to the global data document. + Data = v1.Data + + // Unused is the free local variable that can be allocated in a plan. + Unused = v1.Unused +) + +// Operand represents a value that a statement operates on. +type Operand = v1.Operand + +// Val represents an abstract value that statements operate on. There are currently +// 3 types of values: +// +// 1. Local - a local variable that can refer to any type. +// 2. StringIndex - a string constant that refers to a compiled string. +// 3. Bool - a boolean constant. +type Val = v1.Val + +// StringIndex represents the index into the plan's list of constant strings +// of a constant string. +type StringIndex = v1.StringIndex + +// Bool represents a constant boolean. +type Bool = v1.Bool + +// ReturnLocalStmt represents a return statement that yields a local value. +type ReturnLocalStmt = v1.ReturnLocalStmt + +// CallStmt represents a named function call. The result should be stored in the +// result local. +type CallStmt = v1.CallStmt + +// CallDynamicStmt represents an indirect (data) function call. The result should +// be stored in the result local. +type CallDynamicStmt = v1.CallDynamicStmt + +// BlockStmt represents a nested block. Nested blocks and break statements can +// be used to short-circuit execution. +type BlockStmt = v1.BlockStmt + +// BreakStmt represents a jump out of the current block. The index specifies how +// many blocks to jump starting from zero (the current block). Execution will +// continue from the end of the block that is jumped to. +type BreakStmt = v1.BreakStmt + +// DotStmt represents a lookup operation on a value (e.g., array, object, etc.) +// The source of a DotStmt may be a scalar value in which case the statement +// will be undefined. +type DotStmt = v1.DotStmt + +// LenStmt represents a length() operation on a local variable. The +// result is stored in the target local variable. +type LenStmt = v1.LenStmt + +// ScanStmt represents a linear scan over a composite value. The +// source may be a scalar in which case the block will never execute. +type ScanStmt = v1.ScanStmt + +// NotStmt represents a negated statement. +type NotStmt = v1.NotStmt + +// AssignIntStmt represents an assignment of an integer value to a +// local variable. +type AssignIntStmt = v1.AssignIntStmt + +// AssignVarStmt represents an assignment of one local variable to another. +type AssignVarStmt = v1.AssignVarStmt + +// AssignVarOnceStmt represents an assignment of one local variable to another. +// If the target is defined, execution aborts with a conflict error. +// +// TODO(tsandall): is there a better name for this? +type AssignVarOnceStmt = v1.AssignVarOnceStmt + +// ResetLocalStmt resets a local variable to 0. +type ResetLocalStmt = v1.ResetLocalStmt + +// MakeNullStmt constructs a local variable that refers to a null value. +type MakeNullStmt = v1.MakeNullStmt + +// MakeNumberIntStmt constructs a local variable that refers to an integer value. +type MakeNumberIntStmt = v1.MakeNumberIntStmt + +// MakeNumberRefStmt constructs a local variable that refers to a number stored as a string. +type MakeNumberRefStmt = v1.MakeNumberRefStmt + +// MakeArrayStmt constructs a local variable that refers to an array value. +type MakeArrayStmt = v1.MakeArrayStmt + +// MakeObjectStmt constructs a local variable that refers to an object value. +type MakeObjectStmt = v1.MakeObjectStmt + +// MakeSetStmt constructs a local variable that refers to a set value. +type MakeSetStmt = v1.MakeSetStmt + +// EqualStmt represents an value-equality check of two local variables. +type EqualStmt = v1.EqualStmt + +// NotEqualStmt represents a != check of two local variables. +type NotEqualStmt = v1.NotEqualStmt + +// IsArrayStmt represents a dynamic type check on a local variable. +type IsArrayStmt = v1.IsArrayStmt + +// IsObjectStmt represents a dynamic type check on a local variable. +type IsObjectStmt = v1.IsObjectStmt + +// IsSetStmt represents a dynamic type check on a local variable. +type IsSetStmt = v1.IsSetStmt + +// IsDefinedStmt represents a check of whether a local variable is defined. +type IsDefinedStmt = v1.IsDefinedStmt + +// IsUndefinedStmt represents a check of whether local variable is undefined. +type IsUndefinedStmt = v1.IsUndefinedStmt + +// ArrayAppendStmt represents a dynamic append operation of a value +// onto an array. +type ArrayAppendStmt = v1.ArrayAppendStmt + +// ObjectInsertStmt represents a dynamic insert operation of a +// key/value pair into an object. +type ObjectInsertStmt = v1.ObjectInsertStmt + +// ObjectInsertOnceStmt represents a dynamic insert operation of a key/value +// pair into an object. If the key already exists and the value differs, +// execution aborts with a conflict error. +type ObjectInsertOnceStmt = v1.ObjectInsertOnceStmt + +// ObjectMergeStmt performs a recursive merge of two object values. If either of +// the locals refer to non-object values this operation will abort with a +// conflict error. Overlapping object keys are merged recursively. +type ObjectMergeStmt = v1.ObjectMergeStmt + +// SetAddStmt represents a dynamic add operation of an element into a set. +type SetAddStmt = v1.SetAddStmt + +// WithStmt replaces the Local or a portion of the document referred to by the +// Local with the Value and executes the contained block. If the Path is +// non-empty, the Value is upserted into the Local. If the intermediate nodes in +// the Local referred to by the Path do not exist, they will be created. When +// the WithStmt finishes the Local is reset to it's original value. +type WithStmt = v1.WithStmt + +// NopStmt adds a nop instruction. Useful during development and debugging only. +type NopStmt = v1.NopStmt + +// ResultSetAddStmt adds a value into the result set returned by the query plan. +type ResultSetAddStmt = v1.ResultSetAddStmt + +// Location records the filen index, and the row and column inside that file +// that a statement can be connected to. +type Location = v1.Location diff --git a/ir/pretty.go b/ir/pretty.go new file mode 100644 index 0000000000..2fb6af0538 --- /dev/null +++ b/ir/pretty.go @@ -0,0 +1,16 @@ +// 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 ir + +import ( + "io" + + v1 "github.com/open-policy-agent/opa/v1/ir" +) + +// Pretty writes a human-readable representation of an IR object to w. +func Pretty(w io.Writer, x interface{}) error { + return v1.Pretty(w, x) +} diff --git a/ir/walk.go b/ir/walk.go new file mode 100644 index 0000000000..9af8c2eaff --- /dev/null +++ b/ir/walk.go @@ -0,0 +1,15 @@ +// 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 ir + +import v1 "github.com/open-policy-agent/opa/v1/ir" + +// Visitor defines the interface for visiting IR nodes. +type Visitor = v1.Visitor + +// Walk invokes the visitor for nodes under x. +func Walk(vis Visitor, x interface{}) error { + return v1.Walk(vis, x) +} diff --git a/keys/doc.go b/keys/doc.go new file mode 100644 index 0000000000..ffcc0f7ca0 --- /dev/null +++ b/keys/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package keys diff --git a/keys/keys.go b/keys/keys.go new file mode 100644 index 0000000000..9eca2effcb --- /dev/null +++ b/keys/keys.go @@ -0,0 +1,25 @@ +package keys + +import ( + "encoding/json" + + v1 "github.com/open-policy-agent/opa/v1/keys" +) + +// IsSupportedAlgorithm true if provided alg is supported +func IsSupportedAlgorithm(alg string) bool { + return v1.IsSupportedAlgorithm(alg) +} + +// Config holds the keys used to sign or verify bundles and tokens +type Config = v1.Config + +// NewKeyConfig return a new Config +func NewKeyConfig(key, alg, scope string) (*Config, error) { + return v1.NewKeyConfig(key, alg, scope) +} + +// ParseKeysConfig returns a map containing the key and the signing algorithm +func ParseKeysConfig(raw json.RawMessage) (map[string]*Config, error) { + return v1.ParseKeysConfig(raw) +} diff --git a/loader/doc.go b/loader/doc.go new file mode 100644 index 0000000000..9f60920d95 --- /dev/null +++ b/loader/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package loader diff --git a/loader/errors.go b/loader/errors.go new file mode 100644 index 0000000000..8dc70b8673 --- /dev/null +++ b/loader/errors.go @@ -0,0 +1,12 @@ +// Copyright 2017 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 loader + +import ( + v1 "github.com/open-policy-agent/opa/v1/loader" +) + +// Errors is a wrapper for multiple loader errors. +type Errors = v1.Errors diff --git a/loader/extension/doc.go b/loader/extension/doc.go new file mode 100644 index 0000000000..cf457092d2 --- /dev/null +++ b/loader/extension/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package extension diff --git a/loader/extension/extension.go b/loader/extension/extension.go new file mode 100644 index 0000000000..2cae4febcb --- /dev/null +++ b/loader/extension/extension.go @@ -0,0 +1,29 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/loader/extension" +) + +// 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 = v1.Handler + +// 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) { + v1.RegisterExtension(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 { + return v1.FindExtension(ext) +} diff --git a/loader/filter/doc.go b/loader/filter/doc.go new file mode 100644 index 0000000000..a6138df6a2 --- /dev/null +++ b/loader/filter/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package filter diff --git a/loader/filter/filter.go b/loader/filter/filter.go new file mode 100644 index 0000000000..cfd03b33a8 --- /dev/null +++ b/loader/filter/filter.go @@ -0,0 +1,5 @@ +package filter + +import v1 "github.com/open-policy-agent/opa/v1/loader/filter" + +type LoaderFilter = v1.LoaderFilter diff --git a/loader/loader.go b/loader/loader.go new file mode 100644 index 0000000000..9b2f91d4e9 --- /dev/null +++ b/loader/loader.go @@ -0,0 +1,145 @@ +// Copyright 2017 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 loader contains utilities for loading files into OPA. +package loader + +import ( + "io/fs" + "os" + "strings" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/bundle" + v1 "github.com/open-policy-agent/opa/v1/loader" +) + +// Result represents the result of successfully loading zero or more files. +type Result = v1.Result + +// RegoFile represents the result of loading a single Rego source file. +type RegoFile = v1.RegoFile + +// Filter defines the interface for filtering files during loading. If the +// filter returns true, the file should be excluded from the result. +type Filter = v1.Filter + +// GlobExcludeName excludes files and directories whose names do not match the +// shell style pattern at minDepth or greater. +func GlobExcludeName(pattern string, minDepth int) Filter { + return v1.GlobExcludeName(pattern, minDepth) +} + +// FileLoader defines an interface for loading OPA data files +// and Rego policies. +type FileLoader = v1.FileLoader + +// NewFileLoader returns a new FileLoader instance. +func NewFileLoader() FileLoader { + return v1.NewFileLoader().WithRegoVersion(ast.DefaultRegoVersion) +} + +// GetBundleDirectoryLoader returns a bundle directory loader which can be used to load +// files in the directory +func GetBundleDirectoryLoader(path string) (bundle.DirectoryLoader, bool, error) { + return v1.GetBundleDirectoryLoader(path) +} + +// 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 v1.GetBundleDirectoryLoaderWithFilter(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) { + return v1.GetBundleDirectoryLoaderFS(fsys, path, filter) +} + +// FilteredPaths is the same as FilterPathsFS using the current diretory file +// system +func FilteredPaths(paths []string, filter Filter) ([]string, error) { + return v1.FilteredPaths(paths, filter) +} + +// FilteredPathsFS return a list of files from the specified +// paths while applying the given filters. If any filter returns true, the +// file/directory is excluded. +func FilteredPathsFS(fsys fs.FS, paths []string, filter Filter) ([]string, error) { + return v1.FilteredPathsFS(fsys, paths, filter) +} + +// Schemas loads a schema set from the specified file path. +func Schemas(schemaPath string) (*ast.SchemaSet, error) { + return v1.Schemas(schemaPath) +} + +// All returns a Result object loaded (recursively) from the specified paths. +// Deprecated: Use FileLoader.Filtered() instead. +func All(paths []string) (*Result, error) { + return NewFileLoader().Filtered(paths, nil) +} + +// Filtered returns a Result object loaded (recursively) from the specified +// paths while applying the given filters. If any filter returns true, the +// file/directory is excluded. +// Deprecated: Use FileLoader.Filtered() instead. +func Filtered(paths []string, filter Filter) (*Result, error) { + return NewFileLoader().Filtered(paths, filter) +} + +// AsBundle loads a path as a bundle. If it is a single file +// it will be treated as a normal tarball bundle. If a directory +// is supplied it will be loaded as an unzipped bundle tree. +// Deprecated: Use FileLoader.AsBundle() instead. +func AsBundle(path string) (*bundle.Bundle, error) { + return NewFileLoader().AsBundle(path) +} + +// AllRegos returns a Result object loaded (recursively) with all Rego source +// files from the specified paths. +func AllRegos(paths []string) (*Result, error) { + return NewFileLoader().Filtered(paths, func(_ string, info os.FileInfo, _ int) bool { + return !info.IsDir() && !strings.HasSuffix(info.Name(), bundle.RegoExt) + }) +} + +// Rego is deprecated. Use RegoWithOpts instead. +func Rego(path string) (*RegoFile, error) { + return RegoWithOpts(path, ast.ParserOptions{}) +} + +// RegoWithOpts returns a RegoFile object loaded from the given path. +func RegoWithOpts(path string, opts ast.ParserOptions) (*RegoFile, error) { + if opts.RegoVersion == ast.RegoUndefined { + opts.RegoVersion = ast.DefaultRegoVersion + } + + return v1.RegoWithOpts(path, opts) +} + +// CleanPath returns the normalized version of a path that can be used as an identifier. +func CleanPath(path string) string { + return v1.CleanPath(path) +} + +// Paths returns a sorted list of files contained at path. If recurse is true +// and path is a directory, then Paths will walk the directory structure +// recursively and list files at each level. +func Paths(path string, recurse bool) (paths []string, err error) { + return v1.Paths(path, recurse) +} + +// Dirs resolves filepaths to directories. It will return a list of unique +// directories. +func Dirs(paths []string) []string { + return v1.Dirs(paths) +} + +// SplitPrefix returns a tuple specifying the document prefix and the file +// path. +func SplitPrefix(path string) ([]string, string) { + return v1.SplitPrefix(path) +} diff --git a/loader/loader_test.go b/loader/loader_test.go new file mode 100644 index 0000000000..d9070e2562 --- /dev/null +++ b/loader/loader_test.go @@ -0,0 +1,364 @@ +// Copyright 2024 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 loader + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/util/test" +) + +func TestAll_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package test + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := All([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestFiltered_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package test + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + filter := func(string, os.FileInfo, int) bool { + return false + } + + loaded, err := Filtered([]string{moduleFile}, filter) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestRego_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package test + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := Rego(moduleFile) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Parsed) + } + } + }) + }) + } +} + +func TestAllRegos_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package test + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := AllRegos([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestLoadRego_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", // v0 is the default rego-version + module: `package test + +p[x] { + x := "a" +}`, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", + module: `package test + +p contains x if { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: var cannot be used for rule name", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := NewFileLoader().All([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} diff --git a/logging/doc.go b/logging/doc.go new file mode 100644 index 0000000000..665cb369a6 --- /dev/null +++ b/logging/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package logging diff --git a/logging/logging.go b/logging/logging.go new file mode 100644 index 0000000000..f84deb86bd --- /dev/null +++ b/logging/logging.go @@ -0,0 +1,79 @@ +package logging + +import ( + "context" + + v1 "github.com/open-policy-agent/opa/v1/logging" +) + +// Level log level for Logger +type Level = v1.Level + +const ( + // Error error log level + Error = v1.Error + // Warn warn log level + Warn = v1.Warn + // Info info log level + Info = v1.Info + // Debug debug log level + Debug = v1.Debug +) + +// Logger provides interface for OPA logger implementations +type Logger = v1.Logger + +// StandardLogger is the default OPA logger implementation. +type StandardLogger = v1.StandardLogger + +// New returns a new standard logger. +func New() *StandardLogger { + return v1.New() +} + +// Get returns the standard logger used throughout OPA. +// +// Deprecated. Do not rely on the global logger. +func Get() *StandardLogger { + return v1.Get() +} + +// NoOpLogger logging implementation that does nothing +type NoOpLogger = v1.NoOpLogger + +// NewNoOpLogger instantiates new NoOpLogger +func NewNoOpLogger() *NoOpLogger { + return v1.NewNoOpLogger() +} + +// RequestContext represents the request context used to store data +// related to the request that could be used on logs. +type RequestContext = v1.RequestContext + +type HTTPRequestContext = v1.HTTPRequestContext + +// NewContext returns a copy of parent with an associated RequestContext. +func NewContext(parent context.Context, val *RequestContext) context.Context { + return v1.NewContext(parent, val) +} + +// FromContext returns the RequestContext associated with ctx, if any. +func FromContext(ctx context.Context) (*RequestContext, bool) { + return v1.FromContext(ctx) +} + +func WithHTTPRequestContext(parent context.Context, val *HTTPRequestContext) context.Context { + return v1.WithHTTPRequestContext(parent, val) +} + +func HTTPRequestContextFromContext(ctx context.Context) (*HTTPRequestContext, bool) { + return v1.HTTPRequestContextFromContext(ctx) +} + +func WithDecisionID(parent context.Context, id string) context.Context { + return v1.WithDecisionID(parent, id) +} + +func DecisionIDFromContext(ctx context.Context) (string, bool) { + return v1.DecisionIDFromContext(ctx) +} diff --git a/logging/test/doc.go b/logging/test/doc.go new file mode 100644 index 0000000000..dffe11ff7c --- /dev/null +++ b/logging/test/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package test diff --git a/logging/test/test.go b/logging/test/test.go new file mode 100644 index 0000000000..1379c1eea3 --- /dev/null +++ b/logging/test/test.go @@ -0,0 +1,16 @@ +package test + +import ( + v1 "github.com/open-policy-agent/opa/v1/logging/test" +) + +// LogEntry represents a log message. +type LogEntry = v1.LogEntry + +// Logger implementation that buffers messages for test purposes. +type Logger = v1.Logger + +// New instantiates new Logger. +func New() *Logger { + return v1.New() +} diff --git a/v1/logo/logo-144x144.png b/logo/logo-144x144.png similarity index 100% rename from v1/logo/logo-144x144.png rename to logo/logo-144x144.png diff --git a/v1/logo/logo.png b/logo/logo.png similarity index 100% rename from v1/logo/logo.png rename to logo/logo.png diff --git a/v1/logo/logo.svg b/logo/logo.svg similarity index 100% rename from v1/logo/logo.svg rename to logo/logo.svg diff --git a/metrics/doc.go b/metrics/doc.go new file mode 100644 index 0000000000..6a306991e7 --- /dev/null +++ b/metrics/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package metrics diff --git a/metrics/metrics.go b/metrics/metrics.go new file mode 100644 index 0000000000..2d2ae78b1a --- /dev/null +++ b/metrics/metrics.go @@ -0,0 +1,57 @@ +// Copyright 2017 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 metrics contains helpers for performance metric management inside the policy engine. +package metrics + +import ( + v1 "github.com/open-policy-agent/opa/v1/metrics" +) + +// Well-known metric names. +const ( + BundleRequest = v1.BundleRequest + ServerHandler = v1.ServerHandler + ServerQueryCacheHit = v1.ServerQueryCacheHit + SDKDecisionEval = v1.SDKDecisionEval + RegoQueryCompile = v1.RegoQueryCompile + RegoQueryEval = v1.RegoQueryEval + RegoQueryParse = v1.RegoQueryParse + RegoModuleParse = v1.RegoModuleParse + RegoDataParse = v1.RegoDataParse + RegoModuleCompile = v1.RegoModuleCompile + RegoPartialEval = v1.RegoPartialEval + RegoInputParse = v1.RegoInputParse + RegoLoadFiles = v1.RegoLoadFiles + RegoLoadBundles = v1.RegoLoadBundles + RegoExternalResolve = v1.RegoExternalResolve +) + +// Info contains attributes describing the underlying metrics provider. +type Info = v1.Info + +// Metrics defines the interface for a collection of performance metrics in the +// policy engine. +type Metrics = v1.Metrics + +type TimerMetrics = v1.TimerMetrics + +// New returns a new Metrics object. +func New() Metrics { + return v1.New() +} + +// Timer defines the interface for a restartable timer that accumulates elapsed +// time. +type Timer = v1.Timer + +// Histogram defines the interface for a histogram with hardcoded percentiles. +type Histogram = v1.Histogram + +// Counter defines the interface for a monotonic increasing counter. +type Counter = v1.Counter + +func Statistics(num ...int64) interface{} { + return v1.Statistics(num...) +} diff --git a/plugins/bundle/config.go b/plugins/bundle/config.go new file mode 100644 index 0000000000..fe103e3575 --- /dev/null +++ b/plugins/bundle/config.go @@ -0,0 +1,42 @@ +// 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 bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/bundle" +) + +// ParseConfig validates the config and injects default values. This is +// for the legacy single bundle configuration. This will add the bundle +// to the `Bundles` map to provide compatibility with newer clients. +// Deprecated: Use `ParseBundlesConfig` with `bundles` OPA config option instead +func ParseConfig(config []byte, services []string) (*Config, error) { + return v1.ParseConfig(config, services) +} + +// ParseBundlesConfig validates the config and injects default values for +// the defined `bundles`. This expects a map of bundle names to resource +// configurations. +func ParseBundlesConfig(config []byte, services []string) (*Config, error) { + return v1.ParseBundlesConfig(config, services) +} + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the bundle config +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// Config represents the configuration of the plugin. +// The Config can define a single bundle source or a map of +// `Source` objects defining where/how to download bundles. The +// older single bundle configuration is deprecated and will be +// removed in the future in favor of the `Bundles` map. +type Config = v1.Config + +// Source is a configured bundle source to download bundles from +type Source = v1.Source diff --git a/plugins/bundle/doc.go b/plugins/bundle/doc.go new file mode 100644 index 0000000000..7ec7c9b332 --- /dev/null +++ b/plugins/bundle/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package bundle diff --git a/plugins/bundle/errors.go b/plugins/bundle/errors.go new file mode 100644 index 0000000000..965c704d33 --- /dev/null +++ b/plugins/bundle/errors.go @@ -0,0 +1,14 @@ +package bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/bundle" +) + +// Errors represents a list of errors that occurred during a bundle load enriched by the bundle name. +type Errors = v1.Errors + +type Error = v1.Error + +func NewBundleError(bundleName string, cause error) Error { + return v1.NewBundleError(bundleName, cause) +} diff --git a/plugins/bundle/plugin.go b/plugins/bundle/plugin.go new file mode 100644 index 0000000000..dffe3a322b --- /dev/null +++ b/plugins/bundle/plugin.go @@ -0,0 +1,31 @@ +// 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 bundle implements bundle loading. +package bundle + +import ( + "github.com/open-policy-agent/opa/plugins" + v1 "github.com/open-policy-agent/opa/v1/plugins/bundle" +) + +// Loader defines the interface that the bundle plugin uses to control bundle +// loading via HTTP, disk, etc. +type Loader = v1.Loader + +// Plugin implements bundle activation. +type Plugin = v1.Plugin + +// New returns a new Plugin with the given config. +func New(parsedConfig *Config, manager *plugins.Manager) *Plugin { + return v1.New(parsedConfig, manager) +} + +// Name identifies the plugin on manager. +const Name = v1.Name + +// Lookup returns the bundle plugin registered with the manager. +func Lookup(manager *plugins.Manager) *Plugin { + return v1.Lookup(manager) +} diff --git a/plugins/bundle/status.go b/plugins/bundle/status.go new file mode 100644 index 0000000000..c49f7f4a5b --- /dev/null +++ b/plugins/bundle/status.go @@ -0,0 +1,12 @@ +// 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 bundle + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/bundle" +) + +// Status represents the status of processing a bundle. +type Status = v1.Status diff --git a/plugins/discovery/config.go b/plugins/discovery/config.go new file mode 100644 index 0000000000..23034d18b7 --- /dev/null +++ b/plugins/discovery/config.go @@ -0,0 +1,25 @@ +// 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 discovery + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/discovery" +) + +// Config represents the configuration for the discovery feature. +type Config = v1.Config + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the discovery config +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} + +// ParseConfig returns a valid Config object with defaults injected. +func ParseConfig(bs []byte, services []string) (*Config, error) { + return v1.ParseConfig(bs, services) +} diff --git a/plugins/discovery/discovery.go b/plugins/discovery/discovery.go new file mode 100644 index 0000000000..8ab4a1f9b6 --- /dev/null +++ b/plugins/discovery/discovery.go @@ -0,0 +1,52 @@ +// 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 discovery implements configuration discovery. +package discovery + +import ( + "github.com/open-policy-agent/opa/plugins" + "github.com/open-policy-agent/opa/v1/hooks" + "github.com/open-policy-agent/opa/v1/metrics" + v1 "github.com/open-policy-agent/opa/v1/plugins/discovery" +) + +const ( + // Name is the discovery plugin name that will be registered with the plugin manager. + Name = v1.Name +) + +// Discovery implements configuration discovery for OPA. When discovery is +// started it will periodically download a configuration bundle and try to +// reconfigure the OPA. +type Discovery = v1.Discovery + +// Factories provides a set of factory functions to use for +// instantiating custom plugins. +func Factories(fs map[string]plugins.Factory) func(*Discovery) { + return v1.Factories(fs) +} + +// Metrics provides a metrics provider to pass to plugins. +func Metrics(m metrics.Metrics) func(*Discovery) { + return v1.Metrics(m) +} + +func Hooks(hs hooks.Hooks) func(*Discovery) { + return v1.Hooks(hs) +} + +func BootConfig(bootConfig map[string]interface{}) func(*Discovery) { + return v1.BootConfig(bootConfig) +} + +// New returns a new discovery plugin. +func New(manager *plugins.Manager, opts ...func(*Discovery)) (*Discovery, error) { + return v1.New(manager, opts...) +} + +// Lookup returns the discovery plugin registered with the manager. +func Lookup(manager *plugins.Manager) *Discovery { + return v1.Lookup(manager) +} diff --git a/plugins/discovery/doc.go b/plugins/discovery/doc.go new file mode 100644 index 0000000000..cebfda6ed5 --- /dev/null +++ b/plugins/discovery/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package discovery diff --git a/plugins/doc.go b/plugins/doc.go new file mode 100644 index 0000000000..77dade9b79 --- /dev/null +++ b/plugins/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package plugins diff --git a/plugins/logs/doc.go b/plugins/logs/doc.go new file mode 100644 index 0000000000..947df36013 --- /dev/null +++ b/plugins/logs/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package logs diff --git a/plugins/logs/plugin.go b/plugins/logs/plugin.go new file mode 100644 index 0000000000..206dc0c8cd --- /dev/null +++ b/plugins/logs/plugin.go @@ -0,0 +1,65 @@ +// 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 logs implements decision log buffering and uploading. +package logs + +import ( + "github.com/open-policy-agent/opa/plugins" + v1 "github.com/open-policy-agent/opa/v1/plugins/logs" +) + +// Logger defines the interface for decision logging plugins. +type Logger = v1.Logger + +// EventV1 represents a decision log event. +// WARNING: The AST() function for EventV1 must be kept in sync with +// the struct. Any changes here MUST be reflected in the AST() +// implementation below. +type EventV1 = v1.EventV1 + +// BundleInfoV1 describes a bundle associated with a decision log event. +type BundleInfoV1 = v1.BundleInfoV1 + +type RequestContext = v1.RequestContext + +type HTTPRequestContext = v1.HTTPRequestContext + +// ReportingConfig represents configuration for the plugin's reporting behaviour. +type ReportingConfig = v1.ReportingConfig + +type RequestContextConfig = v1.RequestContextConfig + +type HTTPRequestContextConfig = v1.HTTPRequestContextConfig + +// Config represents the plugin configuration. +type Config = v1.Config + +// Plugin implements decision log buffering and uploading. +type Plugin = v1.Plugin + +func ParseConfig(config []byte, services []string, pluginList []string) (*Config, error) { + return v1.ParseConfig(config, services, pluginList) +} + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the plugin config. +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} + +// New returns a new Plugin with the given config. +func New(parsedConfig *Config, manager *plugins.Manager) *Plugin { + return v1.New(parsedConfig, manager) +} + +// Name identifies the plugin on manager. +const Name = v1.Name + +// Lookup returns the decision logs plugin registered with the manager. +func Lookup(manager *plugins.Manager) *Plugin { + return v1.Lookup(manager) +} diff --git a/plugins/logs/status/doc.go b/plugins/logs/status/doc.go new file mode 100644 index 0000000000..083f744abc --- /dev/null +++ b/plugins/logs/status/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package status diff --git a/plugins/logs/status/status.go b/plugins/logs/status/status.go new file mode 100644 index 0000000000..b23579c780 --- /dev/null +++ b/plugins/logs/status/status.go @@ -0,0 +1,14 @@ +// 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 status + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/logs/status" +) + +// Status represents the status of processing a decision log. +type Status = v1.Status + +type HTTPError = v1.HTTPError diff --git a/plugins/plugins.go b/plugins/plugins.go new file mode 100644 index 0000000000..b050979e34 --- /dev/null +++ b/plugins/plugins.go @@ -0,0 +1,268 @@ +// 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 ( + "github.com/open-policy-agent/opa/internal/report" + "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/hooks" + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/logging" + "github.com/open-policy-agent/opa/resolver/wasm" + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/topdown/print" + "github.com/open-policy-agent/opa/tracing" + v1 "github.com/open-policy-agent/opa/v1/plugins" +) + +// 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 = v1.Factory + +// 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 = v1.Plugin + +// Triggerable defines the interface plugins use for manual plugin triggers. +type Triggerable = v1.Triggerable + +// State defines the state that a Plugin instance is currently +// in with pre-defined states. +type State = v1.State + +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 = v1.StateNotReady + + // StateOK signifies that the Plugin is operating normally. + StateOK = v1.StateOK + + // StateErr indicates that the Plugin is in an error state and should not + // be considered as functional. + StateErr = v1.StateErr + + // 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 = v1.StateWarn +) + +// TriggerMode defines the trigger mode utilized by a Plugin for bundle download, +// log upload etc. +type TriggerMode = v1.TriggerMode + +const ( + // TriggerPeriodic represents periodic polling mechanism + TriggerPeriodic = v1.TriggerPeriodic + + // TriggerManual represents manual triggering mechanism + TriggerManual = v1.TriggerManual + + // DefaultTriggerMode represents default trigger mechanism + DefaultTriggerMode = v1.DefaultTriggerMode +) + +// Status has a Plugin's current status plus an optional Message. +type Status = v1.Status + +// StatusListener defines a handler to register for status updates. +type StatusListener v1.StatusListener + +// Manager implements lifecycle management of plugins and gives plugins access +// to engine-wide components like storage. +type Manager = v1.Manager + +// 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) { + v1.SetCompilerOnContext(context, compiler) +} + +// GetCompilerOnContext gets the compiler cached on the storage context. +func GetCompilerOnContext(context *storage.Context) *ast.Compiler { + return v1.GetCompilerOnContext(context) +} + +// 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) { + v1.SetWasmResolversOnContext(context, rs) +} + +// ValidateAndInjectDefaultsForTriggerMode validates the trigger mode and injects default values +func ValidateAndInjectDefaultsForTriggerMode(a, b *TriggerMode) (*TriggerMode, error) { + return v1.ValidateAndInjectDefaultsForTriggerMode(a, b) +} + +// 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 v1.Info(term) +} + +// InitBundles provides the initial set of bundles to load. +func InitBundles(b map[string]*bundle.Bundle) func(*Manager) { + return v1.InitBundles(b) +} + +// InitFiles provides the initial set of other data/policy files to load. +func InitFiles(f loader.Result) func(*Manager) { + return v1.InitFiles(f) +} + +// MaxErrors sets the error limit for the manager's shared compiler. +func MaxErrors(n int) func(*Manager) { + return v1.MaxErrors(n) +} + +// GracefulShutdownPeriod passes the configured graceful shutdown period to plugins +func GracefulShutdownPeriod(gracefulShutdownPeriod int) func(*Manager) { + return v1.GracefulShutdownPeriod(gracefulShutdownPeriod) +} + +// Logger configures the passed logger on the plugin manager (useful to +// configure default fields) +func Logger(logger logging.Logger) func(*Manager) { + return v1.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 v1.ConsoleLogger(logger) +} + +func EnablePrintStatements(yes bool) func(*Manager) { + return v1.EnablePrintStatements(yes) +} + +func PrintHook(h print.Hook) func(*Manager) { + return v1.PrintHook(h) +} + +func WithRouter(r *mux.Router) func(*Manager) { + return v1.WithRouter(r) +} + +// WithPrometheusRegister sets the passed prometheus.Registerer to be used by plugins +func WithPrometheusRegister(prometheusRegister prometheus.Registerer) func(*Manager) { + return v1.WithPrometheusRegister(prometheusRegister) +} + +// WithTracerProvider sets the passed *trace.TracerProvider to be used by plugins +func WithTracerProvider(tracerProvider *trace.TracerProvider) func(*Manager) { + return v1.WithTracerProvider(tracerProvider) +} + +// WithDistributedTracingOpts sets the options to be used by distributed tracing. +func WithDistributedTracingOpts(tr tracing.Options) func(*Manager) { + return v1.WithDistributedTracingOpts(tr) +} + +// WithHooks allows passing hooks to the plugin manager. +func WithHooks(hs hooks.Hooks) func(*Manager) { + return v1.WithHooks(hs) +} + +// WithParserOptions sets the parser options to be used by the plugin manager. +func WithParserOptions(opts ast.ParserOptions) func(*Manager) { + return v1.WithParserOptions(opts) +} + +// WithEnableTelemetry controls whether OPA will send telemetry reports to an external service. +func WithEnableTelemetry(enableTelemetry bool) func(*Manager) { + return v1.WithEnableTelemetry(enableTelemetry) +} + +// WithTelemetryGatherers allows registration of telemetry gatherers which enable injection of additional data in the +// telemetry report +func WithTelemetryGatherers(gs map[string]report.Gatherer) func(*Manager) { + return v1.WithTelemetryGatherers(gs) +} + +// New creates a new Manager using config. +func New(raw []byte, id string, store storage.Store, opts ...func(*Manager)) (*Manager, error) { + options := make([]func(*Manager), 0, len(opts)+1) + options = append(options, opts...) + + // Add option to apply default Rego version if not set. Must be last in list of options. + options = append(options, func(m *Manager) { + if m.ParserOptions().RegoVersion == ast.RegoUndefined { + cpy := m.ParserOptions() + cpy.RegoVersion = ast.DefaultRegoVersion + WithParserOptions(cpy)(m) + } + }) + + return v1.New(raw, id, store, options...) +} diff --git a/plugins/plugins_test.go b/plugins/plugins_test.go new file mode 100644 index 0000000000..c453549de5 --- /dev/null +++ b/plugins/plugins_test.go @@ -0,0 +1,47 @@ +// Copyright 2024 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 + +import ( + "testing" + + "github.com/open-policy-agent/opa/storage/inmem" + "github.com/open-policy-agent/opa/v1/ast" +) + +func TestNew_DefaultRegoVersion(t *testing.T) { + popts := ast.ParserOptions{ + Capabilities: &ast.Capabilities{ + Features: []string{ + "my_custom_feature", + }, + }, + ProcessAnnotation: true, + AllFutureKeywords: true, + FutureKeywords: []string{"foo", "bar"}, + } + m, err := New([]byte(`{"plugins": {"someplugin": {}}}`), "test", inmem.New(), + WithParserOptions(popts)) + if err != nil { + t.Fatal(err) + } + + if exp, act := ast.RegoV0, m.ParserOptions().RegoVersion; exp != act { + t.Fatalf("Expected default Rego version to be %v but got %v", exp, act) + } + + // Check a couple of other options to make sure they haven't changed + if exp, act := popts.ProcessAnnotation, m.ParserOptions().ProcessAnnotation; exp != act { + t.Fatalf("Expected ProcessAnnotation to be %v but got %v", exp, act) + } + + if exp, act := popts.AllFutureKeywords, m.ParserOptions().AllFutureKeywords; exp != act { + t.Fatalf("Expected AllFutureKeywords to be %v but got %v", exp, act) + } + + if exp, act := popts.Capabilities, m.ParserOptions().Capabilities; exp != act { + t.Fatalf("Expected Capabilities to be %v but got %v", exp, act) + } +} diff --git a/plugins/rest/auth.go b/plugins/rest/auth.go new file mode 100644 index 0000000000..adf467bf5f --- /dev/null +++ b/plugins/rest/auth.go @@ -0,0 +1,22 @@ +// 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 ( + "crypto/tls" + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/plugins/rest" +) + +// DefaultTLSConfig defines standard TLS configurations based on the Config +func DefaultTLSConfig(c Config) (*tls.Config, error) { + return v1.DefaultTLSConfig(c) +} + +// DefaultRoundTripperClient is a reasonable set of defaults for HTTP auth plugins +func DefaultRoundTripperClient(t *tls.Config, timeout int64) *http.Client { + return v1.DefaultRoundTripperClient(t, timeout) +} diff --git a/plugins/rest/doc.go b/plugins/rest/doc.go new file mode 100644 index 0000000000..7f953965d9 --- /dev/null +++ b/plugins/rest/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package rest diff --git a/plugins/rest/gcp.go b/plugins/rest/gcp.go new file mode 100644 index 0000000000..d728eda76e --- /dev/null +++ b/plugins/rest/gcp.go @@ -0,0 +1,12 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/plugins/rest" +) + +// AccessToken holds a GCP access token. +type AccessToken = v1.AccessToken diff --git a/plugins/rest/rest.go b/plugins/rest/rest.go new file mode 100644 index 0000000000..b9ca3ae5cd --- /dev/null +++ b/plugins/rest/rest.go @@ -0,0 +1,54 @@ +// 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 ( + "github.com/open-policy-agent/opa/logging" + "github.com/open-policy-agent/opa/v1/keys" + v1 "github.com/open-policy-agent/opa/v1/plugins/rest" + "github.com/open-policy-agent/opa/v1/tracing" +) + +// An HTTPAuthPlugin represents a mechanism to construct and configure HTTP authentication for a REST service +type HTTPAuthPlugin = v1.HTTPAuthPlugin + +// Config represents configuration for a REST client. +type Config = v1.Config + +// An AuthPluginLookupFunc can lookup auth plugins by their name. +type AuthPluginLookupFunc = v1.AuthPluginLookupFunc + +// Client implements an HTTP/REST client for communicating with remote +// services. +type Client = v1.Client + +// Name returns an option that overrides the service name on the client. +func Name(s string) func(*Client) { + return v1.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 v1.AuthPluginLookup(l) +} + +// Logger assigns a logger to the client +func Logger(l logging.Logger) func(*Client) { + return v1.Logger(l) +} + +// DistributedTracingOpts sets the options to be used by distributed tracing. +func DistributedTracingOpts(tr tracing.Options) func(*Client) { + return v1.DistributedTracingOpts(tr) +} + +// New returns a new Client for config. +func New(config []byte, keys map[string]*keys.Config, opts ...func(*Client)) (Client, error) { + return v1.New(config, keys, opts...) +} diff --git a/plugins/server/decoding/config.go b/plugins/server/decoding/config.go new file mode 100644 index 0000000000..3a28a49f58 --- /dev/null +++ b/plugins/server/decoding/config.go @@ -0,0 +1,35 @@ +// Package decoding implements the configuration side of the upgraded gzip +// decompression framework. The original work only enabled gzip decoding for +// a few endpoints-- here we enable if for all of OPA. Additionally, we provide +// some new defensive configuration options: max_length, and gzip.max_length. +// These allow rejecting requests that indicate their contents are larger than +// the size limits. +// +// The request handling pipeline now looks roughly like this: +// +// Request -> MaxBytesReader(Config.MaxLength) -> ir.CopyN(dest, req, Gzip.MaxLength) +// +// The intent behind this design is to improve how OPA handles large and/or +// malicious requests, compressed or otherwise. The benefit of being a little +// more strict in what we allow is that we can now use "riskier", but +// dramatically more performant techniques, like preallocating content buffers +// for gzipped data. This also should help OPAs in limited memory situations. +package decoding + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/server/decoding" +) + +// Config represents the configuration for the Server.Decoding settings +type Config = v1.Config + +// Gzip represents the configuration for the Server.Decoding.Gzip settings +type Gzip = v1.Gzip + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the server config +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} diff --git a/plugins/server/decoding/doc.go b/plugins/server/decoding/doc.go new file mode 100644 index 0000000000..cbba4144e5 --- /dev/null +++ b/plugins/server/decoding/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package decoding diff --git a/plugins/server/doc.go b/plugins/server/doc.go new file mode 100644 index 0000000000..93588bae71 --- /dev/null +++ b/plugins/server/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package server diff --git a/plugins/server/encoding/config.go b/plugins/server/encoding/config.go new file mode 100644 index 0000000000..68faf0aac9 --- /dev/null +++ b/plugins/server/encoding/config.go @@ -0,0 +1,19 @@ +package encoding + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/server/encoding" +) + +// Config represents the configuration for the Server.Encoding settings +type Config = v1.Config + +// Gzip represents the configuration for the Server.Encoding.Gzip settings +type Gzip = v1.Gzip + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the server config +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} diff --git a/plugins/server/encoding/doc.go b/plugins/server/encoding/doc.go new file mode 100644 index 0000000000..bf2818e524 --- /dev/null +++ b/plugins/server/encoding/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package encoding diff --git a/plugins/server/metrics/config.go b/plugins/server/metrics/config.go new file mode 100644 index 0000000000..f77ac7f348 --- /dev/null +++ b/plugins/server/metrics/config.go @@ -0,0 +1,22 @@ +package metrics + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/server/metrics" +) + +// Config represents the configuration for the Server.Metrics settings +type Config = v1.Config + +// Prom represents the configuration for the Server.Metrics.Prom settings +type Prom = v1.Prom + +// HTTPRequestDurationSeconds represents the configuration for the Server.Metrics.Prom.HTTPRequestDurationSeconds settings +type HTTPRequestDurationSeconds = v1.HTTPRequestDurationSeconds + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the server config +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} diff --git a/plugins/server/metrics/doc.go b/plugins/server/metrics/doc.go new file mode 100644 index 0000000000..6a306991e7 --- /dev/null +++ b/plugins/server/metrics/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package metrics diff --git a/plugins/status/doc.go b/plugins/status/doc.go new file mode 100644 index 0000000000..083f744abc --- /dev/null +++ b/plugins/status/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package status diff --git a/plugins/status/metrics.go b/plugins/status/metrics.go new file mode 100644 index 0000000000..1b8763ccdf --- /dev/null +++ b/plugins/status/metrics.go @@ -0,0 +1,9 @@ +package status + +import ( + v1 "github.com/open-policy-agent/opa/v1/plugins/status" +) + +type PrometheusConfig = v1.PrometheusConfig + +type Collectors = v1.Collectors diff --git a/plugins/status/plugin.go b/plugins/status/plugin.go new file mode 100644 index 0000000000..54d9927cc7 --- /dev/null +++ b/plugins/status/plugin.go @@ -0,0 +1,53 @@ +// 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 status implements status reporting. +package status + +import ( + "github.com/open-policy-agent/opa/plugins" + v1 "github.com/open-policy-agent/opa/v1/plugins/status" +) + +// Logger defines the interface for status plugins. +type Logger = v1.Logger + +// UpdateRequestV1 represents the status update message that OPA sends to +// remote HTTP endpoints. +type UpdateRequestV1 = v1.UpdateRequestV1 + +// Plugin implements status reporting. Updates can be triggered by the caller. +type Plugin = v1.Plugin + +// Config contains configuration for the plugin. +type Config = v1.Config + +// BundleLoadDurationNanoseconds represents the configuration for the status.prometheus_config.bundle_loading_duration_ns settings +type BundleLoadDurationNanoseconds = v1.BundleLoadDurationNanoseconds + +// ParseConfig validates the config and injects default values. +func ParseConfig(config []byte, services []string, pluginsList []string) (*Config, error) { + return v1.ParseConfig(config, services, pluginsList) +} + +// ConfigBuilder assists in the construction of the plugin configuration. +type ConfigBuilder = v1.ConfigBuilder + +// NewConfigBuilder returns a new ConfigBuilder to build and parse the plugin config. +func NewConfigBuilder() *ConfigBuilder { + return v1.NewConfigBuilder() +} + +// New returns a new Plugin with the given config. +func New(parsedConfig *Config, manager *plugins.Manager) *Plugin { + return v1.New(parsedConfig, manager) +} + +// Name identifies the plugin on manager. +const Name = v1.Name + +// Lookup returns the status plugin registered with the manager. +func Lookup(manager *plugins.Manager) *Plugin { + return v1.Lookup(manager) +} diff --git a/profiler/doc.go b/profiler/doc.go new file mode 100644 index 0000000000..c04a317028 --- /dev/null +++ b/profiler/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package profiler diff --git a/profiler/profiler.go b/profiler/profiler.go new file mode 100644 index 0000000000..65085cd9b6 --- /dev/null +++ b/profiler/profiler.go @@ -0,0 +1,35 @@ +// 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 profiler computes and reports on the time spent on expressions. +package profiler + +import ( + v1 "github.com/open-policy-agent/opa/v1/profiler" +) + +// Profiler computes and reports on the time spent on expressions. +type Profiler = v1.Profiler + +// New returns a new Profiler object. +func New() *Profiler { + return v1.New() +} + +// ExprStats represents the result of profiling an expression. +type ExprStats = v1.ExprStats + +// ExprStatsAggregated represents the result of profiling an expression +// by aggregating `n` profiles. +type ExprStatsAggregated = v1.ExprStatsAggregated + +func AggregateProfiles(profiles ...[]ExprStats) []ExprStatsAggregated { + return v1.AggregateProfiles(profiles...) +} + +// Report represents the profiler report for a set of files. +type Report = v1.Report + +// FileReport represents a profiler report for a single file. +type FileReport = v1.FileReport diff --git a/refactor/doc.go b/refactor/doc.go new file mode 100644 index 0000000000..3746120621 --- /dev/null +++ b/refactor/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package refactor diff --git a/refactor/refactor.go b/refactor/refactor.go new file mode 100644 index 0000000000..e110ec101f --- /dev/null +++ b/refactor/refactor.go @@ -0,0 +1,30 @@ +// Copyright 2021 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 refactor implements different refactoring operations over Rego modules. +package refactor + +import ( + v1 "github.com/open-policy-agent/opa/v1/refactor" +) + +// Error defines the structure of errors returned by refactor. +type Error = v1.Error + +// Refactor implements different refactoring operations over Rego modules eg. renaming packages. +type Refactor = v1.Refactor + +// New returns a new Refactor object. +func New() *Refactor { + return v1.New() +} + +// MoveQuery holds the set of Rego modules whose package paths and other references are to be rewritten +// as per the mapping defined in SrcDstMapping. +// If validate is true, the moved modules will be compiled to ensure they are valid. +type MoveQuery = v1.MoveQuery + +// MoveQueryResult defines the output of a move query and holds the rewritten modules with updated packages paths +// and references. +type MoveQueryResult = v1.MoveQueryResult diff --git a/rego/doc.go b/rego/doc.go new file mode 100644 index 0000000000..febe75696c --- /dev/null +++ b/rego/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package rego diff --git a/rego/errors.go b/rego/errors.go new file mode 100644 index 0000000000..bcbd2efedd --- /dev/null +++ b/rego/errors.go @@ -0,0 +1,17 @@ +package rego + +import v1 "github.com/open-policy-agent/opa/v1/rego" + +// HaltError is an error type to return from a custom function implementation +// that will abort the evaluation process (analogous to topdown.Halt). +type HaltError = v1.HaltError + +// NewHaltError wraps an error such that the evaluation process will stop +// when it occurs. +func NewHaltError(err error) error { + return v1.NewHaltError(err) +} + +// ErrorDetails interface is satisfied by an error that provides further +// details. +type ErrorDetails = v1.ErrorDetails diff --git a/rego/plugins.go b/rego/plugins.go new file mode 100644 index 0000000000..38ef84416f --- /dev/null +++ b/rego/plugins.go @@ -0,0 +1,17 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/rego" +) + +type TargetPlugin = v1.TargetPlugin + +type TargetPluginEval = v1.TargetPluginEval + +func RegisterPlugin(name string, p TargetPlugin) { + v1.RegisterPlugin(name, p) +} diff --git a/rego/rego.go b/rego/rego.go new file mode 100644 index 0000000000..e6af30c39c --- /dev/null +++ b/rego/rego.go @@ -0,0 +1,628 @@ +// Copyright 2017 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 exposes high level APIs for evaluating Rego policies. +package rego + +import ( + "io" + "time" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/bundle" + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/v1/metrics" + v1 "github.com/open-policy-agent/opa/v1/rego" + "github.com/open-policy-agent/opa/v1/resolver" + "github.com/open-policy-agent/opa/v1/topdown" + "github.com/open-policy-agent/opa/v1/topdown/builtins" + "github.com/open-policy-agent/opa/v1/topdown/cache" + "github.com/open-policy-agent/opa/v1/topdown/print" + "github.com/open-policy-agent/opa/v1/tracing" +) + +// CompileResult represents the result of compiling a Rego query, zero or more +// Rego modules, and arbitrary contextual data into an executable. +type CompileResult = v1.CompileResult + +// PartialQueries contains the queries and support modules produced by partial +// evaluation. +type PartialQueries = v1.PartialQueries + +// PartialResult represents the result of partial evaluation. The result can be +// used to generate a new query that can be run when inputs are known. +type PartialResult = v1.PartialResult + +// EvalContext defines the set of options allowed to be set at evaluation +// time. Any other options will need to be set on a new Rego object. +type EvalContext = v1.EvalContext + +// EvalOption defines a function to set an option on an EvalConfig +type EvalOption = v1.EvalOption + +// EvalInput configures the input for a Prepared Query's evaluation +func EvalInput(input interface{}) EvalOption { + return v1.EvalInput(input) +} + +// EvalParsedInput configures the input for a Prepared Query's evaluation +func EvalParsedInput(input ast.Value) EvalOption { + return v1.EvalParsedInput(input) +} + +// EvalMetrics configures the metrics for a Prepared Query's evaluation +func EvalMetrics(metric metrics.Metrics) EvalOption { + return v1.EvalMetrics(metric) +} + +// EvalTransaction configures the Transaction for a Prepared Query's evaluation +func EvalTransaction(txn storage.Transaction) EvalOption { + return v1.EvalTransaction(txn) +} + +// EvalInstrument enables or disables instrumenting for a Prepared Query's evaluation +func EvalInstrument(instrument bool) EvalOption { + return v1.EvalInstrument(instrument) +} + +// EvalTracer configures a tracer for a Prepared Query's evaluation +// Deprecated: Use EvalQueryTracer instead. +func EvalTracer(tracer topdown.Tracer) EvalOption { + return v1.EvalTracer(tracer) +} + +// EvalQueryTracer configures a tracer for a Prepared Query's evaluation +func EvalQueryTracer(tracer topdown.QueryTracer) EvalOption { + return v1.EvalQueryTracer(tracer) +} + +// EvalPartialNamespace returns an argument that sets the namespace to use for +// partial evaluation results. The namespace must be a valid package path +// component. +func EvalPartialNamespace(ns string) EvalOption { + return v1.EvalPartialNamespace(ns) +} + +// EvalUnknowns returns an argument that sets the values to treat as +// unknown during partial evaluation. +func EvalUnknowns(unknowns []string) EvalOption { + return v1.EvalUnknowns(unknowns) +} + +// EvalDisableInlining returns an argument that adds a set of paths to exclude from +// partial evaluation inlining. +func EvalDisableInlining(paths []ast.Ref) EvalOption { + return v1.EvalDisableInlining(paths) +} + +// EvalParsedUnknowns returns an argument that sets the values to treat +// as unknown during partial evaluation. +func EvalParsedUnknowns(unknowns []*ast.Term) EvalOption { + return v1.EvalParsedUnknowns(unknowns) +} + +// EvalRuleIndexing will disable indexing optimizations for the +// evaluation. This should only be used when tracing in debug mode. +func EvalRuleIndexing(enabled bool) EvalOption { + return v1.EvalRuleIndexing(enabled) +} + +// EvalEarlyExit will disable 'early exit' optimizations for the +// evaluation. This should only be used when tracing in debug mode. +func EvalEarlyExit(enabled bool) EvalOption { + return v1.EvalEarlyExit(enabled) +} + +// EvalTime sets the wall clock time to use during policy evaluation. +// time.now_ns() calls will return this value. +func EvalTime(x time.Time) EvalOption { + return v1.EvalTime(x) +} + +// EvalSeed sets a reader that will seed randomization required by built-in functions. +// If a seed is not provided crypto/rand.Reader is used. +func EvalSeed(r io.Reader) EvalOption { + return v1.EvalSeed(r) +} + +// EvalInterQueryBuiltinCache sets the inter-query cache that built-in functions can utilize +// during evaluation. +func EvalInterQueryBuiltinCache(c cache.InterQueryCache) EvalOption { + return v1.EvalInterQueryBuiltinCache(c) +} + +// EvalInterQueryBuiltinValueCache sets the inter-query value cache that built-in functions can utilize +// during evaluation. +func EvalInterQueryBuiltinValueCache(c cache.InterQueryValueCache) EvalOption { + return v1.EvalInterQueryBuiltinValueCache(c) +} + +// EvalNDBuiltinCache sets the non-deterministic builtin cache that built-in functions can +// use during evaluation. +func EvalNDBuiltinCache(c builtins.NDBCache) EvalOption { + return v1.EvalNDBuiltinCache(c) +} + +// EvalResolver sets a Resolver for a specified ref path for this evaluation. +func EvalResolver(ref ast.Ref, r resolver.Resolver) EvalOption { + return v1.EvalResolver(ref, r) +} + +// EvalSortSets causes the evaluator to sort sets before returning them as JSON arrays. +func EvalSortSets(yes bool) EvalOption { + return v1.EvalSortSets(yes) +} + +// EvalCopyMaps causes the evaluator to copy `map[string]interface{}`s before returning them. +func EvalCopyMaps(yes bool) EvalOption { + return v1.EvalCopyMaps(yes) +} + +// EvalPrintHook sets the object to use for handling print statement outputs. +func EvalPrintHook(ph print.Hook) EvalOption { + return v1.EvalPrintHook(ph) +} + +// EvalVirtualCache sets the topdown.VirtualCache to use for evaluation. This is +// optional, and if not set, the default cache is used. +func EvalVirtualCache(vc topdown.VirtualCache) EvalOption { + return v1.EvalVirtualCache(vc) +} + +// PreparedEvalQuery holds the prepared Rego state that has been pre-processed +// for subsequent evaluations. +type PreparedEvalQuery = v1.PreparedEvalQuery + +// PreparedPartialQuery holds the prepared Rego state that has been pre-processed +// for partial evaluations. +type PreparedPartialQuery = v1.PreparedPartialQuery + +// Errors represents a collection of errors returned when evaluating Rego. +type Errors = v1.Errors + +// IsPartialEvaluationNotEffectiveErr returns true if err is an error returned by +// this package to indicate that partial evaluation was ineffective. +func IsPartialEvaluationNotEffectiveErr(err error) bool { + return v1.IsPartialEvaluationNotEffectiveErr(err) +} + +// Rego constructs a query and can be evaluated to obtain results. +type Rego = v1.Rego + +// Function represents a built-in function that is callable in Rego. +type Function = v1.Function + +// BuiltinContext contains additional attributes from the evaluator that +// built-in functions can use, e.g., the request context.Context, caches, etc. +type BuiltinContext = v1.BuiltinContext + +type ( + // Builtin1 defines a built-in function that accepts 1 argument. + Builtin1 = v1.Builtin1 + + // Builtin2 defines a built-in function that accepts 2 arguments. + Builtin2 = v1.Builtin2 + + // Builtin3 defines a built-in function that accepts 3 argument. + Builtin3 = v1.Builtin3 + + // Builtin4 defines a built-in function that accepts 4 argument. + Builtin4 = v1.Builtin4 + + // BuiltinDyn defines a built-in function that accepts a list of arguments. + BuiltinDyn = v1.BuiltinDyn +) + +// RegisterBuiltin1 adds a built-in function globally inside the OPA runtime. +func RegisterBuiltin1(decl *Function, impl Builtin1) { + v1.RegisterBuiltin1(decl, impl) +} + +// RegisterBuiltin2 adds a built-in function globally inside the OPA runtime. +func RegisterBuiltin2(decl *Function, impl Builtin2) { + v1.RegisterBuiltin2(decl, impl) +} + +// RegisterBuiltin3 adds a built-in function globally inside the OPA runtime. +func RegisterBuiltin3(decl *Function, impl Builtin3) { + v1.RegisterBuiltin3(decl, impl) +} + +// RegisterBuiltin4 adds a built-in function globally inside the OPA runtime. +func RegisterBuiltin4(decl *Function, impl Builtin4) { + v1.RegisterBuiltin4(decl, impl) +} + +// RegisterBuiltinDyn adds a built-in function globally inside the OPA runtime. +func RegisterBuiltinDyn(decl *Function, impl BuiltinDyn) { + v1.RegisterBuiltinDyn(decl, impl) +} + +// Function1 returns an option that adds a built-in function to the Rego object. +func Function1(decl *Function, f Builtin1) func(*Rego) { + return v1.Function1(decl, f) +} + +// Function2 returns an option that adds a built-in function to the Rego object. +func Function2(decl *Function, f Builtin2) func(*Rego) { + return v1.Function2(decl, f) +} + +// Function3 returns an option that adds a built-in function to the Rego object. +func Function3(decl *Function, f Builtin3) func(*Rego) { + return v1.Function3(decl, f) +} + +// Function4 returns an option that adds a built-in function to the Rego object. +func Function4(decl *Function, f Builtin4) func(*Rego) { + return v1.Function4(decl, f) +} + +// FunctionDyn returns an option that adds a built-in function to the Rego object. +func FunctionDyn(decl *Function, f BuiltinDyn) func(*Rego) { + return v1.FunctionDyn(decl, f) +} + +// FunctionDecl returns an option that adds a custom-built-in function +// __declaration__. NO implementation is provided. This is used for +// non-interpreter execution envs (e.g., Wasm). +func FunctionDecl(decl *Function) func(*Rego) { + return v1.FunctionDecl(decl) +} + +// Dump returns an argument that sets the writer to dump debugging information to. +func Dump(w io.Writer) func(r *Rego) { + return v1.Dump(w) +} + +// Query returns an argument that sets the Rego query. +func Query(q string) func(r *Rego) { + return v1.Query(q) +} + +// ParsedQuery returns an argument that sets the Rego query. +func ParsedQuery(q ast.Body) func(r *Rego) { + return v1.ParsedQuery(q) +} + +// Package returns an argument that sets the Rego package on the query's +// context. +func Package(p string) func(r *Rego) { + return v1.Package(p) +} + +// ParsedPackage returns an argument that sets the Rego package on the query's +// context. +func ParsedPackage(pkg *ast.Package) func(r *Rego) { + return v1.ParsedPackage(pkg) +} + +// Imports returns an argument that adds a Rego import to the query's context. +func Imports(p []string) func(r *Rego) { + return v1.Imports(p) +} + +// ParsedImports returns an argument that adds Rego imports to the query's +// context. +func ParsedImports(imp []*ast.Import) func(r *Rego) { + return v1.ParsedImports(imp) +} + +// Input returns an argument that sets the Rego input document. Input should be +// a native Go value representing the input document. +func Input(x interface{}) func(r *Rego) { + return v1.Input(x) +} + +// ParsedInput returns an argument that sets the Rego input document. +func ParsedInput(x ast.Value) func(r *Rego) { + return v1.ParsedInput(x) +} + +// Unknowns returns an argument that sets the values to treat as unknown during +// partial evaluation. +func Unknowns(unknowns []string) func(r *Rego) { + return v1.Unknowns(unknowns) +} + +// ParsedUnknowns returns an argument that sets the values to treat as unknown +// during partial evaluation. +func ParsedUnknowns(unknowns []*ast.Term) func(r *Rego) { + return v1.ParsedUnknowns(unknowns) +} + +// DisableInlining adds a set of paths to exclude from partial evaluation inlining. +func DisableInlining(paths []string) func(r *Rego) { + return v1.DisableInlining(paths) +} + +// ShallowInlining prevents rules that depend on unknown values from being inlined. +// Rules that only depend on known values are inlined. +func ShallowInlining(yes bool) func(r *Rego) { + return v1.ShallowInlining(yes) +} + +// SkipPartialNamespace disables namespacing of partial evalution results for support +// rules generated from policy. Synthetic support rules are still namespaced. +func SkipPartialNamespace(yes bool) func(r *Rego) { + return v1.SkipPartialNamespace(yes) +} + +// PartialNamespace returns an argument that sets the namespace to use for +// partial evaluation results. The namespace must be a valid package path +// component. +func PartialNamespace(ns string) func(r *Rego) { + return v1.PartialNamespace(ns) +} + +// Module returns an argument that adds a Rego module. +func Module(filename, input string) func(r *Rego) { + return v1.Module(filename, input) +} + +// ParsedModule returns an argument that adds a parsed Rego module. If a string +// module with the same filename name is added, it will override the parsed +// module. +func ParsedModule(module *ast.Module) func(*Rego) { + return v1.ParsedModule(module) +} + +// Load returns an argument that adds a filesystem path to load data +// and Rego modules from. Any file with a *.rego, *.yaml, or *.json +// extension will be loaded. The path can be either a directory or file, +// directories are loaded recursively. The optional ignore string patterns +// can be used to filter which files are used. +// The Load option can only be used once. +// Note: Loading files will require a write transaction on the store. +func Load(paths []string, filter loader.Filter) func(r *Rego) { + return v1.Load(paths, filter) +} + +// LoadBundle returns an argument that adds a filesystem path to load +// a bundle from. The path can be a compressed bundle file or a directory +// to be loaded as a bundle. +// Note: Loading bundles will require a write transaction on the store. +func LoadBundle(path string) func(r *Rego) { + return v1.LoadBundle(path) +} + +// ParsedBundle returns an argument that adds a bundle to be loaded. +func ParsedBundle(name string, b *bundle.Bundle) func(r *Rego) { + return v1.ParsedBundle(name, b) +} + +// Compiler returns an argument that sets the Rego compiler. +func Compiler(c *ast.Compiler) func(r *Rego) { + return v1.Compiler(c) +} + +// Store returns an argument that sets the policy engine's data storage layer. +// +// If using the Load, LoadBundle, or ParsedBundle options then a transaction +// must also be provided via the Transaction() option. After loading files +// or bundles the transaction should be aborted or committed. +func Store(s storage.Store) func(r *Rego) { + return v1.Store(s) +} + +// StoreReadAST returns an argument that sets whether the store should eagerly convert data to AST values. +// +// Only applicable when no store has been set on the Rego object through the Store option. +func StoreReadAST(enabled bool) func(r *Rego) { + return v1.StoreReadAST(enabled) +} + +// Transaction returns an argument that sets the transaction to use for storage +// layer operations. +// +// Requires the store associated with the transaction to be provided via the +// Store() option. If using Load(), LoadBundle(), or ParsedBundle() options +// the transaction will likely require write params. +func Transaction(txn storage.Transaction) func(r *Rego) { + return v1.Transaction(txn) +} + +// Metrics returns an argument that sets the metrics collection. +func Metrics(m metrics.Metrics) func(r *Rego) { + return v1.Metrics(m) +} + +// Instrument returns an argument that enables instrumentation for diagnosing +// performance issues. +func Instrument(yes bool) func(r *Rego) { + return v1.Instrument(yes) +} + +// Trace returns an argument that enables tracing on r. +func Trace(yes bool) func(r *Rego) { + return v1.Trace(yes) +} + +// Tracer returns an argument that adds a query tracer to r. +// Deprecated: Use QueryTracer instead. +func Tracer(t topdown.Tracer) func(r *Rego) { + return v1.Tracer(t) +} + +// QueryTracer returns an argument that adds a query tracer to r. +func QueryTracer(t topdown.QueryTracer) func(r *Rego) { + return v1.QueryTracer(t) +} + +// Runtime returns an argument that sets the runtime data to provide to the +// evaluation engine. +func Runtime(term *ast.Term) func(r *Rego) { + return v1.Runtime(term) +} + +// Time sets the wall clock time to use during policy evaluation. Prepared queries +// do not inherit this parameter. Use EvalTime to set the wall clock time when +// executing a prepared query. +func Time(x time.Time) func(r *Rego) { + return v1.Time(x) +} + +// Seed sets a reader that will seed randomization required by built-in functions. +// If a seed is not provided crypto/rand.Reader is used. +func Seed(r io.Reader) func(*Rego) { + return v1.Seed(r) +} + +// PrintTrace is a helper function to write a human-readable version of the +// trace to the writer w. +func PrintTrace(w io.Writer, r *Rego) { + v1.PrintTrace(w, r) +} + +// PrintTraceWithLocation is a helper function to write a human-readable version of the +// trace to the writer w. +func PrintTraceWithLocation(w io.Writer, r *Rego) { + v1.PrintTraceWithLocation(w, r) +} + +// UnsafeBuiltins sets the built-in functions to treat as unsafe and not allow. +// This option is ignored for module compilation if the caller supplies the +// compiler. This option is always honored for query compilation. Provide an +// empty (non-nil) map to disable checks on queries. +func UnsafeBuiltins(unsafeBuiltins map[string]struct{}) func(r *Rego) { + return v1.UnsafeBuiltins(unsafeBuiltins) +} + +// SkipBundleVerification skips verification of a signed bundle. +func SkipBundleVerification(yes bool) func(r *Rego) { + return v1.SkipBundleVerification(yes) +} + +// InterQueryBuiltinCache sets the inter-query cache that built-in functions can utilize +// during evaluation. +func InterQueryBuiltinCache(c cache.InterQueryCache) func(r *Rego) { + return v1.InterQueryBuiltinCache(c) +} + +// InterQueryBuiltinValueCache sets the inter-query value cache that built-in functions can utilize +// during evaluation. +func InterQueryBuiltinValueCache(c cache.InterQueryValueCache) func(r *Rego) { + return v1.InterQueryBuiltinValueCache(c) +} + +// NDBuiltinCache sets the non-deterministic builtins cache. +func NDBuiltinCache(c builtins.NDBCache) func(r *Rego) { + return v1.NDBuiltinCache(c) +} + +// StrictBuiltinErrors tells the evaluator to treat all built-in function errors as fatal errors. +func StrictBuiltinErrors(yes bool) func(r *Rego) { + return v1.StrictBuiltinErrors(yes) +} + +// BuiltinErrorList supplies an error slice to store built-in function errors. +func BuiltinErrorList(list *[]topdown.Error) func(r *Rego) { + return v1.BuiltinErrorList(list) +} + +// Resolver sets a Resolver for a specified ref path. +func Resolver(ref ast.Ref, r resolver.Resolver) func(r *Rego) { + return v1.Resolver(ref, r) +} + +// Schemas sets the schemaSet +func Schemas(x *ast.SchemaSet) func(r *Rego) { + return v1.Schemas(x) +} + +// Capabilities configures the underlying compiler's capabilities. +// This option is ignored for module compilation if the caller supplies the +// compiler. +func Capabilities(c *ast.Capabilities) func(r *Rego) { + return v1.Capabilities(c) +} + +// Target sets the runtime to exercise. +func Target(t string) func(r *Rego) { + return v1.Target(t) +} + +// GenerateJSON sets the AST to JSON converter for the results. +func GenerateJSON(f func(*ast.Term, *EvalContext) (interface{}, error)) func(r *Rego) { + return v1.GenerateJSON(f) +} + +// PrintHook sets the object to use for handling print statement outputs. +func PrintHook(h print.Hook) func(r *Rego) { + return v1.PrintHook(h) +} + +// DistributedTracingOpts sets the options to be used by distributed tracing. +func DistributedTracingOpts(tr tracing.Options) func(r *Rego) { + return v1.DistributedTracingOpts(tr) +} + +// EnablePrintStatements enables print() calls. If this option is not provided, +// print() calls will be erased from the policy. This option only applies to +// queries and policies that passed as raw strings, i.e., this function will not +// have any affect if the caller supplies the ast.Compiler instance. +func EnablePrintStatements(yes bool) func(r *Rego) { + return v1.EnablePrintStatements(yes) +} + +// Strict enables or disables strict-mode in the compiler +func Strict(yes bool) func(r *Rego) { + return v1.Strict(yes) +} + +func SetRegoVersion(version ast.RegoVersion) func(r *Rego) { + return v1.SetRegoVersion(version) +} + +// New returns a new Rego object. +func New(options ...func(r *Rego)) *Rego { + opts := make([]func(r *Rego), 0, len(options)+1) + opts = append(opts, options...) + opts = append(opts, func(r *Rego) { + if r.RegoVersion() == ast.RegoUndefined { + SetRegoVersion(ast.DefaultRegoVersion)(r) + } + }) + + return v1.New(opts...) +} + +// CompileOption defines a function to set options on Compile calls. +type CompileOption = v1.CompileOption + +// CompileContext contains options for Compile calls. +type CompileContext = v1.CompileContext + +// CompilePartial defines an option to control whether partial evaluation is run +// before the query is planned and compiled. +func CompilePartial(yes bool) CompileOption { + return v1.CompilePartial(yes) +} + +// PrepareOption defines a function to set an option to control +// the behavior of the Prepare call. +type PrepareOption = v1.PrepareOption + +// PrepareConfig holds settings to control the behavior of the +// Prepare call. +type PrepareConfig = v1.PrepareConfig + +// WithPartialEval configures an option for PrepareForEval +// which will have it perform partial evaluation while preparing +// the query (similar to rego.Rego#PartialResult) +func WithPartialEval() PrepareOption { + return v1.WithPartialEval() +} + +// WithNoInline adds a set of paths to exclude from partial evaluation inlining. +func WithNoInline(paths []string) PrepareOption { + return v1.WithNoInline(paths) +} + +// 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 v1.WithBuiltinFuncs(bis) +} diff --git a/rego/rego_test.go b/rego/rego_test.go new file mode 100644 index 0000000000..7719edf008 --- /dev/null +++ b/rego/rego_test.go @@ -0,0 +1,126 @@ +// Copyright 2024 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" + "reflect" + "strings" + "testing" + + "github.com/open-policy-agent/opa/v1/util/test" +) + +func TestRegoEval_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expResult interface{} + expErrs []string + }{ + { + note: "v0", // v0 is the default version + module: `package test + +p[x] { + x = ["a", "b", "c"][_] +}`, + expResult: []string{"a", "b", "c"}, + }, + { + note: "v0, v1 compile-time violations", + module: `package test +import data.foo +import data.bar as foo + +p[x] { + x = ["a", "b", "c"][_] +}`, + expResult: []string{"a", "b", "c"}, + }, + { + note: "import rego.v1", + module: `package test +import rego.v1 + +p contains x if { + some x in ["a", "b", "c"] +}`, + expResult: []string{"a", "b", "c"}, + }, + { + note: "v0 import rego.v1, v1 compile-time violations", + module: `package test +import rego.v1 + +import data.foo +import data.bar as foo + +p contains x if { + some x in ["a", "b", "c"] +}`, + expErrs: []string{ + "test.rego:5: rego_compile_error: import must not shadow import data.foo", + }, + }, + { + note: "v1", // v1 is NOT the default version + module: `package test + +p contains x if { + some x in ["a", "b", "c"] +}`, + expErrs: []string{ + "test.rego:4: rego_parse_error: unexpected identifier token", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + test.WithTempFS(files, func(root string) { + ctx := context.Background() + + pq, err := New( + Load([]string{root}, nil), + Query("data.test.p"), + ).PrepareForEval(ctx) + + if tc.expErrs != nil { + if err == nil { + t.Fatalf("Expected error but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain %q but got: %v", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + rs, err := pq.Eval(ctx) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(rs) != 1 { + t.Fatalf("Expected exactly one result but got: %v", rs) + } + + if reflect.DeepEqual(rs[0].Expressions[0].Value, tc.expResult) { + t.Fatalf("Expected %v but got: %v", tc.expResult, rs[0].Expressions[0].Value) + } + } + }) + }) + } +} diff --git a/rego/resultset.go b/rego/resultset.go new file mode 100644 index 0000000000..5c03360dfa --- /dev/null +++ b/rego/resultset.go @@ -0,0 +1,22 @@ +package rego + +import ( + v1 "github.com/open-policy-agent/opa/v1/rego" +) + +// ResultSet represents a collection of output from Rego evaluation. An empty +// result set represents an undefined query. +type ResultSet = v1.ResultSet + +// Vars represents a collection of variable bindings. The keys are the variable +// names and the values are the binding values. +type Vars = v1.Vars + +// Result defines the output of Rego evaluation. +type Result = v1.Result + +// Location defines a position in a Rego query or module. +type Location = v1.Location + +// ExpressionValue defines the value of an expression in a Rego query. +type ExpressionValue = v1.ExpressionValue diff --git a/repl/doc.go b/repl/doc.go new file mode 100644 index 0000000000..fa92ef10ee --- /dev/null +++ b/repl/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package repl diff --git a/repl/errors.go b/repl/errors.go new file mode 100644 index 0000000000..2be89dca20 --- /dev/null +++ b/repl/errors.go @@ -0,0 +1,16 @@ +// Copyright 2016 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 repl + +import v1 "github.com/open-policy-agent/opa/v1/repl" + +// Error is the error type returned by the REPL. +type Error = v1.Error + +const ( + // BadArgsErr indicates bad arguments were provided to a built-in REPL + // command. + BadArgsErr string = v1.BadArgsErr +) diff --git a/repl/repl.go b/repl/repl.go new file mode 100644 index 0000000000..d88742d7af --- /dev/null +++ b/repl/repl.go @@ -0,0 +1,26 @@ +// Copyright 2016 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 repl implements a Read-Eval-Print-Loop (REPL) for interacting with the policy engine. +// +// The REPL is typically used from the command line, however, it can also be used as a library. +// nolint: goconst // String reuse here doesn't make sense to deduplicate. +package repl + +import ( + "io" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/repl" +) + +// REPL represents an instance of the interactive shell. +type REPL = v1.REPL + +// New returns a new instance of the REPL. +func New(store storage.Store, historyPath string, output io.Writer, outputFormat string, errLimit int, banner string) *REPL { + return v1.New(store, historyPath, output, outputFormat, errLimit, banner). + WithRegoVersion(ast.DefaultRegoVersion) +} diff --git a/repl/repl_test.go b/repl/repl_test.go new file mode 100644 index 0000000000..c2bb5c279f --- /dev/null +++ b/repl/repl_test.go @@ -0,0 +1,161 @@ +// Copyright 2024 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 repl + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/storage/inmem" + "github.com/open-policy-agent/opa/util" +) + +func TestOneShot_DefaultRegoVersion(t *testing.T) { + type action struct { + line string + expOutput string + expErrs []string + } + + tests := []struct { + note string + actions []action + }{ + { + note: "v0 rule, v1 compile-time violation", + actions: []action{ + { + line: "b { data := 1; data == 1 }", + expOutput: "Rule 'b' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 keywords used", + actions: []action{ + { + line: "a contains 2 if { true }", + expErrs: []string{ + "rego_unsafe_var_error: var a is unsafe", + }, + }, + }, + }, + { + note: "v1 keywords not used", + actions: []action{ + { + line: "a[2] { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 keywords imported", + actions: []action{ + { + line: "import future.keywords", + }, + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "rego.v1 imported", + actions: []action{ + { + line: "import rego.v1", + }, + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 keywords", + actions: []action{ + { + line: "a contains 2 if { true }", + expErrs: []string{ + "rego_unsafe_var_error: var a is unsafe", + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + ctx := context.Background() + store := newTestStore() + var buffer bytes.Buffer + repl := newRepl(store, &buffer) + + for _, action := range tc.actions { + err := repl.OneShot(ctx, action.line) + + if len(action.expErrs) != 0 { + if err == nil { + t.Fatalf("Expected error but got: %s", buffer.String()) + } + + for _, e := range action.expErrs { + if !strings.Contains(err.Error(), e) { + t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", e, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expectOutput(t, buffer.String(), action.expOutput) + } + } + }) + } +} + +func expectOutput(t *testing.T, output string, expected string) { + t.Helper() + if output != expected { + t.Errorf("Repl output: expected %#v but got %#v", expected, output) + } +} + +func newRepl(store storage.Store, buffer *bytes.Buffer) *REPL { + repl := New(store, "", buffer, "", 0, "") + return repl +} + +func newTestStore() storage.Store { + input := ` + { + "a": [ + { + "b": { + "c": [true,2,false] + } + }, + { + "b": { + "c": [false,true,1] + } + } + ] + } + ` + var data map[string]interface{} + err := util.UnmarshalJSON([]byte(input), &data) + if err != nil { + panic(err) + } + return inmem.NewFromObject(data) +} diff --git a/resolver/doc.go b/resolver/doc.go new file mode 100644 index 0000000000..5d6675dffb --- /dev/null +++ b/resolver/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package resolver diff --git a/resolver/interface.go b/resolver/interface.go new file mode 100644 index 0000000000..12b8a6aed4 --- /dev/null +++ b/resolver/interface.go @@ -0,0 +1,18 @@ +// 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 resolver + +import ( + v1 "github.com/open-policy-agent/opa/v1/resolver" +) + +// Resolver defines an external value resolver for OPA evaluations. +type Resolver = v1.Resolver + +// Input as provided to a Resolver instance when evaluating. +type Input = v1.Input + +// Result of resolving a ref. +type Result = v1.Result diff --git a/resolver/wasm/doc.go b/resolver/wasm/doc.go new file mode 100644 index 0000000000..165997e4a2 --- /dev/null +++ b/resolver/wasm/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package wasm diff --git a/resolver/wasm/wasm.go b/resolver/wasm/wasm.go new file mode 100644 index 0000000000..ff8b9b8208 --- /dev/null +++ b/resolver/wasm/wasm.go @@ -0,0 +1,20 @@ +// 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 wasm + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/resolver/wasm" +) + +// New creates a new Resolver instance which is using the Wasm module +// policy for the given entrypoint ref. +func New(entrypoints []ast.Ref, policy []byte, data interface{}) (*Resolver, error) { + return v1.New(entrypoints, policy, data) +} + +// Resolver implements the resolver.Resolver interface +// using Wasm modules to perform an evaluation. +type Resolver = v1.Resolver diff --git a/runtime/doc.go b/runtime/doc.go new file mode 100644 index 0000000000..7d72c66a7f --- /dev/null +++ b/runtime/doc.go @@ -0,0 +1,10 @@ +// Copyright 2016 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 runtime contains the entry point to the policy engine. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package runtime diff --git a/runtime/logging.go b/runtime/logging.go new file mode 100644 index 0000000000..5b84ea6a51 --- /dev/null +++ b/runtime/logging.go @@ -0,0 +1,21 @@ +// Copyright 2016 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 runtime + +import ( + "net/http" + + "github.com/open-policy-agent/opa/logging" + v1 "github.com/open-policy-agent/opa/v1/runtime" +) + +// LoggingHandler returns an http.Handler that will print log messages +// containing the request information as well as response status and latency. +type LoggingHandler = v1.LoggingHandler + +// NewLoggingHandler returns a new http.Handler. +func NewLoggingHandler(logger logging.Logger, inner http.Handler) http.Handler { + return v1.NewLoggingHandler(logger, inner) +} diff --git a/runtime/runtime.go b/runtime/runtime.go new file mode 100644 index 0000000000..42b4bda2cf --- /dev/null +++ b/runtime/runtime.go @@ -0,0 +1,40 @@ +// Copyright 2016 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 runtime + +import ( + "context" + + "github.com/open-policy-agent/opa/plugins" + v1 "github.com/open-policy-agent/opa/v1/runtime" +) + +// RegisterPlugin registers a plugin factory with the runtime +// package. When the runtime is created, the factories are used to parse +// plugin configuration and instantiate plugins. If no configuration is +// provided, plugins are not instantiated. This function is idempotent. +func RegisterPlugin(name string, factory plugins.Factory) { + v1.RegisterPlugin(name, factory) +} + +// Params stores the configuration for an OPA instance. +type Params = v1.Params + +// LoggingConfig stores the configuration for OPA's logging behaviour. +type LoggingConfig = v1.LoggingConfig + +// NewParams returns a new Params object. +func NewParams() Params { + return v1.NewParams() +} + +// Runtime represents a single OPA instance. +type Runtime = v1.Runtime + +// NewRuntime returns a new Runtime object initialized with params. Clients must +// call StartServer() or StartREPL() to start the runtime in either mode. +func NewRuntime(ctx context.Context, params Params) (*Runtime, error) { + return v1.NewRuntime(ctx, params) +} diff --git a/schemas/doc.go b/schemas/doc.go new file mode 100644 index 0000000000..968bdea043 --- /dev/null +++ b/schemas/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package schemas diff --git a/schemas/schemas.go b/schemas/schemas.go new file mode 100644 index 0000000000..42fa9db9ba --- /dev/null +++ b/schemas/schemas.go @@ -0,0 +1,13 @@ +// 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 ( + v1 "github.com/open-policy-agent/opa/v1/schemas" +) + +// FS contains the known schemas for OPA's Authorization Policy etc. +// "authorizationPolicy.json" contains the input schema for OPA's Authorization Policy +var FS = v1.FS diff --git a/sdk/doc.go b/sdk/doc.go new file mode 100644 index 0000000000..4386bae762 --- /dev/null +++ b/sdk/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package sdk diff --git a/sdk/opa.go b/sdk/opa.go new file mode 100644 index 0000000000..2c92d635ca --- /dev/null +++ b/sdk/opa.go @@ -0,0 +1,41 @@ +// Copyright 2024 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 sdk + +import ( + "context" + + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/sdk" +) + +type OPA = v1.OPA + +type Options = v1.Options + +type DecisionOptions = v1.DecisionOptions + +type DecisionResult = v1.DecisionResult + +type PartialQueryMapper = v1.PartialQueryMapper + +type PartialOptions = v1.PartialOptions + +type PartialResult = v1.PartialResult + +type Error = v1.Error + +type RawMapper = v1.RawMapper + +func New(ctx context.Context, opts Options) (*OPA, error) { + if opts.RegoVersion == ast.RegoUndefined { + opts.RegoVersion = ast.DefaultRegoVersion + } + return v1.New(ctx, opts) +} + +func IsUndefinedErr(err error) bool { + return v1.IsUndefinedErr(err) +} diff --git a/sdk/opa_test.go b/sdk/opa_test.go new file mode 100644 index 0000000000..684f3c9a0a --- /dev/null +++ b/sdk/opa_test.go @@ -0,0 +1,87 @@ +// Copyright 2024 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 sdk_test + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/open-policy-agent/opa/sdk" + sdktest "github.com/open-policy-agent/opa/v1/sdk/test" +) + +func TestDefaultRegoVersion(t *testing.T) { + + ctx := context.Background() + + server := sdktest.MustNewServer( + sdktest.RawBundles(true), + sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ + // v0 module + "main.rego": ` +package system + +main { + p[_] == "a" +} + +p[x] { + x = "a" +} + +str = "foo" + +loopback = input +`, + }), + ) + + defer server.Stop() + + config := fmt.Sprintf(`{ + "services": { + "test": { + "url": %q + } + }, + "bundles": { + "test": { + "resource": "/bundles/bundle.tar.gz" + } + } + }`, server.URL()) + + opa, err := sdk.New(ctx, sdk.Options{ + Config: strings.NewReader(config), + }) + if err != nil { + t.Fatal(err) + } + + defer opa.Stop(ctx) + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{}); err != nil { + t.Fatal(err) + } else if decision, ok := result.Result.(bool); !ok || !decision { + t.Fatal("expected true but got:", decision, ok) + } + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{Path: "/system/str"}); err != nil { + t.Fatal(err) + } else if decision, ok := result.Result.(string); !ok || decision != "foo" { + t.Fatal(`expected "foo" but got:`, decision) + } + + exp := map[string]interface{}{"foo": "bar"} + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{Path: "/system/loopback", Input: map[string]interface{}{"foo": "bar"}}); err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(result.Result, exp) { + t.Fatalf("expected %v but got %v", exp, result.Result) + } +} diff --git a/sdk/test/doc.go b/sdk/test/doc.go new file mode 100644 index 0000000000..dffe11ff7c --- /dev/null +++ b/sdk/test/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package test diff --git a/sdk/test/test.go b/sdk/test/test.go new file mode 100644 index 0000000000..85e2e2a200 --- /dev/null +++ b/sdk/test/test.go @@ -0,0 +1,65 @@ +package test + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/sdk/test" +) + +// MockBundle sets a bundle named file on the test server containing the given +// policies. +func MockBundle(file string, policies map[string]string) func(*Server) error { + return v1.MockBundle(file, policies) +} + +// MockOCIBundle prepares the server to allow serving "/v2" OCI responses from the supplied policies +// Ref parameter must be in the form of //: that will be used in detecting future calls +func MockOCIBundle(ref string, policies map[string]string) func(*Server) error { + return v1.MockOCIBundle(ref, policies) +} + +// Ready provides a channel that the server will use to gate readiness. The +// caller can provide this channel to prevent the server from becoming ready. +// The server will response with HTTP 500 responses until ready. The caller +// should close the channel to indicate readiness. +func Ready(ch chan struct{}) func(*Server) error { + return v1.Ready(ch) +} + +// Server provides a mock HTTP server for testing the SDK and integrations. +type Server = v1.Server + +// MustNewServer returns a new Server for test purposes or panics if an error occurs. +func MustNewServer(opts ...func(*Server) error) *Server { + return v1.MustNewServer(setRegoVersion(opts)...) +} + +// NewServer returns a new Server for test purposes. +func NewServer(opts ...func(*Server) error) (*Server, error) { + return v1.NewServer(setRegoVersion(opts)...) +} + +func RawBundles(raw bool) func(*Server) error { + return v1.RawBundles(raw) +} + +// ParserOptions sets the ast.ParserOptions to use when parsing modules when preparing bundles. +func ParserOptions(popts ast.ParserOptions) func(*Server) error { + return v1.ParserOptions(popts) +} + +func setRegoVersion(opts []func(*Server) error) []func(*v1.Server) error { + cpy := make([]func(*v1.Server) error, 0, len(opts)+1) + cpy = append(cpy, opts...) + + // Sets rego-version to default (v0) if not set. + // Must be last in list of options. + cpy = append(cpy, func(s *v1.Server) error { + if popts := s.ParserOptions(); popts.RegoVersion == ast.RegoUndefined { + popts.RegoVersion = ast.DefaultRegoVersion + return ParserOptions(popts)(s) + } + return nil + }) + + return cpy +} diff --git a/server/authorizer/authorizer.go b/server/authorizer/authorizer.go new file mode 100644 index 0000000000..c0ffcc736f --- /dev/null +++ b/server/authorizer/authorizer.go @@ -0,0 +1,71 @@ +// Copyright 2017 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 authorizer provides authorization handlers to the server. +package authorizer + +import ( + "context" + "net/http" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/topdown/cache" + "github.com/open-policy-agent/opa/topdown/print" + v1 "github.com/open-policy-agent/opa/v1/server/authorizer" +) + +// Basic provides policy-based authorization over incoming requests. +type Basic = v1.Basic + +// Runtime returns an argument that sets the runtime on the authorizer. +func Runtime(term *ast.Term) func(*Basic) { + return v1.Runtime(term) +} + +// Decision returns an argument that sets the path of the authorization decision +// to query. +func Decision(ref func() ast.Ref) func(*Basic) { + return v1.Decision(ref) +} + +// PrintHook sets the object to use for handling print statement outputs. +func PrintHook(printHook print.Hook) func(*Basic) { + return v1.PrintHook(printHook) +} + +// EnablePrintStatements enables print() calls. If this option is not provided, +// print() calls will be erased from the policy. This option only applies to +// queries and policies that passed as raw strings, i.e., this function will not +// have any affect if the caller supplies the ast.Compiler instance. +func EnablePrintStatements(yes bool) func(r *Basic) { + return v1.EnablePrintStatements(yes) +} + +// InterQueryCache enables the inter-query cache on the authorizer +func InterQueryCache(interQueryCache cache.InterQueryCache) func(*Basic) { + return v1.InterQueryCache(interQueryCache) +} + +// InterQueryValueCache enables the inter-query value cache on the authorizer +func InterQueryValueCache(interQueryValueCache cache.InterQueryValueCache) func(*Basic) { + return v1.InterQueryValueCache(interQueryValueCache) +} + +// NewBasic returns a new Basic object. +func NewBasic(inner http.Handler, compiler func() *ast.Compiler, store storage.Store, opts ...func(*Basic)) http.Handler { + return v1.NewBasic(inner, compiler, store, opts...) +} + +// SetBodyOnContext adds the parsed input value to the context. This function is only +// exposed for test purposes. +func SetBodyOnContext(ctx context.Context, x interface{}) context.Context { + return v1.SetBodyOnContext(ctx, x) +} + +// GetBodyOnContext returns the parsed input from the request context if it exists. +// The authorizer saves the parsed input on the context when it runs. +func GetBodyOnContext(ctx context.Context) (interface{}, bool) { + return v1.GetBodyOnContext(ctx) +} diff --git a/server/authorizer/doc.go b/server/authorizer/doc.go new file mode 100644 index 0000000000..3abace2768 --- /dev/null +++ b/server/authorizer/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package authorizer diff --git a/server/buffer.go b/server/buffer.go new file mode 100644 index 0000000000..be44f3a9cd --- /dev/null +++ b/server/buffer.go @@ -0,0 +1,15 @@ +// Copyright 2017 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 server + +import ( + v1 "github.com/open-policy-agent/opa/v1/server" +) + +// Info contains information describing a policy decision. +type Info = v1.Info + +// BundleInfo contains information describing a bundle. +type BundleInfo = v1.BundleInfo diff --git a/server/doc.go b/server/doc.go new file mode 100644 index 0000000000..4eb7efad2a --- /dev/null +++ b/server/doc.go @@ -0,0 +1,10 @@ +// Copyright 2016 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 server contains the policy engine's server handlers. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package server diff --git a/server/features.go b/server/features.go new file mode 100644 index 0000000000..3b01537220 --- /dev/null +++ b/server/features.go @@ -0,0 +1,10 @@ +// Copyright 2021 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. + +//go:build opa_wasm +// +build opa_wasm + +package server + +import _ "github.com/open-policy-agent/opa/v1/features/wasm" diff --git a/server/handlers/compress.go b/server/handlers/compress.go new file mode 100644 index 0000000000..08c73ac1ae --- /dev/null +++ b/server/handlers/compress.go @@ -0,0 +1,20 @@ +package handlers + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/handlers" +) + +// This handler applies only for data and compile endpoints, for selected HTTP methods +// +// If the client asked for a gzip response, this handler will buffer the response and +// wait until it reached a certain threshold. If the threshold is not hit, the uncompressed response is sent +// +// If a gzip response is not asked by the client, it'll send the uncompressed response +// +// The threshold and the gzip compression level can be modified from server's configuration + +func CompressHandler(handler http.Handler, gzipMinLength int, gzipCompressionLevel int) http.Handler { + return v1.CompressHandler(handler, gzipMinLength, gzipCompressionLevel) +} diff --git a/server/handlers/decoding.go b/server/handlers/decoding.go new file mode 100644 index 0000000000..a276972ab9 --- /dev/null +++ b/server/handlers/decoding.go @@ -0,0 +1,19 @@ +package handlers + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/handlers" +) + +// This handler provides hard limits on the size of the request body, for both +// the raw body content, and also for the decompressed size when gzip +// compression is used. +// +// The Content-Length restriction happens here in the handler, but the +// decompressed size limit is enforced later, in `util.ReadMaybeCompressedBody`. +// The handler passes the gzip size limits down to that function through the +// request context whenever gzip encoding is present. +func DecodingLimitsHandler(handler http.Handler, maxLength, gzipMaxLength int64) http.Handler { + return v1.DecodingLimitsHandler(handler, maxLength, gzipMaxLength) +} diff --git a/server/handlers/doc.go b/server/handlers/doc.go new file mode 100644 index 0000000000..9ec8997a0b --- /dev/null +++ b/server/handlers/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package handlers diff --git a/server/identifier/certs.go b/server/identifier/certs.go new file mode 100644 index 0000000000..3a224fa194 --- /dev/null +++ b/server/identifier/certs.go @@ -0,0 +1,18 @@ +package identifier + +import ( + "crypto/x509" + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/identifier" +) + +// ClientCertificates returns the ClientCertificates of the caller associated with ctx. +func ClientCertificates(r *http.Request) ([]*x509.Certificate, bool) { + return v1.ClientCertificates(r) +} + +// SetClientCertificates returns a new http.Request with the ClientCertificates set to v. +func SetClientCertificates(r *http.Request, v []*x509.Certificate) *http.Request { + return v1.SetClientCertificates(r, v) +} diff --git a/server/identifier/doc.go b/server/identifier/doc.go new file mode 100644 index 0000000000..59bda00fd0 --- /dev/null +++ b/server/identifier/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package identifier diff --git a/server/identifier/identifier.go b/server/identifier/identifier.go new file mode 100644 index 0000000000..908fb62c33 --- /dev/null +++ b/server/identifier/identifier.go @@ -0,0 +1,22 @@ +// Copyright 2017 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 identifier provides handlers for associating identity information with incoming requests. +package identifier + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/identifier" +) + +// Identity returns the identity of the caller associated with ctx. +func Identity(r *http.Request) (string, bool) { + return v1.Identity(r) +} + +// SetIdentity returns a new http.Request with the identity set to v. +func SetIdentity(r *http.Request, v string) *http.Request { + return v1.SetIdentity(r, v) +} diff --git a/server/identifier/tls.go b/server/identifier/tls.go new file mode 100644 index 0000000000..6fe432290c --- /dev/null +++ b/server/identifier/tls.go @@ -0,0 +1,19 @@ +// 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 identifier + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/identifier" +) + +// TLSBased extracts the CN of the client's TLS ceritificate +type TLSBased = v1.TLSBased + +// NewTLSBased returns a new TLSBased object. +func NewTLSBased(inner http.Handler) *TLSBased { + return v1.NewTLSBased(inner) +} diff --git a/server/identifier/token.go b/server/identifier/token.go new file mode 100644 index 0000000000..0126e3f809 --- /dev/null +++ b/server/identifier/token.go @@ -0,0 +1,15 @@ +package identifier + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/server/identifier" +) + +// TokenBased extracts Bearer tokens from the request. +type TokenBased = v1.TokenBased + +// NewTokenBased returns a new TokenBased object. +func NewTokenBased(inner http.Handler) *TokenBased { + return v1.NewTokenBased(inner) +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000000..fb19a9bc0e --- /dev/null +++ b/server/server.go @@ -0,0 +1,65 @@ +// Copyright 2016 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 server + +import ( + v1 "github.com/open-policy-agent/opa/v1/server" +) + +// AuthenticationScheme enumerates the supported authentication schemes. The +// authentication scheme determines how client identities are established. +type AuthenticationScheme = v1.AuthenticationScheme + +// Set of supported authentication schemes. +const ( + AuthenticationOff = v1.AuthenticationOff + AuthenticationToken = v1.AuthenticationToken + AuthenticationTLS = v1.AuthenticationTLS +) + +// AuthorizationScheme enumerates the supported authorization schemes. The authorization +// scheme determines how access to OPA is controlled. +type AuthorizationScheme = v1.AuthorizationScheme + +// Set of supported authorization schemes. +const ( + AuthorizationOff = v1.AuthorizationOff + AuthorizationBasic = v1.AuthorizationBasic +) + +// Set of handlers for use in the "handler" dimension of the duration metric. +const ( + PromHandlerV0Data = v1.PromHandlerV0Data + PromHandlerV1Data = v1.PromHandlerV1Data + PromHandlerV1Query = v1.PromHandlerV1Query + PromHandlerV1Policies = v1.PromHandlerV1Policies + PromHandlerV1Compile = v1.PromHandlerV1Compile + PromHandlerV1Config = v1.PromHandlerV1Config + PromHandlerV1Status = v1.PromHandlerV1Status + PromHandlerIndex = v1.PromHandlerIndex + PromHandlerCatch = v1.PromHandlerCatch + PromHandlerHealth = v1.PromHandlerHealth + PromHandlerAPIAuthz = v1.PromHandlerAPIAuthz +) + +// Server represents an instance of OPA running in server mode. +type Server = v1.Server + +// Metrics defines the interface that the server requires for recording HTTP +// handler metrics. +type Metrics = v1.Metrics + +// TLSConfig represents the TLS configuration for the server. +// This configuration is used to configure file watchers to reload each file as it +// changes on disk. +type TLSConfig = v1.TLSConfig + +// Loop will contain all the calls from the server that we'll be listening on. +type Loop = v1.Loop + +// New returns a new Server. +func New() *Server { + return v1.New() +} diff --git a/server/types/doc.go b/server/types/doc.go new file mode 100644 index 0000000000..d616c7aa56 --- /dev/null +++ b/server/types/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package types diff --git a/server/types/types.go b/server/types/types.go new file mode 100644 index 0000000000..deda9dc7ad --- /dev/null +++ b/server/types/types.go @@ -0,0 +1,245 @@ +// Copyright 2016 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 types contains request/response types and codes for the server. +package types + +import ( + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/topdown" + v1 "github.com/open-policy-agent/opa/v1/server/types" +) + +// Error codes returned by OPA's REST API. +const ( + CodeInternal = v1.CodeInternal + CodeEvaluation = v1.CodeEvaluation + CodeUnauthorized = v1.CodeUnauthorized + CodeInvalidParameter = v1.CodeInvalidParameter + CodeInvalidOperation = v1.CodeInvalidOperation + CodeResourceNotFound = v1.CodeResourceNotFound + CodeResourceConflict = v1.CodeResourceConflict + CodeUndefinedDocument = v1.CodeUndefinedDocument +) + +// ErrorV1 models an error response sent to the client. +type ErrorV1 = v1.ErrorV1 + +// NewErrorV1 returns a new ErrorV1 object. +func NewErrorV1(code, f string, a ...interface{}) *ErrorV1 { + return v1.NewErrorV1(code, f, a...) +} + +// Messages included in error responses. +const ( + MsgCompileModuleError = v1.MsgCompileModuleError + MsgParseQueryError = v1.MsgParseQueryError + MsgCompileQueryError = v1.MsgCompileQueryError + MsgEvaluationError = v1.MsgEvaluationError + MsgUnauthorizedUndefinedError = v1.MsgUnauthorizedUndefinedError + MsgUnauthorizedError = v1.MsgUnauthorizedError + MsgUndefinedError = v1.MsgUndefinedError + MsgMissingError = v1.MsgMissingError + MsgFoundUndefinedError = v1.MsgFoundUndefinedError + MsgPluginConfigError = v1.MsgPluginConfigError + MsgDecodingLimitError = v1.MsgDecodingLimitError + MsgDecodingGzipLimitError = v1.MsgDecodingGzipLimitError +) + +// PatchV1 models a single patch operation against a document. +type PatchV1 = v1.PatchV1 + +// PolicyListResponseV1 models the response message for the Policy API list operation. +type PolicyListResponseV1 = v1.PolicyListResponseV1 + +// PolicyGetResponseV1 models the response message for the Policy API get operation. +type PolicyGetResponseV1 = v1.PolicyGetResponseV1 + +// PolicyPutResponseV1 models the response message for the Policy API put operation. +type PolicyPutResponseV1 = v1.PolicyPutResponseV1 + +// PolicyDeleteResponseV1 models the response message for the Policy API delete operation. +type PolicyDeleteResponseV1 = v1.PolicyDeleteResponseV1 + +// PolicyV1 models a policy module in OPA. +type PolicyV1 = v1.PolicyV1 + +// ProvenanceV1 models a collection of build/version information. +type ProvenanceV1 = v1.ProvenanceV1 + +// ProvenanceBundleV1 models a bundle at some point in time +type ProvenanceBundleV1 = v1.ProvenanceBundleV1 + +// DataRequestV1 models the request message for Data API POST operations. +type DataRequestV1 = v1.DataRequestV1 + +// DataResponseV1 models the response message for Data API read operations. +type DataResponseV1 = v1.DataResponseV1 + +// Warning models DataResponse warnings +type Warning = v1.Warning + +// Warning Codes +const CodeAPIUsageWarn = v1.CodeAPIUsageWarn + +// Warning Messages +const MsgInputKeyMissing = v1.MsgInputKeyMissing + +// NewWarning returns a new Warning object +func NewWarning(code, message string) *Warning { + return v1.NewWarning(code, message) +} + +// MetricsV1 models a collection of performance metrics. +type MetricsV1 = v1.MetricsV1 + +// QueryResponseV1 models the response message for Query API operations. +type QueryResponseV1 = v1.QueryResponseV1 + +// AdhocQueryResultSetV1 models the result of a Query API query. +type AdhocQueryResultSetV1 = v1.AdhocQueryResultSetV1 + +// ExplainModeV1 defines supported values for the "explain" query parameter. +type ExplainModeV1 = v1.ExplainModeV1 + +// Explanation mode enumeration. +const ( + ExplainOffV1 ExplainModeV1 = v1.ExplainOffV1 + ExplainFullV1 ExplainModeV1 = v1.ExplainFullV1 + ExplainNotesV1 ExplainModeV1 = v1.ExplainNotesV1 + ExplainFailsV1 ExplainModeV1 = v1.ExplainFailsV1 + ExplainDebugV1 ExplainModeV1 = v1.ExplainDebugV1 +) + +// TraceV1 models the trace result returned for queries that include the +// "explain" parameter. +type TraceV1 = v1.TraceV1 + +// TraceV1Raw models the trace result returned for queries that include the +// "explain" parameter. The trace is modelled as series of trace events that +// identify the expression, local term bindings, query hierarchy, etc. +type TraceV1Raw = v1.TraceV1Raw + +// TraceV1Pretty models the trace result returned for queries that include the "explain" +// parameter. The trace is modelled as a human readable array of strings representing the +// evaluation of the query. +type TraceV1Pretty = v1.TraceV1Pretty + +// NewTraceV1 returns a new TraceV1 object. +func NewTraceV1(trace []*topdown.Event, pretty bool) (result TraceV1, err error) { + return v1.NewTraceV1(trace, pretty) +} + +// TraceEventV1 represents a step in the query evaluation process. +type TraceEventV1 = v1.TraceEventV1 + +// BindingsV1 represents a set of term bindings. +type BindingsV1 = v1.BindingsV1 + +// BindingV1 represents a single term binding. +type BindingV1 = v1.BindingV1 + +// NewBindingsV1 returns a new BindingsV1 object. +func NewBindingsV1(locals *ast.ValueMap) (result []*BindingV1) { + return v1.NewBindingsV1(locals) +} + +// CompileRequestV1 models the request message for Compile API operations. +type CompileRequestV1 = v1.CompileRequestV1 + +// CompileResponseV1 models the response message for Compile API operations. +type CompileResponseV1 = v1.CompileResponseV1 + +// PartialEvaluationResultV1 represents the output of partial evaluation and is +// included in Compile API responses. +type PartialEvaluationResultV1 = v1.PartialEvaluationResultV1 + +// QueryRequestV1 models the request message for Query API operations. +type QueryRequestV1 = v1.QueryRequestV1 + +// ConfigResponseV1 models the response message for Config API operations. +type ConfigResponseV1 = v1.ConfigResponseV1 + +// StatusResponseV1 models the response message for Status API (pull) operations. +type StatusResponseV1 = v1.StatusResponseV1 + +// HealthResponseV1 models the response message for Health API operations. +type HealthResponseV1 = v1.HealthResponseV1 + +const ( + // ParamQueryV1 defines the name of the HTTP URL parameter that specifies + // values for the request query. + ParamQueryV1 = v1.ParamQueryV1 + + // ParamInputV1 defines the name of the HTTP URL parameter that specifies + // values for the "input" document. + ParamInputV1 = v1.ParamInputV1 + + // ParamPrettyV1 defines the name of the HTTP URL parameter that indicates + // the client wants to receive a pretty-printed version of the response. + ParamPrettyV1 = v1.ParamPrettyV1 + + // ParamExplainV1 defines the name of the HTTP URL parameter that indicates the + // client wants to receive explanations in addition to the result. + ParamExplainV1 = v1.ParamExplainV1 + + // ParamMetricsV1 defines the name of the HTTP URL parameter that indicates + // the client wants to receive performance metrics in addition to the + // result. + ParamMetricsV1 = v1.ParamMetricsV1 + + // ParamInstrumentV1 defines the name of the HTTP URL parameter that + // indicates the client wants to receive instrumentation data for + // diagnosing performance issues. + ParamInstrumentV1 = v1.ParamInstrumentV1 + + // ParamProvenanceV1 defines the name of the HTTP URL parameter that indicates + // the client wants build and version information in addition to the result. + ParamProvenanceV1 = v1.ParamProvenanceV1 + + // ParamBundleActivationV1 defines the name of the HTTP URL parameter that + // indicates the client wants to include bundle activation in the results + // of the health API. + // Deprecated: Use ParamBundlesActivationV1 instead. + ParamBundleActivationV1 = v1.ParamBundleActivationV1 + + // ParamBundlesActivationV1 defines the name of the HTTP URL parameter that + // indicates the client wants to include bundle activation in the results + // of the health API. + ParamBundlesActivationV1 = v1.ParamBundlesActivationV1 + + // ParamPluginsV1 defines the name of the HTTP URL parameter that + // indicates the client wants to include bundle status in the results + // of the health API. + ParamPluginsV1 = v1.ParamPluginsV1 + + // ParamExcludePluginV1 defines the name of the HTTP URL parameter that + // indicates the client wants to exclude plugin status in the results + // of the health API for the specified plugin(s) + ParamExcludePluginV1 = v1.ParamExcludePluginV1 + + // ParamStrictBuiltinErrors names the HTTP URL parameter that indicates the client + // wants built-in function errors to be treated as fatal. + ParamStrictBuiltinErrors = v1.ParamStrictBuiltinErrors +) + +// BadRequestErr represents an error condition raised if the caller passes +// invalid parameters. +type BadRequestErr = v1.BadRequestErr + +// BadPatchOperationErr returns BadRequestErr indicating the patch operation was +// invalid. +func BadPatchOperationErr(op string) error { + return v1.BadPatchOperationErr(op) +} + +// BadPatchPathErr returns BadRequestErr indicating the patch path was invalid. +func BadPatchPathErr(path string) error { + return v1.BadPatchPathErr(path) +} + +// IsBadRequest returns true if err is a BadRequestErr. +func IsBadRequest(err error) bool { + return v1.IsBadRequest(err) +} diff --git a/server/writer/doc.go b/server/writer/doc.go new file mode 100644 index 0000000000..245c404fe2 --- /dev/null +++ b/server/writer/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package writer diff --git a/server/writer/writer.go b/server/writer/writer.go new file mode 100644 index 0000000000..2994fbf1a1 --- /dev/null +++ b/server/writer/writer.go @@ -0,0 +1,56 @@ +// Copyright 2017 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 writer contains utilities for writing responses in the server. +package writer + +import ( + "net/http" + + "github.com/open-policy-agent/opa/v1/server/types" + v1 "github.com/open-policy-agent/opa/v1/server/writer" +) + +// HTTPStatus is used to set a specific status code +// Adapted from https://stackoverflow.com/questions/27711154/what-response-code-to-return-on-a-non-supported-http-method-on-rest +func HTTPStatus(code int) http.HandlerFunc { + return v1.HTTPStatus(code) +} + +// ErrorAuto writes a response with status and code set automatically based on +// the type of err. +func ErrorAuto(w http.ResponseWriter, err error) { + v1.ErrorAuto(w, err) +} + +// ErrorString writes a response with specified status, code, and message set to +// the err's string representation. +func ErrorString(w http.ResponseWriter, status int, code string, err error) { + v1.ErrorString(w, status, code, err) +} + +// Error writes a response with specified status and error response. +func Error(w http.ResponseWriter, status int, err *types.ErrorV1) { + v1.Error(w, status, err) +} + +// JSON writes a response with the specified status code and object. The object +// will be JSON serialized. +// Deprecated: This method is problematic when using a non-200 status `code`: if +// encoding the payload fails, it'll print "superfluous call to WriteHeader()" +// logs. +func JSON(w http.ResponseWriter, code int, v interface{}, pretty bool) { + v1.JSON(w, code, v, pretty) +} + +// JSONOK is a helper for status "200 OK" responses +func JSONOK(w http.ResponseWriter, v interface{}, pretty bool) { + v1.JSONOK(w, v, pretty) +} + +// Bytes writes a response with the specified status code and bytes. +// Deprecated: Unused in OPA, will be removed in the future. +func Bytes(w http.ResponseWriter, code int, bs []byte) { + v1.Bytes(w, code, bs) +} diff --git a/storage/disk/config.go b/storage/disk/config.go new file mode 100644 index 0000000000..94a32c0971 --- /dev/null +++ b/storage/disk/config.go @@ -0,0 +1,17 @@ +// Copyright 2022 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 disk + +import ( + v1 "github.com/open-policy-agent/opa/v1/storage/disk" +) + +var ErrInvalidPartitionPath = v1.ErrInvalidPartitionPath + +// OptionsFromConfig parses the passed config, extracts the disk storage +// settings, validates it, and returns a *Options struct pointer on success. +func OptionsFromConfig(raw []byte, id string) (*Options, error) { + return v1.OptionsFromConfig(raw, id) +} diff --git a/storage/disk/disk.go b/storage/disk/disk.go new file mode 100644 index 0000000000..ed71938068 --- /dev/null +++ b/storage/disk/disk.go @@ -0,0 +1,77 @@ +// Copyright 2021 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 disk provides disk-based implementation of the storage.Store +// interface. +// +// The disk.Store implementation uses an embedded key-value store to persist +// policies and data. Policy modules are stored as raw byte strings with one +// module per key. Data is mapped to the underlying key-value store with the +// assistance of caller-supplied "partitions". Partitions allow the caller to +// control the portions of the /data namespace that are mapped to individual +// keys. Operations that span multiple keys (e.g., a read against the entirety +// of /data) are more expensive than reads that target a specific key because +// the storage layer has to reconstruct the object from individual key-value +// pairs and page all of the data into memory. By supplying partitions that +// align with lookups in the policies, callers can optimize policy evaluation. +// +// Partitions are specified as a set of storage paths (e.g., {/foo/bar} declares +// a single partition at /foo/bar). Each partition tells the store that values +// under the partition path should be mapped to individual keys. Values that +// fall outside of the partitions are stored at adjacent keys without further +// splitting. For example, given the partition set {/foo/bar}, /foo/bar/abcd and +// /foo/bar/efgh are be written to separate keys. All other values under /foo +// are not split any further (e.g., all values under /foo/baz would be written +// to a single key). Similarly, values that fall outside of partitions are +// stored under individual keys at the root (e.g., the full extent of the value +// at /qux would be stored under one key.) +// There is support for wildcards in partitions: {/foo/*} will cause /foo/bar/abc +// and /foo/buz/def to be written to separate keys. Multiple wildcards are +// supported (/tenants/*/users/*/bindings), and they can also appear at the end +// of a partition (/users/*). +// +// All keys written by the disk.Store implementation are prefixed as follows: +// +// /// +// +// The value represents the version of the schema understood by +// this version of OPA. Currently this is always set to 1. The +// value represents the version of the partition layout +// supplied by the caller. Currently this is always set to 1. Currently, the +// disk.Store implementation only supports _additive_ changes to the +// partitioning layout, i.e., new partitions can be added as long as they do not +// overlap with existing unpartitioned data. The value is either "data" +// or "policies" depending on the value being stored. +// +// The disk.Store implementation attempts to be compatible with the inmem.store +// implementation however there are some minor differences: +// +// * Writes that add partitioned values implicitly create an object hierarchy +// containing the value (e.g., `add /foo/bar/abcd` implicitly creates the +// structure `{"foo": {"bar": {"abcd": ...}}}`). This is unavoidable because of +// how nested /data values are mapped to key-value pairs. +// +// * Trigger events do not include a set of changed paths because the underlying +// key-value store does not make them available. +package disk + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/open-policy-agent/opa/logging" + v1 "github.com/open-policy-agent/opa/v1/storage/disk" +) + +// Options contains parameters that configure the disk-based store. +type Options = v1.Options + +// Store provides a disk-based implementation of the storage.Store interface. +type Store = v1.Store + +// New returns a new disk-based store based on the provided options. +func New(ctx context.Context, logger logging.Logger, prom prometheus.Registerer, opts Options) (*Store, error) { + return v1.New(ctx, logger, prom, opts) +} diff --git a/storage/disk/doc.go b/storage/disk/doc.go new file mode 100644 index 0000000000..89774340f7 --- /dev/null +++ b/storage/disk/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package disk diff --git a/storage/doc.go b/storage/doc.go new file mode 100644 index 0000000000..c33db689ed --- /dev/null +++ b/storage/doc.go @@ -0,0 +1,10 @@ +// Copyright 2016 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 storage exposes the policy engine's storage layer. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package storage diff --git a/storage/errors.go b/storage/errors.go new file mode 100644 index 0000000000..1403b3a988 --- /dev/null +++ b/storage/errors.go @@ -0,0 +1,73 @@ +// Copyright 2016 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 storage + +import ( + v1 "github.com/open-policy-agent/opa/v1/storage" +) + +const ( + // InternalErr indicates an unknown, internal error has occurred. + InternalErr = v1.InternalErr + + // NotFoundErr indicates the path used in the storage operation does not + // locate a document. + NotFoundErr = v1.NotFoundErr + + // WriteConflictErr indicates a write on the path enocuntered a conflicting + // value inside the transaction. + WriteConflictErr = v1.WriteConflictErr + + // InvalidPatchErr indicates an invalid patch/write was issued. The patch + // was rejected. + InvalidPatchErr = v1.InvalidPatchErr + + // InvalidTransactionErr indicates an invalid operation was performed + // inside of the transaction. + InvalidTransactionErr = v1.InvalidTransactionErr + + // TriggersNotSupportedErr indicates the caller attempted to register a + // trigger against a store that does not support them. + TriggersNotSupportedErr = v1.TriggersNotSupportedErr + + // WritesNotSupportedErr indicate the caller attempted to perform a write + // against a store that does not support them. + WritesNotSupportedErr = v1.WritesNotSupportedErr + + // PolicyNotSupportedErr indicate the caller attempted to perform a policy + // management operation against a store that does not support them. + PolicyNotSupportedErr = v1.PolicyNotSupportedErr +) + +// Error is the error type returned by the storage layer. +type Error = v1.Error + +// IsNotFound returns true if this error is a NotFoundErr. +func IsNotFound(err error) bool { + return v1.IsNotFound(err) +} + +// IsWriteConflictError returns true if this error a WriteConflictErr. +func IsWriteConflictError(err error) bool { + return v1.IsWriteConflictError(err) +} + +// IsInvalidPatch returns true if this error is a InvalidPatchErr. +func IsInvalidPatch(err error) bool { + return v1.IsInvalidPatch(err) +} + +// IsInvalidTransaction returns true if this error is a InvalidTransactionErr. +func IsInvalidTransaction(err error) bool { + return v1.IsInvalidTransaction(err) +} + +// IsIndexingNotSupported is a stub for backwards-compatibility. +// +// Deprecated: We no longer return IndexingNotSupported errors, so it is +// unnecessary to check for them. +func IsIndexingNotSupported(err error) bool { + return v1.IsIndexingNotSupported(err) +} diff --git a/storage/inmem/doc.go b/storage/inmem/doc.go new file mode 100644 index 0000000000..5f536b66dd --- /dev/null +++ b/storage/inmem/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package inmem diff --git a/storage/inmem/inmem.go b/storage/inmem/inmem.go new file mode 100644 index 0000000000..0a41b9d0da --- /dev/null +++ b/storage/inmem/inmem.go @@ -0,0 +1,56 @@ +// Copyright 2016 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 inmem implements an in-memory version of the policy engine's storage +// layer. +// +// The in-memory store is used as the default storage layer implementation. The +// in-memory store supports multi-reader/single-writer concurrency with +// rollback. +// +// Callers should assume the in-memory store does not make copies of written +// data. Once data is written to the in-memory store, it should not be modified +// (outside of calling Store.Write). Furthermore, data read from the in-memory +// store should be treated as read-only. +package inmem + +import ( + "io" + + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/storage/inmem" +) + +// New returns an empty in-memory store. +func New() storage.Store { + return v1.New() +} + +// NewWithOpts returns an empty in-memory store, with extra options passed. +func NewWithOpts(opts ...Opt) storage.Store { + return v1.NewWithOpts(opts...) +} + +// NewFromObject returns a new in-memory store from the supplied data object. +func NewFromObject(data map[string]interface{}) storage.Store { + return v1.NewFromObject(data) +} + +// NewFromObjectWithOpts returns a new in-memory store from the supplied data object, with the +// options passed. +func NewFromObjectWithOpts(data map[string]interface{}, opts ...Opt) storage.Store { + return v1.NewFromObjectWithOpts(data, opts...) +} + +// NewFromReader returns a new in-memory store from a reader that produces a +// JSON serialized object. This function is for test purposes. +func NewFromReader(r io.Reader) storage.Store { + return v1.NewFromReader(r) +} + +// NewFromReader returns a new in-memory store from a reader that produces a +// JSON serialized object, with extra options. This function is for test purposes. +func NewFromReaderWithOpts(r io.Reader, opts ...Opt) storage.Store { + return v1.NewFromReaderWithOpts(r, opts...) +} diff --git a/storage/inmem/opts.go b/storage/inmem/opts.go new file mode 100644 index 0000000000..43f03ef27b --- /dev/null +++ b/storage/inmem/opts.go @@ -0,0 +1,35 @@ +package inmem + +import v1 "github.com/open-policy-agent/opa/v1/storage/inmem" + +// An Opt modifies store at instantiation. +type Opt = v1.Opt + +// OptRoundTripOnWrite sets whether incoming objects written to store are +// round-tripped through JSON to ensure they are serializable to JSON. +// +// Callers should disable this if they can guarantee all objects passed to +// Write() are serializable to JSON. Failing to do so may result in undefined +// behavior, including panics. +// +// Usually, when only storing objects in the inmem store that have been read +// via encoding/json, this is safe to disable, and comes with an improvement +// in performance and memory use. +// +// If setting to false, callers should deep-copy any objects passed to Write() +// unless they can guarantee the objects will not be mutated after being written, +// and that mutations happening to the objects after they have been passed into +// Write() don't affect their logic. +func OptRoundTripOnWrite(enabled bool) Opt { + return v1.OptRoundTripOnWrite(enabled) +} + +// OptReturnASTValuesOnRead sets whether data values added to the store should be +// eagerly converted to AST values, which are then returned on read. +// +// When enabled, this feature does not sanity check data before converting it to AST values, +// which may result in panics if the data is not valid. Callers should ensure that passed data +// can be serialized to AST values; otherwise, it's recommended to also enable OptRoundTripOnWrite. +func OptReturnASTValuesOnRead(enabled bool) Opt { + return v1.OptReturnASTValuesOnRead(enabled) +} diff --git a/storage/inmem/test/doc.go b/storage/inmem/test/doc.go new file mode 100644 index 0000000000..4e2f40c478 --- /dev/null +++ b/storage/inmem/test/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package test diff --git a/storage/inmem/test/testutil.go b/storage/inmem/test/testutil.go new file mode 100644 index 0000000000..dda9eb8f69 --- /dev/null +++ b/storage/inmem/test/testutil.go @@ -0,0 +1,22 @@ +// Copyright 2022 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 test + +import ( + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/storage/inmem/test" +) + +// New returns an inmem store with some common options set: opt-out of write +// roundtripping. +func New() storage.Store { + return v1.New() +} + +// NewFromObject returns an inmem store from the passed object, with some +// common options set: opt-out of write roundtripping. +func NewFromObject(x map[string]interface{}) storage.Store { + return v1.NewFromObject(x) +} diff --git a/storage/interface.go b/storage/interface.go new file mode 100644 index 0000000000..0192c459c8 --- /dev/null +++ b/storage/interface.go @@ -0,0 +1,86 @@ +// Copyright 2016 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 storage + +import ( + v1 "github.com/open-policy-agent/opa/v1/storage" +) + +// Transaction defines the interface that identifies a consistent snapshot over +// the policy engine's storage layer. +type Transaction = v1.Transaction + +// Store defines the interface for the storage layer's backend. +type Store = v1.Store + +// MakeDirer defines the interface a Store could realize to override the +// generic MakeDir functionality in storage.MakeDir +type MakeDirer = v1.MakeDirer + +// TransactionParams describes a new transaction. +type TransactionParams = v1.TransactionParams + +// Context is a simple container for key/value pairs. +type Context = v1.Context + +// NewContext returns a new context object. +func NewContext() *Context { + return v1.NewContext() +} + +// WriteParams specifies the TransactionParams for a write transaction. +var WriteParams = v1.WriteParams + +// PatchOp is the enumeration of supposed modifications. +type PatchOp = v1.PatchOp + +// Patch supports add, remove, and replace operations. +const ( + AddOp = v1.AddOp + RemoveOp = v1.RemoveOp + ReplaceOp = v1.ReplaceOp +) + +// WritesNotSupported provides a default implementation of the write +// interface which may be used if the backend does not support writes. +type WritesNotSupported = v1.WritesNotSupported + +// Policy defines the interface for policy module storage. +type Policy = v1.Policy + +// PolicyNotSupported provides a default implementation of the policy interface +// which may be used if the backend does not support policy storage. +type PolicyNotSupported = v1.PolicyNotSupported + +// PolicyEvent describes a change to a policy. +type PolicyEvent = v1.PolicyEvent + +// DataEvent describes a change to a base data document. +type DataEvent = v1.DataEvent + +// TriggerEvent describes the changes that caused the trigger to be invoked. +type TriggerEvent = v1.TriggerEvent + +// TriggerConfig contains the trigger registration configuration. +type TriggerConfig = v1.TriggerConfig + +// Trigger defines the interface that stores implement to register for change +// notifications when the store is changed. +type Trigger = v1.Trigger + +// TriggersNotSupported provides default implementations of the Trigger +// interface which may be used if the backend does not support triggers. +type TriggersNotSupported = v1.TriggersNotSupported + +// TriggerHandle defines the interface that can be used to unregister triggers that have +// been registered on a Store. +type TriggerHandle = v1.TriggerHandle + +// Iterator defines the interface that can be used to read files from a directory starting with +// files at the base of the directory, then sub-directories etc. +type Iterator = v1.Iterator + +// Update contains information about a file +type Update = v1.Update diff --git a/storage/path.go b/storage/path.go new file mode 100644 index 0000000000..91d4f34f2b --- /dev/null +++ b/storage/path.go @@ -0,0 +1,34 @@ +// Copyright 2016 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 storage + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/storage" +) + +// Path refers to a document in storage. +type Path = v1.Path + +// ParsePath returns a new path for the given str. +func ParsePath(str string) (path Path, ok bool) { + return v1.ParsePath(str) +} + +// ParsePathEscaped returns a new path for the given escaped str. +func ParsePathEscaped(str string) (path Path, ok bool) { + return v1.ParsePathEscaped(str) +} + +// NewPathForRef returns a new path for the given ref. +func NewPathForRef(ref ast.Ref) (path Path, err error) { + return v1.NewPathForRef(ref) +} + +// MustParsePath returns a new Path for s. If s cannot be parsed, this function +// will panic. This is mostly for test purposes. +func MustParsePath(s string) Path { + return v1.MustParsePath(s) +} diff --git a/storage/storage.go b/storage/storage.go new file mode 100644 index 0000000000..c02773d985 --- /dev/null +++ b/storage/storage.go @@ -0,0 +1,53 @@ +// Copyright 2016 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 storage + +import ( + "context" + + v1 "github.com/open-policy-agent/opa/v1/storage" +) + +// NewTransactionOrDie is a helper function to create a new transaction. If the +// storage layer cannot create a new transaction, this function will panic. This +// function should only be used for tests. +func NewTransactionOrDie(ctx context.Context, store Store, params ...TransactionParams) Transaction { + return v1.NewTransactionOrDie(ctx, store, params...) +} + +// ReadOne is a convenience function to read a single value from the provided Store. It +// will create a new Transaction to perform the read with, and clean up after itself +// should an error occur. +func ReadOne(ctx context.Context, store Store, path Path) (interface{}, error) { + return v1.ReadOne(ctx, store, path) +} + +// WriteOne is a convenience function to write a single value to the provided Store. It +// will create a new Transaction to perform the write with, and clean up after itself +// should an error occur. +func WriteOne(ctx context.Context, store Store, op PatchOp, path Path, value interface{}) error { + return v1.WriteOne(ctx, store, op, path, value) +} + +// MakeDir inserts an empty object at path. If the parent path does not exist, +// MakeDir will create it recursively. +func MakeDir(ctx context.Context, store Store, txn Transaction, path Path) error { + return v1.MakeDir(ctx, store, txn, path) +} + +// Txn is a convenience function that executes f inside a new transaction +// opened on the store. If the function returns an error, the transaction is +// aborted and the error is returned. Otherwise, the transaction is committed +// and the result of the commit is returned. +func Txn(ctx context.Context, store Store, params TransactionParams, f func(Transaction) error) error { + return v1.Txn(ctx, store, params, f) +} + +// NonEmpty returns a function that tests if a path is non-empty. A +// path is non-empty if a Read on the path returns a value or a Read +// on any of the path prefixes returns a non-object value. +func NonEmpty(ctx context.Context, store Store, txn Transaction) func([]string) (bool, error) { + return v1.NonEmpty(ctx, store, txn) +} diff --git a/test/authz/doc.go b/test/authz/doc.go new file mode 100644 index 0000000000..80389caf4f --- /dev/null +++ b/test/authz/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package authz diff --git a/test/authz/testing.go b/test/authz/testing.go new file mode 100644 index 0000000000..8f2bd570b7 --- /dev/null +++ b/test/authz/testing.go @@ -0,0 +1,43 @@ +// 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 authz contains unit and benchmark tests for authz use-cases +// The public (non-test) APIs are meant to be used as helpers for +// other tests to build off of. +package authz + +import ( + v1 "github.com/open-policy-agent/opa/v1/test/authz" +) + +// Policy is a test rego policy for a token based authz system +const Policy = v1.Policy + +// AllowQuery is the test query that goes with the Policy +// defined in this package +const AllowQuery = v1.AllowQuery + +// DataSetProfile defines how the test data should be generated +type DataSetProfile = v1.DataSetProfile + +// InputMode defines what type of inputs to generate for testings +type InputMode = v1.InputMode + +// InputMode types supported by GenerateInput +const ( + ForbidIdentity = v1.ForbidIdentity + ForbidPath = v1.ForbidPath + ForbidMethod = v1.ForbidMethod + Allow = v1.Allow +) + +// GenerateInput will use a dataset profile and desired InputMode to generate inputs for testing +func GenerateInput(profile DataSetProfile, mode InputMode) (interface{}, interface{}) { + return v1.GenerateInput(profile, mode) +} + +// GenerateDataset will generate a dataset for the given DatasetProfile +func GenerateDataset(profile DataSetProfile) map[string]interface{} { + return v1.GenerateDataset(profile) +} diff --git a/test/cases/cases.go b/test/cases/cases.go new file mode 100644 index 0000000000..516db66e86 --- /dev/null +++ b/test/cases/cases.go @@ -0,0 +1,26 @@ +// 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 cases contains utilities for evaluation test cases. +package cases + +import ( + v1 "github.com/open-policy-agent/opa/v1/test/cases" +) + +// Set represents a collection of test cases. +type Set = v1.Set + +// TestCase represents a single test case. +type TestCase = v1.TestCase + +// Load returns a set of built-in test cases. +func Load(path string) (Set, error) { + return v1.Load(path) +} + +// MustLoad returns a set of built-in test cases or panics if an error occurs. +func MustLoad(path string) Set { + return v1.MustLoad(path) +} diff --git a/test/cases/doc.go b/test/cases/doc.go new file mode 100644 index 0000000000..170d8ecaf3 --- /dev/null +++ b/test/cases/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package cases diff --git a/test/e2e/doc.go b/test/e2e/doc.go new file mode 100644 index 0000000000..3d936695ac --- /dev/null +++ b/test/e2e/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package e2e diff --git a/test/e2e/logs/doc.go b/test/e2e/logs/doc.go new file mode 100644 index 0000000000..947df36013 --- /dev/null +++ b/test/e2e/logs/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package logs diff --git a/test/e2e/logs/utils.go b/test/e2e/logs/utils.go new file mode 100644 index 0000000000..8f28a5e140 --- /dev/null +++ b/test/e2e/logs/utils.go @@ -0,0 +1,16 @@ +package logs + +import ( + v1 "github.com/open-policy-agent/opa/v1/test/e2e/logs" +) + +// GeneratePolicy generates a policy for use in Decision Log e2e tests. The +// `ruleCounts` determine how many total rules to generate, and the `ruleHits` +// are the number of them that will be evaluated. This is keyed off of +// the `input.hit` boolean value. +func GeneratePolicy(ruleCounts int, ruleHits int) string { + return v1.GeneratePolicy(ruleCounts, ruleHits) +} + +// TestLogServer implements the decision log endpoint for e2e testing. +type TestLogServer = v1.TestLogServer diff --git a/test/e2e/testing.go b/test/e2e/testing.go new file mode 100644 index 0000000000..913208a3c8 --- /dev/null +++ b/test/e2e/testing.go @@ -0,0 +1,49 @@ +// 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 e2e + +import ( + "context" + "testing" + + "github.com/open-policy-agent/opa/v1/runtime" + v1 "github.com/open-policy-agent/opa/v1/test/e2e" +) + +// NewAPIServerTestParams creates a new set of runtime.Params with enough +// default values filled in to start the server. Options can/should +// be customized for the test case. +func NewAPIServerTestParams() runtime.Params { + return v1.NewAPIServerTestParams() +} + +// TestRuntime holds metadata and provides helper methods +// to interact with the runtime being tested. +type TestRuntime = v1.TestRuntime + +// NewTestRuntime returns a new TestRuntime. +func NewTestRuntime(params runtime.Params) (*TestRuntime, error) { + return v1.NewTestRuntime(params) +} + +// NewTestRuntimeWithOpts returns a new TestRuntime. +func NewTestRuntimeWithOpts(opts TestRuntimeOpts, params runtime.Params) (*TestRuntime, error) { + return v1.NewTestRuntimeWithOpts(opts, params) +} + +// WrapRuntime creates a new TestRuntime by wrapping an existing runtime +func WrapRuntime(ctx context.Context, cancel context.CancelFunc, rt *runtime.Runtime) *TestRuntime { + return v1.WrapRuntime(ctx, cancel, rt) +} + +// TestRuntimeOpts contains parameters for the test runtime. +type TestRuntimeOpts = v1.TestRuntimeOpts + +// WithRuntime invokes f with a new TestRuntime after waiting for server +// readiness. This function can be called inside of each test that requires a +// runtime as opposed to RunTests which can only be called once. +func WithRuntime(t *testing.T, opts TestRuntimeOpts, params runtime.Params, f func(rt *TestRuntime)) { + v1.WithRuntime(t, opts, params, f) +} diff --git a/tester/doc.go b/tester/doc.go new file mode 100644 index 0000000000..f7abf25d1e --- /dev/null +++ b/tester/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package tester diff --git a/tester/reporter.go b/tester/reporter.go new file mode 100644 index 0000000000..3779fc9b2d --- /dev/null +++ b/tester/reporter.go @@ -0,0 +1,21 @@ +// Copyright 2017 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 tester + +import ( + v1 "github.com/open-policy-agent/opa/v1/tester" +) + +// Reporter defines the interface for reporting test results. +type Reporter = v1.Reporter + +// PrettyReporter reports test results in a simple human readable format. +type PrettyReporter = v1.PrettyReporter + +// JSONReporter reports test results as array of JSON objects. +type JSONReporter = v1.JSONReporter + +// JSONCoverageReporter reports coverage as a JSON structure. +type JSONCoverageReporter = v1.JSONCoverageReporter diff --git a/tester/runner.go b/tester/runner.go new file mode 100644 index 0000000000..d84681bd22 --- /dev/null +++ b/tester/runner.go @@ -0,0 +1,71 @@ +// Copyright 2017 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 tester contains utilities for executing Rego tests. +package tester + +import ( + "context" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/bundle" + "github.com/open-policy-agent/opa/loader" + "github.com/open-policy-agent/opa/storage" + v1 "github.com/open-policy-agent/opa/v1/tester" +) + +// TestPrefix declares the prefix for all test rules. +const TestPrefix = v1.TestPrefix + +// SkipTestPrefix declares the prefix for tests that should be skipped. +const SkipTestPrefix = v1.SkipTestPrefix + +// Run executes all test cases found under files in path. +func Run(ctx context.Context, paths ...string) ([]*Result, error) { + return v1.Run(ctx, paths...) +} + +// RunWithFilter executes all test cases found under files in path. The filter +// will be applied to exclude files that should not be included. +func RunWithFilter(ctx context.Context, _ loader.Filter, paths ...string) ([]*Result, error) { + return v1.Run(ctx, paths...) +} + +// Result represents a single test case result. +type Result = v1.Result + +// BenchmarkOptions defines options specific to benchmarking tests +type BenchmarkOptions = v1.BenchmarkOptions + +// Runner implements simple test discovery and execution. +type Runner = v1.Runner + +// NewRunner returns a new runner. +func NewRunner() *Runner { + return v1.NewRunner().SetDefaultRegoVersion(ast.DefaultRegoVersion) +} + +type Builtin = v1.Builtin + +// Load returns modules and an in-memory store for running tests. +func Load(args []string, filter loader.Filter) (map[string]*ast.Module, storage.Store, error) { + return LoadWithRegoVersion(args, filter, ast.DefaultRegoVersion) +} + +// LoadWithRegoVersion returns modules and an in-memory store for running tests. +// Modules are parsed in accordance with the given RegoVersion. +func LoadWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*ast.Module, storage.Store, error) { + return v1.LoadWithRegoVersion(args, filter, regoVersion) +} + +// LoadBundles will load the given args as bundles, either tarball or directory is OK. +func LoadBundles(args []string, filter loader.Filter) (map[string]*bundle.Bundle, error) { + return LoadBundlesWithRegoVersion(args, filter, ast.DefaultRegoVersion) +} + +// LoadBundlesWithRegoVersion will load the given args as bundles, either tarball or directory is OK. +// Bundles are parsed in accordance with the given RegoVersion. +func LoadBundlesWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*bundle.Bundle, error) { + return v1.LoadBundlesWithRegoVersion(args, filter, regoVersion) +} diff --git a/tester/runner_test.go b/tester/runner_test.go new file mode 100644 index 0000000000..0cdb58f8ce --- /dev/null +++ b/tester/runner_test.go @@ -0,0 +1,171 @@ +// Copyright 2024 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 tester + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/storage" + "github.com/open-policy-agent/opa/storage/inmem" + "github.com/open-policy-agent/opa/util/test" +) + +func TestLoad_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0 module", // default rego-version + module: `package test + +p[x] { + x = "a" +} + +test_p { + p["a"] +}`, + }, + { + note: "import rego.v1", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +} + +test_p if { + "a" in p +}`, + }, + { + note: "v1 module", // NOT default rego-version + module: `package test + +p contains x if { + x := "a" +} + +test_p if { + "a" in p +}`, + expErrs: []string{ + "test.rego:8: rego_parse_error: unexpected identifier token", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + test.WithTempFS(files, func(root string) { + modules, store, err := Load([]string{root}, nil) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if modules == nil { + t.Fatalf("Expected modules to be non-nil") + } + + if store == nil { + t.Fatalf("Expected store to be non-nil") + } + } + }) + }) + } +} + +// TestRun_DefaultRegoVersion asserts that the internal compiler instantiated by the runner has the correct default rego-version. +func TestRun_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module ast.Module + }{ + { + note: "no v1 violations", + module: ast.Module{ + Package: ast.MustParsePackage(`package test`), + Rules: []*ast.Rule{ + ast.MustParseRule(`p[x] { x = "a" }`), + ast.MustParseRule(`test_p { p["a"] }`), + }, + }, + }, + { + note: "v1 violations", + module: ast.Module{ + Package: ast.MustParsePackage(`package test`), + Imports: ast.MustParseImports(` + import data.foo + import data.bar as foo + `), + Rules: []*ast.Rule{ + ast.MustParseRule(`p[x] { x = "a" }`), + ast.MustParseRule(`test_p { p["a"] }`), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + ctx := context.Background() + + modules := map[string]*ast.Module{ + "test": &tc.module, + } + + store := inmem.New() + txn := storage.NewTransactionOrDie(ctx, store) + defer store.Abort(ctx, txn) + + runner := NewRunner(). + SetStore(store). + SetModules(modules). + SetTimeout(10 * time.Second) + + ch, err := runner.RunTests(ctx, txn) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + var rs []*Result + for r := range ch { + rs = append(rs, r) + } + + if len(rs) != 1 { + t.Fatalf("Expected exactly one result but got: %v", rs) + } + + if rs[0].Fail { + t.Fatalf("Expected test to pass but it failed") + } + }) + } +} diff --git a/topdown/builtins.go b/topdown/builtins.go new file mode 100644 index 0000000000..f28c6c795d --- /dev/null +++ b/topdown/builtins.go @@ -0,0 +1,67 @@ +// Copyright 2016 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 topdown + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +type ( + // Deprecated: Functional-style builtins are deprecated. Use BuiltinFunc instead. + FunctionalBuiltin1 = v1.FunctionalBuiltin1 //nolint:staticcheck // SA1019: Intentional use of deprecated type. + + // Deprecated: Functional-style builtins are deprecated. Use BuiltinFunc instead. + FunctionalBuiltin2 = v1.FunctionalBuiltin2 //nolint:staticcheck // SA1019: Intentional use of deprecated type. + + // Deprecated: Functional-style builtins are deprecated. Use BuiltinFunc instead. + FunctionalBuiltin3 = v1.FunctionalBuiltin3 //nolint:staticcheck // SA1019: Intentional use of deprecated type. + + // Deprecated: Functional-style builtins are deprecated. Use BuiltinFunc instead. + FunctionalBuiltin4 = v1.FunctionalBuiltin4 //nolint:staticcheck // SA1019: Intentional use of deprecated type. + + // BuiltinContext contains context from the evaluator that may be used by + // built-in functions. + BuiltinContext = v1.BuiltinContext + + // BuiltinFunc defines an interface for implementing built-in functions. + // The built-in function is called with the plugged operands from the call + // (including the output operands.) The implementation should evaluate the + // operands and invoke the iterator for each successful/defined output + // value. + BuiltinFunc = v1.BuiltinFunc +) + +// RegisterBuiltinFunc adds a new built-in function to the evaluation engine. +func RegisterBuiltinFunc(name string, f BuiltinFunc) { + v1.RegisterBuiltinFunc(name, f) +} + +// Deprecated: Functional-style builtins are deprecated. Use RegisterBuiltinFunc instead. +func RegisterFunctionalBuiltin1(name string, fun FunctionalBuiltin1) { + v1.RegisterFunctionalBuiltin1(name, fun) +} + +// Deprecated: Functional-style builtins are deprecated. Use RegisterBuiltinFunc instead. +func RegisterFunctionalBuiltin2(name string, fun FunctionalBuiltin2) { + v1.RegisterFunctionalBuiltin2(name, fun) +} + +// Deprecated: Functional-style builtins are deprecated. Use RegisterBuiltinFunc instead. +func RegisterFunctionalBuiltin3(name string, fun FunctionalBuiltin3) { + v1.RegisterFunctionalBuiltin3(name, fun) +} + +// Deprecated: Functional-style builtins are deprecated. Use RegisterBuiltinFunc instead. +func RegisterFunctionalBuiltin4(name string, fun FunctionalBuiltin4) { + v1.RegisterFunctionalBuiltin4(name, fun) +} + +// GetBuiltin returns a built-in function implementation, nil if no built-in found. +func GetBuiltin(name string) BuiltinFunc { + return v1.GetBuiltin(name) +} + +// Deprecated: The BuiltinEmpty type is no longer needed. Use nil return values instead. +type BuiltinEmpty = v1.Builtin diff --git a/topdown/builtins/builtins.go b/topdown/builtins/builtins.go new file mode 100644 index 0000000000..152c37717a --- /dev/null +++ b/topdown/builtins/builtins.go @@ -0,0 +1,123 @@ +// Copyright 2016 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 builtins contains utilities for implementing built-in functions. +package builtins + +import ( + "math/big" + + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/topdown/builtins" +) + +// Cache defines the built-in cache used by the top-down evaluation. The keys +// must be comparable and should not be of type string. +type Cache = v1.Cache + +// We use an ast.Object for the cached keys/values because a naive +// map[ast.Value]ast.Value will not correctly detect value equality of +// the member keys. +type NDBCache = v1.NDBCache + +// ErrOperand represents an invalid operand has been passed to a built-in +// function. Built-ins should return ErrOperand to indicate a type error has +// occurred. +type ErrOperand = v1.ErrOperand + +// NewOperandErr returns a generic operand error. +func NewOperandErr(pos int, f string, a ...interface{}) error { + return v1.NewOperandErr(pos, f, a...) +} + +// NewOperandTypeErr returns an operand error indicating the operand's type was wrong. +func NewOperandTypeErr(pos int, got ast.Value, expected ...string) error { + return v1.NewOperandTypeErr(pos, got, expected...) +} + +// NewOperandElementErr returns an operand error indicating an element in the +// composite operand was wrong. +func NewOperandElementErr(pos int, composite ast.Value, got ast.Value, expected ...string) error { + return v1.NewOperandElementErr(pos, composite, got, expected...) +} + +// NewOperandEnumErr returns an operand error indicating a value was wrong. +func NewOperandEnumErr(pos int, expected ...string) error { + return v1.NewOperandEnumErr(pos, expected...) +} + +// IntOperand converts x to an int. If the cast fails, a descriptive error is +// returned. +func IntOperand(x ast.Value, pos int) (int, error) { + return v1.IntOperand(x, pos) +} + +// BigIntOperand converts x to a big int. If the cast fails, a descriptive error +// is returned. +func BigIntOperand(x ast.Value, pos int) (*big.Int, error) { + return v1.BigIntOperand(x, pos) +} + +// NumberOperand converts x to a number. If the cast fails, a descriptive error is +// returned. +func NumberOperand(x ast.Value, pos int) (ast.Number, error) { + return v1.NumberOperand(x, pos) +} + +// SetOperand converts x to a set. If the cast fails, a descriptive error is +// returned. +func SetOperand(x ast.Value, pos int) (ast.Set, error) { + return v1.SetOperand(x, pos) +} + +// StringOperand converts x to a string. If the cast fails, a descriptive error is +// returned. +func StringOperand(x ast.Value, pos int) (ast.String, error) { + return v1.StringOperand(x, pos) +} + +// ObjectOperand converts x to an object. If the cast fails, a descriptive +// error is returned. +func ObjectOperand(x ast.Value, pos int) (ast.Object, error) { + return v1.ObjectOperand(x, pos) +} + +// ArrayOperand converts x to an array. If the cast fails, a descriptive +// error is returned. +func ArrayOperand(x ast.Value, pos int) (*ast.Array, error) { + return v1.ArrayOperand(x, pos) +} + +// NumberToFloat converts n to a big float. +func NumberToFloat(n ast.Number) *big.Float { + return v1.NumberToFloat(n) +} + +// FloatToNumber converts f to a number. +func FloatToNumber(f *big.Float) ast.Number { + return v1.FloatToNumber(f) +} + +// NumberToInt converts n to a big int. +// If n cannot be converted to an big int, an error is returned. +func NumberToInt(n ast.Number) (*big.Int, error) { + return v1.NumberToInt(n) +} + +// IntToNumber converts i to a number. +func IntToNumber(i *big.Int) ast.Number { + return v1.IntToNumber(i) +} + +// StringSliceOperand converts x to a []string. If the cast fails, a descriptive error is +// returned. +func StringSliceOperand(a ast.Value, pos int) ([]string, error) { + return v1.StringSliceOperand(a, pos) +} + +// RuneSliceOperand converts x to a []rune. If the cast fails, a descriptive error is +// returned. +func RuneSliceOperand(x ast.Value, pos int) ([]rune, error) { + return v1.RuneSliceOperand(x, pos) +} diff --git a/topdown/builtins/doc.go b/topdown/builtins/doc.go new file mode 100644 index 0000000000..1eec536949 --- /dev/null +++ b/topdown/builtins/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package builtins diff --git a/topdown/cache.go b/topdown/cache.go new file mode 100644 index 0000000000..bb39df03e0 --- /dev/null +++ b/topdown/cache.go @@ -0,0 +1,19 @@ +// Copyright 2017 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 topdown + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// VirtualCache defines the interface for a cache that stores the results of +// evaluated virtual documents (rules). +// The cache is a stack of frames, where each frame is a mapping from references +// to values. +type VirtualCache = v1.VirtualCache + +func NewVirtualCache() VirtualCache { + return v1.NewVirtualCache() +} diff --git a/topdown/cache/cache.go b/topdown/cache/cache.go new file mode 100644 index 0000000000..e95617b228 --- /dev/null +++ b/topdown/cache/cache.go @@ -0,0 +1,64 @@ +// 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 cache defines the inter-query cache interface that can cache data across queries +package cache + +import ( + "context" + + v1 "github.com/open-policy-agent/opa/v1/topdown/cache" +) + +// Config represents the configuration for the inter-query builtin cache. +type Config = v1.Config + +// InterQueryBuiltinValueCacheConfig represents the configuration of the inter-query value cache that built-in functions can utilize. +// MaxNumEntries - max number of cache entries +type InterQueryBuiltinValueCacheConfig = v1.InterQueryBuiltinValueCacheConfig + +// InterQueryBuiltinCacheConfig represents the configuration of the inter-query cache that built-in functions can utilize. +// MaxSizeBytes - max capacity of cache in bytes +// ForcedEvictionThresholdPercentage - capacity usage in percentage after which forced FIFO eviction starts +// StaleEntryEvictionPeriodSeconds - time period between end of previous and start of new stale entry eviction routine +type InterQueryBuiltinCacheConfig = v1.InterQueryBuiltinCacheConfig + +// ParseCachingConfig returns the config for the inter-query cache. +func ParseCachingConfig(raw []byte) (*Config, error) { + return v1.ParseCachingConfig(raw) +} + +// InterQueryCacheValue defines the interface for the data that the inter-query cache holds. +type InterQueryCacheValue = v1.InterQueryCacheValue + +// InterQueryCache defines the interface for the inter-query cache. +type InterQueryCache = v1.InterQueryCache + +// NewInterQueryCache returns a new inter-query cache. +// The cache uses a FIFO eviction policy when it reaches the forced eviction threshold. +// Parameters: +// +// config - to configure the InterQueryCache +func NewInterQueryCache(config *Config) InterQueryCache { + return v1.NewInterQueryCache(config) +} + +// NewInterQueryCacheWithContext returns a new inter-query cache with context. +// The cache uses a combination of FIFO eviction policy when it reaches the forced eviction threshold +// and a periodic cleanup routine to remove stale entries that exceed their expiration time, if specified. +// If configured with a zero stale_entry_eviction_period_seconds value, the stale entry cleanup routine is disabled. +// +// Parameters: +// +// ctx - used to control lifecycle of the stale entry cleanup routine +// config - to configure the InterQueryCache +func NewInterQueryCacheWithContext(ctx context.Context, config *Config) InterQueryCache { + return v1.NewInterQueryCacheWithContext(ctx, config) +} + +type InterQueryValueCache = v1.InterQueryValueCache + +func NewInterQueryValueCache(ctx context.Context, config *Config) InterQueryValueCache { + return v1.NewInterQueryValueCache(ctx, config) +} diff --git a/topdown/cache/doc.go b/topdown/cache/doc.go new file mode 100644 index 0000000000..640530c081 --- /dev/null +++ b/topdown/cache/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package cache diff --git a/topdown/cancel.go b/topdown/cancel.go new file mode 100644 index 0000000000..395a14a80d --- /dev/null +++ b/topdown/cancel.go @@ -0,0 +1,18 @@ +// Copyright 2017 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 topdown + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// Cancel defines the interface for cancelling topdown queries. Cancel +// operations are thread-safe and idempotent. +type Cancel = v1.Cancel + +// NewCancel returns a new Cancel object. +func NewCancel() Cancel { + return v1.NewCancel() +} diff --git a/topdown/copypropagation/copypropagation.go b/topdown/copypropagation/copypropagation.go new file mode 100644 index 0000000000..0f26ded0d4 --- /dev/null +++ b/topdown/copypropagation/copypropagation.go @@ -0,0 +1,34 @@ +// 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 copypropagation + +import ( + "github.com/open-policy-agent/opa/ast" + v1 "github.com/open-policy-agent/opa/v1/topdown/copypropagation" +) + +// CopyPropagator implements a simple copy propagation optimization to remove +// intermediate variables in partial evaluation results. +// +// For example, given the query: input.x > 1 where 'input' is unknown, the +// compiled query would become input.x = a; a > 1 which would remain in the +// partial evaluation result. The CopyPropagator will remove the variable +// assignment so that partial evaluation simply outputs input.x > 1. +// +// In many cases, copy propagation can remove all variables from the result of +// partial evaluation which simplifies evaluation for non-OPA consumers. +// +// In some cases, copy propagation cannot remove all variables. If the output of +// a built-in call is subsequently used as a ref head, the output variable must +// be kept. For example. sort(input, x); x[0] == 1. In this case, copy +// propagation cannot replace x[0] == 1 with sort(input, x)[0] == 1 as this is +// not legal. +type CopyPropagator = v1.CopyPropagator + +// New returns a new CopyPropagator that optimizes queries while preserving vars +// in the livevars set. +func New(livevars ast.VarSet) *CopyPropagator { + return v1.New(livevars) +} diff --git a/topdown/copypropagation/doc.go b/topdown/copypropagation/doc.go new file mode 100644 index 0000000000..238ced31ac --- /dev/null +++ b/topdown/copypropagation/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package copypropagation diff --git a/topdown/doc.go b/topdown/doc.go new file mode 100644 index 0000000000..a303ef7886 --- /dev/null +++ b/topdown/doc.go @@ -0,0 +1,14 @@ +// Copyright 2016 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 topdown provides low-level query evaluation support. +// +// The topdown implementation is a modified version of the standard top-down +// evaluation algorithm used in Datalog. References and comprehensions are +// evaluated eagerly while all other terms are evaluated lazily. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package topdown diff --git a/topdown/errors.go b/topdown/errors.go new file mode 100644 index 0000000000..47853ec6d1 --- /dev/null +++ b/topdown/errors.go @@ -0,0 +1,54 @@ +// Copyright 2017 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 topdown + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// Halt is a special error type that built-in function implementations return to indicate +// that policy evaluation should stop immediately. +type Halt = v1.Halt + +// Error is the error type returned by the Eval and Query functions when +// an evaluation error occurs. +type Error = v1.Error + +const ( + + // InternalErr represents an unknown evaluation error. + InternalErr = v1.InternalErr + + // CancelErr indicates the evaluation process was cancelled. + CancelErr = v1.CancelErr + + // ConflictErr indicates a conflict was encountered during evaluation. For + // instance, a conflict occurs if a rule produces multiple, differing values + // for the same key in an object. Conflict errors indicate the policy does + // not account for the data loaded into the policy engine. + ConflictErr = v1.ConflictErr + + // TypeErr indicates evaluation stopped because an expression was applied to + // a value of an inappropriate type. + TypeErr = v1.TypeErr + + // BuiltinErr indicates a built-in function received a semantically invalid + // input or encountered some kind of runtime error, e.g., connection + // timeout, connection refused, etc. + BuiltinErr = v1.BuiltinErr + + // WithMergeErr indicates that the real and replacement data could not be merged. + WithMergeErr = v1.WithMergeErr +) + +// IsError returns true if the err is an Error. +func IsError(err error) bool { + return v1.IsError(err) +} + +// IsCancel returns true if err was caused by cancellation. +func IsCancel(err error) bool { + return v1.IsCancel(err) +} diff --git a/topdown/graphql.go b/topdown/graphql.go new file mode 100644 index 0000000000..0d6ebda0a8 --- /dev/null +++ b/topdown/graphql.go @@ -0,0 +1,485 @@ +// Copyright 2022 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 topdown + +import ( + "encoding/json" + "fmt" + "strings" + + gqlast "github.com/open-policy-agent/opa/internal/gqlparser/ast" + gqlparser "github.com/open-policy-agent/opa/internal/gqlparser/parser" + gqlvalidator "github.com/open-policy-agent/opa/internal/gqlparser/validator" + + // Side-effecting import. Triggers GraphQL library's validation rule init() functions. + _ "github.com/open-policy-agent/opa/internal/gqlparser/validator/rules" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/topdown/builtins" +) + +// Parses a GraphQL schema, and returns the GraphQL AST for the schema. +func parseSchema(schema string) (*gqlast.SchemaDocument, error) { + // NOTE(philipc): We don't include the "built-in schema defs" from the + // underlying graphql parsing library here, because those definitions + // generate enormous AST blobs. In the future, if there is demand for + // a "full-spec" version of schema ASTs, we may need to provide a + // version of this function that includes the built-in schema + // definitions. + schemaAST, err := gqlparser.ParseSchema(&gqlast.Source{Input: schema}) + if err != nil { + errorParts := strings.SplitN(err.Error(), ":", 4) + msg := strings.TrimLeft(errorParts[3], " ") + return nil, fmt.Errorf("%s in GraphQL string at location %s:%s", msg, errorParts[1], errorParts[2]) + } + return schemaAST, nil +} + +// Parses a GraphQL query, and returns the GraphQL AST for the query. +func parseQuery(query string) (*gqlast.QueryDocument, error) { + queryAST, err := gqlparser.ParseQuery(&gqlast.Source{Input: query}) + if err != nil { + errorParts := strings.SplitN(err.Error(), ":", 4) + msg := strings.TrimLeft(errorParts[3], " ") + return nil, fmt.Errorf("%s in GraphQL string at location %s:%s", msg, errorParts[1], errorParts[2]) + } + return queryAST, nil +} + +// Validates a GraphQL query against a schema, and returns an error. +// In this case, we get a wrappered error list type, and pluck out +// just the first error message in the list. +func validateQuery(schema *gqlast.Schema, query *gqlast.QueryDocument) error { + // Validate the query against the schema, erroring if there's an issue. + err := gqlvalidator.Validate(schema, query) + if err != nil { + // We use strings.TrimSuffix to remove the '.' characters that the library + // authors include on most of their validation errors. This should be safe, + // since variable names in their error messages are usually quoted, and + // this affects only the last character(s) in the string. + // NOTE(philipc): We know the error location will be in the query string, + // because schema validation always happens before this function is called. + errorParts := strings.SplitN(err.Error(), ":", 4) + msg := strings.TrimSuffix(strings.TrimLeft(errorParts[3], " "), ".\n") + return fmt.Errorf("%s in GraphQL query string at location %s:%s", msg, errorParts[1], errorParts[2]) + } + return nil +} + +func getBuiltinSchema() *gqlast.SchemaDocument { + schema, err := gqlparser.ParseSchema(gqlvalidator.Prelude) + if err != nil { + panic(fmt.Errorf("Error in gqlparser Prelude (should be impossible): %w", err)) + } + return schema +} + +// NOTE(philipc): This function expects *validated* schema documents, and will break +// if it is fed arbitrary structures. +func mergeSchemaDocuments(docA *gqlast.SchemaDocument, docB *gqlast.SchemaDocument) *gqlast.SchemaDocument { + ast := &gqlast.SchemaDocument{} + ast.Merge(docA) + ast.Merge(docB) + return ast +} + +// Converts a SchemaDocument into a gqlast.Schema object that can be used for validation. +// It merges in the builtin schema typedefs exactly as gqltop.LoadSchema did internally. +func convertSchema(schemaDoc *gqlast.SchemaDocument) (*gqlast.Schema, error) { + // Merge builtin schema + schema we were provided. + builtinsSchemaDoc := getBuiltinSchema() + mergedSchemaDoc := mergeSchemaDocuments(builtinsSchemaDoc, schemaDoc) + schema, err := gqlvalidator.ValidateSchemaDocument(mergedSchemaDoc) + if err != nil { + return nil, fmt.Errorf("Error in gqlparser SchemaDocument to Schema conversion: %w", err) + } + return schema, nil +} + +// Converts an ast.Object into a gqlast.QueryDocument object. +func objectToQueryDocument(value ast.Object) (*gqlast.QueryDocument, error) { + // Convert ast.Term to interface{} for JSON encoding below. + asJSON, err := ast.JSON(value) + if err != nil { + return nil, err + } + // Marshal to JSON. + bs, err := json.Marshal(asJSON) + if err != nil { + return nil, err + } + // Unmarshal from JSON -> gqlast.QueryDocument. + var result gqlast.QueryDocument + err = json.Unmarshal(bs, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +// Converts an ast.Object into a gqlast.SchemaDocument object. +func objectToSchemaDocument(value ast.Object) (*gqlast.SchemaDocument, error) { + // Convert ast.Term to interface{} for JSON encoding below. + asJSON, err := ast.JSON(value) + if err != nil { + return nil, err + } + // Marshal to JSON. + bs, err := json.Marshal(asJSON) + if err != nil { + return nil, err + } + // Unmarshal from JSON -> gqlast.SchemaDocument. + var result gqlast.SchemaDocument + err = json.Unmarshal(bs, &result) + if err != nil { + return nil, err + } + return &result, nil +} + +// Recursively traverses an AST that has been run through InterfaceToValue, +// and prunes away the fields with null or empty values, and all `Position` +// structs. +// NOTE(philipc): We currently prune away null values to reduce the level +// of clutter in the returned AST objects. In the future, if there is demand +// for ASTs that have a more regular/fixed structure, we may need to provide +// a "raw" version of the AST, where we still prune away the `Position` +// structs, but leave in the null fields. +func pruneIrrelevantGraphQLASTNodes(value ast.Value) ast.Value { + // We iterate over the Value we've been provided, and recurse down + // in the case of complex types, such as Arrays/Objects. + // We are guaranteed to only have to deal with standard JSON types, + // so this is much less ugly than what we'd need for supporting every + // extant ast type! + switch x := value.(type) { + case *ast.Array: + result := ast.NewArray() + // Iterate over the array's elements, and do the following: + // - Drop any Nulls + // - Drop any any empty object/array value (after running the pruner) + for i := 0; i < x.Len(); i++ { + vTerm := x.Elem(i) + switch v := vTerm.Value.(type) { + case ast.Null: + continue + case *ast.Array: + // Safe, because we knew the type before going to prune it. + va := pruneIrrelevantGraphQLASTNodes(v).(*ast.Array) + if va.Len() > 0 { + result = result.Append(ast.NewTerm(va)) + } + case ast.Object: + // Safe, because we knew the type before going to prune it. + vo := pruneIrrelevantGraphQLASTNodes(v).(ast.Object) + if len(vo.Keys()) > 0 { + result = result.Append(ast.NewTerm(vo)) + } + default: + result = result.Append(vTerm) + } + } + return result + case ast.Object: + result := ast.NewObject() + // Iterate over our object's keys, and do the following: + // - Drop "Position". + // - Drop any key with a Null value. + // - Drop any key with an empty object/array value (after running the pruner) + keys := x.Keys() + for _, k := range keys { + // We drop the "Position" objects because we don't need the + // source-backref/location info they provide for policy rules. + // Note that keys are ast.Strings. + if ast.String("Position").Equal(k.Value) { + continue + } + vTerm := x.Get(k) + switch v := vTerm.Value.(type) { + case ast.Null: + continue + case *ast.Array: + // Safe, because we knew the type before going to prune it. + va := pruneIrrelevantGraphQLASTNodes(v).(*ast.Array) + if va.Len() > 0 { + result.Insert(k, ast.NewTerm(va)) + } + case ast.Object: + // Safe, because we knew the type before going to prune it. + vo := pruneIrrelevantGraphQLASTNodes(v).(ast.Object) + if len(vo.Keys()) > 0 { + result.Insert(k, ast.NewTerm(vo)) + } + default: + result.Insert(k, vTerm) + } + } + return result + default: + return x + } +} + +// Reports errors from parsing/validation. +func builtinGraphQLParse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var queryDoc *gqlast.QueryDocument + var schemaDoc *gqlast.SchemaDocument + var err error + + // Parse/translate query if it's a string/object. + switch x := operands[0].Value.(type) { + case ast.String: + queryDoc, err = parseQuery(string(x)) + case ast.Object: + queryDoc, err = objectToQueryDocument(x) + default: + // Error if wrong type. + return builtins.NewOperandTypeErr(0, x, "string", "object") + } + if err != nil { + return err + } + + // Parse/translate schema if it's a string/object. + switch x := operands[1].Value.(type) { + case ast.String: + schemaDoc, err = parseSchema(string(x)) + case ast.Object: + schemaDoc, err = objectToSchemaDocument(x) + default: + // Error if wrong type. + return builtins.NewOperandTypeErr(1, x, "string", "object") + } + if err != nil { + return err + } + + // Transform the ASTs into Objects. + queryASTValue, err := ast.InterfaceToValue(queryDoc) + if err != nil { + return err + } + schemaASTValue, err := ast.InterfaceToValue(schemaDoc) + if err != nil { + return err + } + + // Validate the query against the schema, erroring if there's an issue. + schema, err := convertSchema(schemaDoc) + if err != nil { + return err + } + if err := validateQuery(schema, queryDoc); err != nil { + return err + } + + // Recursively remove irrelevant AST structures. + queryResult := pruneIrrelevantGraphQLASTNodes(queryASTValue.(ast.Object)) + querySchema := pruneIrrelevantGraphQLASTNodes(schemaASTValue.(ast.Object)) + + // Construct return value. + verified := ast.ArrayTerm( + ast.NewTerm(queryResult), + ast.NewTerm(querySchema), + ) + + return iter(verified) +} + +// Returns default value when errors occur. +func builtinGraphQLParseAndVerify(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var queryDoc *gqlast.QueryDocument + var schemaDoc *gqlast.SchemaDocument + var err error + + unverified := ast.ArrayTerm( + ast.InternedBooleanTerm(false), + ast.NewTerm(ast.NewObject()), + ast.NewTerm(ast.NewObject()), + ) + + // Parse/translate query if it's a string/object. + switch x := operands[0].Value.(type) { + case ast.String: + queryDoc, err = parseQuery(string(x)) + case ast.Object: + queryDoc, err = objectToQueryDocument(x) + default: + // Error if wrong type. + return iter(unverified) + } + if err != nil { + return iter(unverified) + } + + // Parse/translate schema if it's a string/object. + switch x := operands[1].Value.(type) { + case ast.String: + schemaDoc, err = parseSchema(string(x)) + case ast.Object: + schemaDoc, err = objectToSchemaDocument(x) + default: + // Error if wrong type. + return iter(unverified) + } + if err != nil { + return iter(unverified) + } + + // Transform the ASTs into Objects. + queryASTValue, err := ast.InterfaceToValue(queryDoc) + if err != nil { + return iter(unverified) + } + schemaASTValue, err := ast.InterfaceToValue(schemaDoc) + if err != nil { + return iter(unverified) + } + + // Validate the query against the schema, erroring if there's an issue. + schema, err := convertSchema(schemaDoc) + if err != nil { + return iter(unverified) + } + if err := validateQuery(schema, queryDoc); err != nil { + return iter(unverified) + } + + // Recursively remove irrelevant AST structures. + queryResult := pruneIrrelevantGraphQLASTNodes(queryASTValue.(ast.Object)) + querySchema := pruneIrrelevantGraphQLASTNodes(schemaASTValue.(ast.Object)) + + // Construct return value. + verified := ast.ArrayTerm( + ast.InternedBooleanTerm(true), + ast.NewTerm(queryResult), + ast.NewTerm(querySchema), + ) + + return iter(verified) +} + +func builtinGraphQLParseQuery(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + raw, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return err + } + + // Get the highly-nested AST struct, along with any errors generated. + query, err := parseQuery(string(raw)) + if err != nil { + return err + } + + // Transform the AST into an Object. + value, err := ast.InterfaceToValue(query) + if err != nil { + return err + } + + // Recursively remove irrelevant AST structures. + result := pruneIrrelevantGraphQLASTNodes(value.(ast.Object)) + + return iter(ast.NewTerm(result)) +} + +func builtinGraphQLParseSchema(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + raw, err := builtins.StringOperand(operands[0].Value, 1) + if err != nil { + return err + } + + // Get the highly-nested AST struct, along with any errors generated. + schema, err := parseSchema(string(raw)) + if err != nil { + return err + } + + // Transform the AST into an Object. + value, err := ast.InterfaceToValue(schema) + if err != nil { + return err + } + + // Recursively remove irrelevant AST structures. + result := pruneIrrelevantGraphQLASTNodes(value.(ast.Object)) + + return iter(ast.NewTerm(result)) +} + +func builtinGraphQLIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var queryDoc *gqlast.QueryDocument + var schemaDoc *gqlast.SchemaDocument + var err error + + switch x := operands[0].Value.(type) { + case ast.String: + queryDoc, err = parseQuery(string(x)) + case ast.Object: + queryDoc, err = objectToQueryDocument(x) + default: + // Error if wrong type. + return iter(ast.InternedBooleanTerm(false)) + } + if err != nil { + return iter(ast.InternedBooleanTerm(false)) + } + + switch x := operands[1].Value.(type) { + case ast.String: + schemaDoc, err = parseSchema(string(x)) + case ast.Object: + schemaDoc, err = objectToSchemaDocument(x) + default: + // Error if wrong type. + return iter(ast.InternedBooleanTerm(false)) + } + if err != nil { + return iter(ast.InternedBooleanTerm(false)) + } + + // Validate the query against the schema, erroring if there's an issue. + schema, err := convertSchema(schemaDoc) + if err != nil { + return iter(ast.InternedBooleanTerm(false)) + } + if err := validateQuery(schema, queryDoc); err != nil { + return iter(ast.InternedBooleanTerm(false)) + } + + // If we got this far, the GraphQL query passed validation. + return iter(ast.InternedBooleanTerm(true)) +} + +func builtinGraphQLSchemaIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var schemaDoc *gqlast.SchemaDocument + var err error + + switch x := operands[0].Value.(type) { + case ast.String: + schemaDoc, err = parseSchema(string(x)) + case ast.Object: + schemaDoc, err = objectToSchemaDocument(x) + default: + // Error if wrong type. + return iter(ast.InternedBooleanTerm(false)) + } + if err != nil { + return iter(ast.InternedBooleanTerm(false)) + } + + // Validate the schema, this determines the result + _, err = convertSchema(schemaDoc) + return iter(ast.InternedBooleanTerm(err == nil)) +} + +func init() { + RegisterBuiltinFunc(ast.GraphQLParse.Name, builtinGraphQLParse) + RegisterBuiltinFunc(ast.GraphQLParseAndVerify.Name, builtinGraphQLParseAndVerify) + RegisterBuiltinFunc(ast.GraphQLParseQuery.Name, builtinGraphQLParseQuery) + RegisterBuiltinFunc(ast.GraphQLParseSchema.Name, builtinGraphQLParseSchema) + RegisterBuiltinFunc(ast.GraphQLIsValid.Name, builtinGraphQLIsValid) + RegisterBuiltinFunc(ast.GraphQLSchemaIsValid.Name, builtinGraphQLSchemaIsValid) +} diff --git a/topdown/http.go b/topdown/http.go new file mode 100644 index 0000000000..693ea4048c --- /dev/null +++ b/topdown/http.go @@ -0,0 +1,17 @@ +// 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 topdown + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +const ( + // HTTPSendInternalErr represents a runtime evaluation error. + HTTPSendInternalErr = v1.HTTPSendInternalErr + + // HTTPSendNetworkErr represents a network error. + HTTPSendNetworkErr = v1.HTTPSendNetworkErr +) diff --git a/topdown/instrumentation.go b/topdown/instrumentation.go new file mode 100644 index 0000000000..845f8da612 --- /dev/null +++ b/topdown/instrumentation.go @@ -0,0 +1,21 @@ +// 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 topdown + +import ( + "github.com/open-policy-agent/opa/v1/metrics" + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// Instrumentation implements helper functions to instrument query evaluation +// to diagnose performance issues. Instrumentation may be expensive in some +// cases, so it is disabled by default. +type Instrumentation = v1.Instrumentation + +// NewInstrumentation returns a new Instrumentation object. Performance +// diagnostics recorded on this Instrumentation object will stored in m. +func NewInstrumentation(m metrics.Metrics) *Instrumentation { + return v1.NewInstrumentation(m) +} diff --git a/topdown/lineage/doc.go b/topdown/lineage/doc.go new file mode 100644 index 0000000000..5a463b697d --- /dev/null +++ b/topdown/lineage/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package lineage diff --git a/topdown/lineage/lineage.go b/topdown/lineage/lineage.go new file mode 100644 index 0000000000..160c6a3aff --- /dev/null +++ b/topdown/lineage/lineage.go @@ -0,0 +1,39 @@ +// 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 lineage + +import ( + "github.com/open-policy-agent/opa/topdown" + v1 "github.com/open-policy-agent/opa/v1/topdown/lineage" +) + +// Debug contains everything in the log. +func Debug(trace []*topdown.Event) []*topdown.Event { + return v1.Debug(trace) +} + +// Full returns a filtered trace that contains everything except Unify ops +func Full(trace []*topdown.Event) (result []*topdown.Event) { + return v1.Full(trace) +} + +// Notes returns a filtered trace that contains Note events and context to +// understand where the Note was emitted. +func Notes(trace []*topdown.Event) []*topdown.Event { + return v1.Notes(trace) +} + +// Fails returns a filtered trace that contains Fail events and context to +// understand where the Fail occurred. +func Fails(trace []*topdown.Event) []*topdown.Event { + return v1.Fails(trace) +} + +// Filter will filter a given trace using the specified filter function. The +// filtering function should return true for events that should be kept, false +// for events that should be filtered out. +func Filter(trace []*topdown.Event, filter func(*topdown.Event) bool) (result []*topdown.Event) { + return v1.Filter(trace, filter) +} diff --git a/topdown/print.go b/topdown/print.go new file mode 100644 index 0000000000..5eacd180d9 --- /dev/null +++ b/topdown/print.go @@ -0,0 +1,16 @@ +// Copyright 2021 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 topdown + +import ( + "io" + + "github.com/open-policy-agent/opa/topdown/print" + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +func NewPrintHook(w io.Writer) print.Hook { + return v1.NewPrintHook(w) +} diff --git a/topdown/print/doc.go b/topdown/print/doc.go new file mode 100644 index 0000000000..c2ee0eca7f --- /dev/null +++ b/topdown/print/doc.go @@ -0,0 +1,8 @@ +// Copyright 2016 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package print diff --git a/topdown/print/print.go b/topdown/print/print.go new file mode 100644 index 0000000000..66ffbb176f --- /dev/null +++ b/topdown/print/print.go @@ -0,0 +1,14 @@ +package print + +import ( + v1 "github.com/open-policy-agent/opa/v1/topdown/print" +) + +// Context provides the Hook implementation context about the print() call. +type Context = v1.Context + +// Hook defines the interface that callers can implement to receive print +// statement outputs. If the hook returns an error, it will be surfaced if +// strict builtin error checking is enabled (otherwise, it will not halt +// execution.) +type Hook = v1.Hook diff --git a/topdown/query.go b/topdown/query.go new file mode 100644 index 0000000000..d24060991f --- /dev/null +++ b/topdown/query.go @@ -0,0 +1,24 @@ +package topdown + +import ( + "github.com/open-policy-agent/opa/v1/ast" + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// QueryResultSet represents a collection of results returned by a query. +type QueryResultSet = v1.QueryResultSet + +// QueryResult represents a single result returned by a query. The result +// contains bindings for all variables that appear in the query. +type QueryResult = v1.QueryResult + +// Query provides a configurable interface for performing query evaluation. +type Query = v1.Query + +// Builtin represents a built-in function that queries can call. +type Builtin = v1.Builtin + +// NewQuery returns a new Query object that can be run. +func NewQuery(query ast.Body) *Query { + return v1.NewQuery(query) +} diff --git a/topdown/trace.go b/topdown/trace.go new file mode 100644 index 0000000000..4d4cc295e2 --- /dev/null +++ b/topdown/trace.go @@ -0,0 +1,112 @@ +// Copyright 2016 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 topdown + +import ( + "io" + + v1 "github.com/open-policy-agent/opa/v1/topdown" +) + +// Op defines the types of tracing events. +type Op = v1.Op + +const ( + // EnterOp is emitted when a new query is about to be evaluated. + EnterOp = v1.EnterOp + + // ExitOp is emitted when a query has evaluated to true. + ExitOp = v1.ExitOp + + // EvalOp is emitted when an expression is about to be evaluated. + EvalOp = v1.EvalOp + + // RedoOp is emitted when an expression, rule, or query is being re-evaluated. + RedoOp = v1.RedoOp + + // SaveOp is emitted when an expression is saved instead of evaluated + // during partial evaluation. + SaveOp = v1.SaveOp + + // FailOp is emitted when an expression evaluates to false. + FailOp = v1.FailOp + + // DuplicateOp is emitted when a query has produced a duplicate value. The search + // will stop at the point where the duplicate was emitted and backtrack. + DuplicateOp = v1.DuplicateOp + + // NoteOp is emitted when an expression invokes a tracing built-in function. + NoteOp = v1.NoteOp + + // IndexOp is emitted during an expression evaluation to represent lookup + // matches. + IndexOp = v1.IndexOp + + // WasmOp is emitted when resolving a ref using an external + // Resolver. + WasmOp = v1.WasmOp + + // UnifyOp is emitted when two terms are unified. Node will be set to an + // equality expression with the two terms. This Node will not have location + // info. + UnifyOp = v1.UnifyOp + FailedAssertionOp = v1.FailedAssertionOp +) + +// VarMetadata provides some user facing information about +// a variable in some policy. +type VarMetadata = v1.VarMetadata + +// Event contains state associated with a tracing event. +type Event = v1.Event + +// Tracer defines the interface for tracing in the top-down evaluation engine. +// Deprecated: Use QueryTracer instead. +type Tracer = v1.Tracer + +// QueryTracer defines the interface for tracing in the top-down evaluation engine. +// The implementation can provide additional configuration to modify the tracing +// behavior for query evaluations. +type QueryTracer = v1.QueryTracer + +// TraceConfig defines some common configuration for Tracer implementations +type TraceConfig = v1.TraceConfig + +// WrapLegacyTracer will create a new QueryTracer which wraps an +// older Tracer instance. +func WrapLegacyTracer(tracer Tracer) QueryTracer { + return v1.WrapLegacyTracer(tracer) +} + +// BufferTracer implements the Tracer and QueryTracer interface by +// simply buffering all events received. +type BufferTracer = v1.BufferTracer + +// NewBufferTracer returns a new BufferTracer. +func NewBufferTracer() *BufferTracer { + return v1.NewBufferTracer() +} + +// PrettyTrace pretty prints the trace to the writer. +func PrettyTrace(w io.Writer, trace []*Event) { + v1.PrettyTrace(w, trace) +} + +// PrettyTraceWithLocation prints the trace to the writer and includes location information +func PrettyTraceWithLocation(w io.Writer, trace []*Event) { + v1.PrettyTraceWithLocation(w, trace) +} + +type PrettyTraceOptions = v1.PrettyTraceOptions + +func PrettyTraceWithOpts(w io.Writer, trace []*Event, opts PrettyTraceOptions) { + v1.PrettyTraceWithOpts(w, trace, opts) +} + +type PrettyEventOpts = v1.PrettyEventOpts + +func PrettyEvent(w io.Writer, e *Event, opts PrettyEventOpts) error { + return v1.PrettyEvent(w, e, opts) +} diff --git a/tracing/doc.go b/tracing/doc.go new file mode 100644 index 0000000000..161a3d0cee --- /dev/null +++ b/tracing/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package tracing diff --git a/tracing/tracing.go b/tracing/tracing.go new file mode 100644 index 0000000000..ad6ac668ed --- /dev/null +++ b/tracing/tracing.go @@ -0,0 +1,45 @@ +// Copyright 2021 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 tracing enables dependency-injection at runtime. When used +// together with an underscore-import of `github.com/open-policy-agent/opa/features/tracing`, +// the server and its runtime will emit OpenTelemetry spans to the +// configured sink. +package tracing + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/tracing" +) + +// Options are options for the HTTPTracingService, passed along as-is. +type Options = v1.Options + +// NewOptions is a helper method for constructing `tracing.Options` +func NewOptions(opts ...interface{}) Options { + return v1.NewOptions(opts...) +} + +// HTTPTracingService defines how distributed tracing comes in, server- and client-side +type HTTPTracingService = v1.HTTPTracingService + +// RegisterHTTPTracing enables a HTTPTracingService for further use. +func RegisterHTTPTracing(ht HTTPTracingService) { + v1.RegisterHTTPTracing(ht) +} + +// NewTransport returns another http.RoundTripper, instrumented to emit tracing +// spans according to Options. Provided by the HTTPTracingService registered with +// this package via RegisterHTTPTracing. +func NewTransport(tr http.RoundTripper, opts Options) http.RoundTripper { + return v1.NewTransport(tr, opts) +} + +// NewHandler returns another http.Handler, instrumented to emit tracing spans +// according to Options. Provided by the HTTPTracingService registered with +// this package via RegisterHTTPTracing. +func NewHandler(f http.Handler, label string, opts Options) http.Handler { + return v1.NewHandler(f, label, opts) +} diff --git a/types/decode.go b/types/decode.go new file mode 100644 index 0000000000..ae04b38ff4 --- /dev/null +++ b/types/decode.go @@ -0,0 +1,14 @@ +// 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 types + +import ( + v1 "github.com/open-policy-agent/opa/v1/types" +) + +// Unmarshal deserializes bs and returns the resulting type. +func Unmarshal(bs []byte) (result Type, err error) { + return v1.Unmarshal(bs) +} diff --git a/types/doc.go b/types/doc.go new file mode 100644 index 0000000000..bfa068e66b --- /dev/null +++ b/types/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package types diff --git a/types/types.go b/types/types.go new file mode 100644 index 0000000000..b888b27b60 --- /dev/null +++ b/types/types.go @@ -0,0 +1,200 @@ +// Copyright 2017 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 types declares data types for Rego values and helper functions to +// operate on these types. +package types + +import ( + v1 "github.com/open-policy-agent/opa/v1/types" +) + +// Sprint returns the string representation of the type. +func Sprint(x Type) string { + return v1.Sprint(x) +} + +// Type represents a type of a term in the language. +type Type = v1.Type + +// Null represents the null type. +type Null = v1.Null + +// NewNull returns a new Null type. +func NewNull() Null { + return v1.NewNull() +} + +// NamedType represents a type alias with an arbitrary name and description. +// This is useful for generating documentation for built-in functions. +type NamedType = v1.NamedType + +// Named returns the passed type as a named type. +// Named types are only valid at the top level of built-in functions. +// Note that nested named types cause panic. +func Named(name string, t Type) *NamedType { + return v1.Named(name, t) +} + +// Boolean represents the boolean type. +type Boolean = v1.Boolean + +// B represents an instance of the boolean type. +var B = NewBoolean() + +// NewBoolean returns a new Boolean type. +func NewBoolean() Boolean { + return v1.NewBoolean() +} + +// String represents the string type. +type String = v1.String + +// S represents an instance of the string type. +var S = NewString() + +// NewString returns a new String type. +func NewString() String { + return v1.NewString() +} + +// Number represents the number type. +type Number = v1.Number + +// N represents an instance of the number type. +var N = NewNumber() + +// NewNumber returns a new Number type. +func NewNumber() Number { + return v1.NewNumber() +} + +// Array represents the array type. +type Array = v1.Array + +// NewArray returns a new Array type. +func NewArray(static []Type, dynamic Type) *Array { + return v1.NewArray(static, dynamic) +} + +// Set represents the set type. +type Set = v1.Set + +// NewSet returns a new Set type. +func NewSet(of Type) *Set { + return v1.NewSet(of) +} + +// StaticProperty represents a static object property. +type StaticProperty = v1.StaticProperty + +// NewStaticProperty returns a new StaticProperty object. +func NewStaticProperty(key interface{}, value Type) *StaticProperty { + return v1.NewStaticProperty(key, value) +} + +// DynamicProperty represents a dynamic object property. +type DynamicProperty = v1.DynamicProperty + +// NewDynamicProperty returns a new DynamicProperty object. +func NewDynamicProperty(key, value Type) *DynamicProperty { + return v1.NewDynamicProperty(key, value) +} + +// Object represents the object type. +type Object = v1.Object + +// NewObject returns a new Object type. +func NewObject(static []*StaticProperty, dynamic *DynamicProperty) *Object { + return v1.NewObject(static, dynamic) +} + +// Any represents a dynamic type. +type Any = v1.Any + +// A represents the superset of all types. +var A = NewAny() + +// NewAny returns a new Any type. +func NewAny(of ...Type) Any { + return v1.NewAny(of...) +} + +// Function represents a function type. +type Function = v1.Function + +// Args returns an argument list. +func Args(x ...Type) []Type { + return v1.Args(x...) +} + +// Void returns true if the function has no return value. This function returns +// false if x is not a function. +func Void(x Type) bool { + return v1.Void(x) +} + +// Arity returns the number of arguments in the function signature or zero if x +// is not a function. If the type is unknown, this function returns -1. +func Arity(x Type) int { + return v1.Arity(x) +} + +// NewFunction returns a new Function object of the given argument and result types. +func NewFunction(args []Type, result Type) *Function { + return v1.NewFunction(args, result) +} + +// NewVariadicFunction returns a new Function object. This function sets the +// variadic bit on the signature. Non-void variadic functions are not currently +// supported. +func NewVariadicFunction(args []Type, varargs Type, result Type) *Function { + return v1.NewVariadicFunction(args, varargs, result) +} + +// FuncArgs represents the arguments that can be passed to a function. +type FuncArgs = v1.FuncArgs + +// Compare returns -1, 0, 1 based on comparison between a and b. +func Compare(a, b Type) int { + return v1.Compare(a, b) +} + +// Contains returns true if a is a superset or equal to b. +func Contains(a, b Type) bool { + return v1.Contains(a, b) +} + +// Or returns a type that represents the union of a and b. If one type is a +// superset of the other, the superset is returned unchanged. +func Or(a, b Type) Type { + return v1.Or(a, b) +} + +// Select returns a property or item of a. +func Select(a Type, x interface{}) Type { + return v1.Select(a, x) +} + +// Keys returns the type of keys that can be enumerated for a. For arrays, the +// keys are always number types, for objects the keys are always string types, +// and for sets the keys are always the type of the set element. +func Keys(a Type) Type { + return v1.Keys(a) +} + +// Values returns the type of values that can be enumerated for a. +func Values(a Type) Type { + return v1.Values(a) +} + +// Nil returns true if a's type is unknown. +func Nil(a Type) bool { + return v1.Nil(a) +} + +// TypeOf returns the type of the Golang native value. +func TypeOf(x interface{}) Type { + return v1.TypeOf(x) +} diff --git a/util/backoff.go b/util/backoff.go new file mode 100644 index 0000000000..11da67d926 --- /dev/null +++ b/util/backoff.go @@ -0,0 +1,23 @@ +// 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 util + +import ( + "time" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// DefaultBackoff returns a delay with an exponential backoff based on the +// number of retries. +func DefaultBackoff(base, maxNS float64, retries int) time.Duration { + return v1.DefaultBackoff(base, maxNS, retries) +} + +// Backoff returns a delay with an exponential backoff based on the number of +// retries. Same algorithm used in gRPC. +func Backoff(base, maxNS, jitter, factor float64, retries int) time.Duration { + return v1.Backoff(base, maxNS, jitter, factor, retries) +} diff --git a/util/close.go b/util/close.go new file mode 100644 index 0000000000..7f14cf0700 --- /dev/null +++ b/util/close.go @@ -0,0 +1,18 @@ +// 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 util + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// Close reads the remaining bytes from the response and then closes it to +// ensure that the connection is freed. If the body is not read and closed, a +// leak can occur. +func Close(resp *http.Response) { + v1.Close(resp) +} diff --git a/util/compare.go b/util/compare.go new file mode 100644 index 0000000000..e74d1d49b8 --- /dev/null +++ b/util/compare.go @@ -0,0 +1,19 @@ +// Copyright 2016 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 util + +import ( + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// Compare returns 0 if a equals b, -1 if a is less than b, and 1 if b is than a. +// +// For comparison between values of different types, the following ordering is used: +// nil < bool < int, float64 < string < []interface{} < map[string]interface{}. Slices and maps +// are compared recursively. If one slice or map is a subset of the other slice or map +// it is considered "less than". Nil is always equal to nil. +func Compare(a, b interface{}) int { + return v1.Compare(a, b) +} diff --git a/util/decoding/context.go b/util/decoding/context.go new file mode 100644 index 0000000000..3aef0e01eb --- /dev/null +++ b/util/decoding/context.go @@ -0,0 +1,24 @@ +package decoding + +import ( + "context" + + v1 "github.com/open-policy-agent/opa/v1/util/decoding" +) + +func AddServerDecodingMaxLen(ctx context.Context, maxLen int64) context.Context { + return v1.AddServerDecodingMaxLen(ctx, maxLen) +} + +func AddServerDecodingGzipMaxLen(ctx context.Context, maxLen int64) context.Context { + return v1.AddServerDecodingGzipMaxLen(ctx, maxLen) +} + +// Used for enforcing max body content limits when dealing with chunked requests. +func GetServerDecodingMaxLen(ctx context.Context) (int64, bool) { + return v1.GetServerDecodingMaxLen(ctx) +} + +func GetServerDecodingGzipMaxLen(ctx context.Context) (int64, bool) { + return v1.GetServerDecodingGzipMaxLen(ctx) +} diff --git a/util/decoding/doc.go b/util/decoding/doc.go new file mode 100644 index 0000000000..456d22616f --- /dev/null +++ b/util/decoding/doc.go @@ -0,0 +1,8 @@ +// Copyright 2017 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package decoding diff --git a/util/doc.go b/util/doc.go new file mode 100644 index 0000000000..25f5f53bd3 --- /dev/null +++ b/util/doc.go @@ -0,0 +1,10 @@ +// Copyright 2016 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 util provides generic utilities used throughout the policy engine. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package util diff --git a/util/enumflag.go b/util/enumflag.go new file mode 100644 index 0000000000..6c28d4df40 --- /dev/null +++ b/util/enumflag.go @@ -0,0 +1,19 @@ +// Copyright 2017 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 util + +import ( + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// EnumFlag implements the pflag.Value interface to provide enumerated command +// line parameter values. +type EnumFlag = v1.EnumFlag + +// NewEnumFlag returns a new EnumFlag that has a defaultValue and vs enumerated +// values. +func NewEnumFlag(defaultValue string, vs []string) *EnumFlag { + return v1.NewEnumFlag(defaultValue, vs) +} diff --git a/util/graph.go b/util/graph.go new file mode 100644 index 0000000000..edf59da912 --- /dev/null +++ b/util/graph.go @@ -0,0 +1,34 @@ +// Copyright 2016 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 util + +import v1 "github.com/open-policy-agent/opa/v1/util" + +// Traversal defines a basic interface to perform traversals. +type Traversal = v1.Traversal + +// Equals should return true if node "u" equals node "v". +type Equals = v1.Equals + +// Iter should return true to indicate stop. +type Iter = v1.Iter + +// DFS performs a depth first traversal calling f for each node starting from u. +// If f returns true, traversal stops and DFS returns true. +func DFS(t Traversal, f Iter, u T) bool { + return v1.DFS(t, f, u) +} + +// BFS performs a breadth first traversal calling f for each node starting from +// u. If f returns true, traversal stops and BFS returns true. +func BFS(t Traversal, f Iter, u T) bool { + return v1.BFS(t, f, u) +} + +// DFSPath returns a path from node a to node z found by performing +// a depth first traversal. If no path is found, an empty slice is returned. +func DFSPath(t Traversal, eq Equals, a, z T) []T { + return v1.DFSPath(t, eq, a, z) +} diff --git a/util/hashmap.go b/util/hashmap.go new file mode 100644 index 0000000000..5f030c77b5 --- /dev/null +++ b/util/hashmap.go @@ -0,0 +1,20 @@ +// Copyright 2016 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 util + +import ( + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// T is a concise way to refer to T. +type T = v1.T + +// HashMap represents a key/value map. +type HashMap = v1.HashMap + +// NewHashMap returns a new empty HashMap. +func NewHashMap(eq func(T, T) bool, hash func(T) int) *HashMap { + return v1.NewHashMap(eq, hash) +} diff --git a/util/json.go b/util/json.go new file mode 100644 index 0000000000..9b19a967ba --- /dev/null +++ b/util/json.go @@ -0,0 +1,68 @@ +// Copyright 2016 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 util + +import ( + "encoding/json" + "io" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// UnmarshalJSON parses the JSON encoded data and stores the result in the value +// pointed to by x. +// +// 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{}) error { + return v1.UnmarshalJSON(bs, x) +} + +// NewJSONDecoder returns a new decoder that reads from r. +// +// This function is intended to be used in place of the standard json.NewDecoder +// when json.Number is required. +func NewJSONDecoder(r io.Reader) *json.Decoder { + return v1.NewJSONDecoder(r) +} + +// MustUnmarshalJSON parse the JSON encoded data and returns the result. +// +// If the data cannot be decoded, this function will panic. This function is for +// test purposes. +func MustUnmarshalJSON(bs []byte) interface{} { + return v1.MustUnmarshalJSON(bs) +} + +// MustMarshalJSON returns the JSON encoding of x +// +// If the data cannot be encoded, this function will panic. This function is for +// test purposes. +func MustMarshalJSON(x interface{}) []byte { + return v1.MustMarshalJSON(x) +} + +// RoundTrip encodes to JSON, and decodes the result again. +// +// Thereby, it is converting its argument to the representation expected by +// rego.Input and inmem's Write operations. Works with both references and +// values. +func RoundTrip(x *interface{}) error { + return v1.RoundTrip(x) +} + +// Reference returns a pointer to its argument unless the argument already is +// a pointer. If the argument is **t, or ***t, etc, it will return *t. +// +// Used for preparing Go types (including pointers to structs) into values to be +// put through util.RoundTrip(). +func Reference(x interface{}) *interface{} { + return v1.Reference(x) +} + +// Unmarshal decodes a YAML, JSON or JSON extension value into the specified type. +func Unmarshal(bs []byte, v interface{}) error { + return v1.Unmarshal(bs, v) +} diff --git a/util/maps.go b/util/maps.go new file mode 100644 index 0000000000..1a9c71f28d --- /dev/null +++ b/util/maps.go @@ -0,0 +1,8 @@ +package util + +import v1 "github.com/open-policy-agent/opa/v1/util" + +// Values returns a slice of values from any map. Copied from golang.org/x/exp/maps. +func Values[M ~map[K]V, K comparable, V any](m M) []V { + return v1.Values(m) +} diff --git a/util/queue.go b/util/queue.go new file mode 100644 index 0000000000..d130a97abe --- /dev/null +++ b/util/queue.go @@ -0,0 +1,25 @@ +// Copyright 2017 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 util + +import v1 "github.com/open-policy-agent/opa/v1/util" + +// LIFO represents a simple LIFO queue. +type LIFO = v1.LIFO + +// NewLIFO returns a new LIFO queue containing elements ts starting with the +// left-most argument at the bottom. +func NewLIFO(ts ...T) *LIFO { + return v1.NewLIFO(ts...) +} + +// FIFO represents a simple FIFO queue. +type FIFO = v1.FIFO + +// NewFIFO returns a new FIFO queue containing elements ts starting with the +// left-most argument at the front. +func NewFIFO(ts ...T) *FIFO { + return v1.NewFIFO(ts...) +} diff --git a/util/read_gzip_body.go b/util/read_gzip_body.go new file mode 100644 index 0000000000..70b615a4e3 --- /dev/null +++ b/util/read_gzip_body.go @@ -0,0 +1,17 @@ +package util + +import ( + "net/http" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// Note(philipc): Originally taken from server/server.go +// The DecodingLimitHandler handles validating that the gzip payload is within the +// allowed max size limit. Thus, in the event of a forged payload size trailer, +// the worst that can happen is that we waste memory up to the allowed max gzip +// payload size, but not an unbounded amount of memory, as was potentially +// possible before. +func ReadMaybeCompressedBody(r *http.Request) ([]byte, error) { + return v1.ReadMaybeCompressedBody(r) +} diff --git a/util/test/benchmark.go b/util/test/benchmark.go new file mode 100644 index 0000000000..d73c437aca --- /dev/null +++ b/util/test/benchmark.go @@ -0,0 +1,58 @@ +package test + +// This file collects some helpers for generating data used in +// benchmarks, +// - topdown/topdown_bench_test.go + +import ( + v1 "github.com/open-policy-agent/opa/v1/util/test" +) + +// PartialObjectBenchmarkCrossModule returns a module with n "bench_test_" prefixed rules +// that each refer to another "cond_bench_" prefixed rule +func PartialObjectBenchmarkCrossModule(n int) []string { + return v1.PartialObjectBenchmarkCrossModule(n) +} + +// ArrayIterationBenchmarkModule returns a module that iterates an array +// with `n` elements +func ArrayIterationBenchmarkModule(n int) string { + return v1.ArrayIterationBenchmarkModule(n) +} + +// SetIterationBenchmarkModule returns a module that iterates a set +// with `n` elements +func SetIterationBenchmarkModule(n int) string { + return v1.SetIterationBenchmarkModule(n) +} + +// ObjectIterationBenchmarkModule returns a module that iterates an object +// with `n` key/val pairs +func ObjectIterationBenchmarkModule(n int) string { + return v1.ObjectIterationBenchmarkModule(n) +} + +// GenerateLargeJSONBenchmarkData returns a map of 100 keys and 100.000 key/value +// pairs. +func GenerateLargeJSONBenchmarkData() map[string]interface{} { + return v1.GenerateLargeJSONBenchmarkData() +} + +// GenerateJSONBenchmarkData returns a map of `k` keys and `v` key/value pairs. +func GenerateJSONBenchmarkData(k, v int) map[string]interface{} { + return v1.GenerateJSONBenchmarkData(k, v) +} + +// GenerateConcurrencyBenchmarkData returns a module and data; the module +// checks some input parameters against that data in a simple API authz +// scheme. +func GenerateConcurrencyBenchmarkData() (string, map[string]interface{}) { + return v1.GenerateConcurrencyBenchmarkData() +} + +// GenerateVirtualDocsBenchmarkData generates a module and input; the +// numTotalRules and numHitRules create as many rules in the module to +// match/miss the returned input. +func GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules int) (string, map[string]interface{}) { + return v1.GenerateVirtualDocsBenchmarkData(numTotalRules, numHitRules) +} diff --git a/util/test/ci_skip.go b/util/test/ci_skip.go new file mode 100644 index 0000000000..3380c15925 --- /dev/null +++ b/util/test/ci_skip.go @@ -0,0 +1,10 @@ +//go:build !darwin +// +build !darwin + +package test + +import "testing" + +// Skip will skip this test on pull request CI runs. +// Used for slow test runners on GHA's darwin machines. +func Skip(*testing.T) {} diff --git a/util/test/ci_skip_darwin.go b/util/test/ci_skip_darwin.go new file mode 100644 index 0000000000..ffdf489007 --- /dev/null +++ b/util/test/ci_skip_darwin.go @@ -0,0 +1,13 @@ +package test + +import ( + "testing" + + v1 "github.com/open-policy-agent/opa/v1/util/test" +) + +// Skip will skip this test on pull request CI runs. +// Used for slow test runners on GHA's darwin machines. +func Skip(t *testing.T) { + v1.Skip(t) +} diff --git a/util/test/doc.go b/util/test/doc.go new file mode 100644 index 0000000000..9b965f3f19 --- /dev/null +++ b/util/test/doc.go @@ -0,0 +1,10 @@ +// Copyright 2017 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 test contains utilities used in the policy engine's test suite. +// +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package test diff --git a/util/test/tempfs.go b/util/test/tempfs.go new file mode 100644 index 0000000000..c54a4b4613 --- /dev/null +++ b/util/test/tempfs.go @@ -0,0 +1,31 @@ +// Copyright 2017 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 test + +import ( + "io/fs" + + v1 "github.com/open-policy-agent/opa/v1/util/test" +) + +// WithTempFS creates a temporary directory structure and invokes f with the +// root directory path. +func WithTempFS(files map[string]string, f func(string)) { + v1.WithTempFS(files, f) +} + +// MakeTempFS creates a temporary directory structure for test purposes rooted at root. +// If root is empty, the dir is created in the default system temp location. +// If the creation fails, cleanup is nil and the caller does not have to invoke it. If +// creation succeeds, the caller should invoke cleanup when they are done. +func MakeTempFS(root, prefix string, files map[string]string) (rootDir string, cleanup func(), err error) { + return v1.MakeTempFS(root, prefix, files) +} + +// WithTestFS creates a temporary file system of `files` in memory +// if `inMemoryFS` is true and invokes `f“ with that filesystem +func WithTestFS(files map[string]string, inMemoryFS bool, f func(string, fs.FS)) { + v1.WithTestFS(files, inMemoryFS, f) +} diff --git a/util/test/tempus.go b/util/test/tempus.go new file mode 100644 index 0000000000..e557667cb8 --- /dev/null +++ b/util/test/tempus.go @@ -0,0 +1,24 @@ +// 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 test + +import ( + "testing" + "time" + + v1 "github.com/open-policy-agent/opa/v1/util/test" +) + +func Eventually(t *testing.T, timeout time.Duration, f func() bool) bool { + t.Helper() + return v1.Eventually(t, timeout, f) +} + +func EventuallyOrFatal(t *testing.T, timeout time.Duration, f func() bool) { + t.Helper() + v1.EventuallyOrFatal(t, timeout, f) +} + +type BlockingWriter = v1.BlockingWriter diff --git a/util/time.go b/util/time.go new file mode 100644 index 0000000000..3641974705 --- /dev/null +++ b/util/time.go @@ -0,0 +1,38 @@ +package util + +import ( + "time" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// TimerWithCancel exists because of memory leaks when using +// time.After in select statements. Instead, we now manually create timers, +// wait on them, and manually free them. +// +// See this for more details: +// https://www.arangodb.com/2020/09/a-story-of-a-memory-leak-in-go-how-to-properly-use-time-after/ +// +// Note: This issue is fixed in Go 1.23, but this fix helps us until then. +// +// Warning: the cancel cannot be done concurrent to reading, everything should +// work in the same goroutine. +// +// Example: +// +// for retries := 0; true; retries++ { +// +// ...main logic... +// +// timer, cancel := utils.TimerWithCancel(utils.Backoff(retries)) +// select { +// case <-ctx.Done(): +// cancel() +// return ctx.Err() +// case <-timer.C: +// continue +// } +// } +func TimerWithCancel(delay time.Duration) (*time.Timer, func()) { + return v1.TimerWithCancel(delay) +} diff --git a/util/wait.go b/util/wait.go new file mode 100644 index 0000000000..235f84a0e1 --- /dev/null +++ b/util/wait.go @@ -0,0 +1,19 @@ +// 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 util + +import ( + "time" + + v1 "github.com/open-policy-agent/opa/v1/util" +) + +// WaitFunc will call passed function at an interval and return nil +// as soon this function returns true. +// If timeout is reached before the passed in function returns true +// an error is returned. +func WaitFunc(fun func() bool, interval, timeout time.Duration) error { + return v1.WaitFunc(fun, interval, timeout) +} diff --git a/v1/ast/compile.go b/v1/ast/compile.go index 10f6c17172..91876a55e3 100644 --- a/v1/ast/compile.go +++ b/v1/ast/compile.go @@ -149,6 +149,11 @@ type Compiler struct { allowUndefinedFuncCalls bool // don't error on calls to unknown functions. evalMode CompilerEvalMode // rewriteTestRulesForTracing bool // rewrite test rules to capture dynamic values for tracing. + defaultRegoVersion RegoVersion +} + +func (c *Compiler) DefaultRegoVersion() RegoVersion { + return c.defaultRegoVersion } // CompilerStage defines the interface for stages in the compiler. @@ -310,6 +315,7 @@ func NewCompiler() *Compiler { deprecatedBuiltinsMap: map[string]struct{}{}, comprehensionIndices: map[*Term]*ComprehensionIndex{}, debug: debug.Discard(), + defaultRegoVersion: DefaultRegoVersion, } c.ModuleTree = NewModuleTree(nil) @@ -892,6 +898,13 @@ func (c *Compiler) WithModuleLoader(f ModuleLoader) *Compiler { return c } +// WithDefaultRegoVersion sets the default Rego version to use when a module doesn't specify one; +// such as when it's hand-crafted instead of parsed. +func (c *Compiler) WithDefaultRegoVersion(regoVersion RegoVersion) *Compiler { + c.defaultRegoVersion = regoVersion + return c +} + func (c *Compiler) counterAdd(name string, n uint64) { if c.metrics == nil { return @@ -1717,7 +1730,7 @@ func (c *Compiler) checkDuplicateImports() { for _, name := range c.sorted { mod := c.Modules[name] - if c.strict || mod.regoV1Compatible() { + if c.strict || c.moduleIsRegoV1(mod) { modules = append(modules, mod) } } @@ -1731,7 +1744,7 @@ func (c *Compiler) checkDuplicateImports() { func (c *Compiler) checkKeywordOverrides() { for _, name := range c.sorted { mod := c.Modules[name] - if c.strict || mod.regoV1Compatible() { + if c.strict || c.moduleIsRegoV1(mod) { errs := checkRootDocumentOverrides(mod) for _, err := range errs { c.err(err) @@ -1740,6 +1753,17 @@ func (c *Compiler) checkKeywordOverrides() { } } +func (c *Compiler) moduleIsRegoV1(mod *Module) bool { + if mod.regoVersion == RegoUndefined { + switch c.defaultRegoVersion { + case RegoV1, RegoV0CompatV1: + return true + } + return false + } + return mod.regoV1Compatible() +} + // resolveAllRefs resolves references in expressions to their fully qualified values. // // For instance, given the following module: diff --git a/v1/ast/compile_test.go b/v1/ast/compile_test.go index 7e4d7f64ee..a76bd277d9 100644 --- a/v1/ast/compile_test.go +++ b/v1/ast/compile_test.go @@ -11183,3 +11183,78 @@ test_something = true if { }) } } + +func TestCompile_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]*Module + expErrs Errors + }{ + { + note: "no module rego-version, no v1 violations", + modules: map[string]*Module{ + "test": { + Package: MustParsePackage(`package test`), + Imports: MustParseImports(`import data.foo + import data.bar`), + }, + }, + }, + { + note: "no module rego-version, v1 violations", // default is v1, errors expected + modules: map[string]*Module{ + "test": { + Package: MustParsePackage(`package test`), + Imports: MustParseImports(`import data.foo + import data.bar as foo`), + }, + }, + expErrs: Errors{ + &Error{ + Code: CompileErr, + Message: "import must not shadow import data.foo", + }, + }, + }, + { + note: "v0 module, v1 violations", + modules: map[string]*Module{ + "test": MustParseModuleWithOpts(`package test + import data.foo + import data.bar as foo`, + ParserOptions{RegoVersion: RegoV0}), + }, + }, + { + note: "v1 module, v1 violations", + modules: map[string]*Module{ + "test": MustParseModuleWithOpts(`package test + import data.foo + import data.bar as foo`, + ParserOptions{RegoVersion: RegoV1}), + }, + expErrs: Errors{ + &Error{ + Code: CompileErr, + Message: "import must not shadow import data.foo", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + compiler := NewCompiler() + + compiler.Compile(tc.modules) + + if len(tc.expErrs) > 0 { + assertErrors(t, compiler.Errors, tc.expErrs, false) + } else { + if len(compiler.Errors) > 0 { + t.Fatalf("Unexpected errors: %v", compiler.Errors) + } + } + }) + } +} diff --git a/v1/ast/compilehelper.go b/v1/ast/compilehelper.go index dd48884f9d..7d81d45e6d 100644 --- a/v1/ast/compilehelper.go +++ b/v1/ast/compilehelper.go @@ -31,7 +31,9 @@ func CompileModulesWithOpt(modules map[string]string, opts CompileOpts) (*Compil parsed[f] = pm } - compiler := NewCompiler().WithEnablePrintStatements(opts.EnablePrintStatements) + compiler := NewCompiler(). + WithDefaultRegoVersion(opts.ParserOptions.RegoVersion). + WithEnablePrintStatements(opts.EnablePrintStatements) compiler.Compile(parsed) if compiler.Failed() { diff --git a/v1/ast/compilehelper_test.go b/v1/ast/compilehelper_test.go new file mode 100644 index 0000000000..28a024efd0 --- /dev/null +++ b/v1/ast/compilehelper_test.go @@ -0,0 +1,236 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast + +import ( + "strings" + "testing" +) + +func TestCompileModules_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]string + expErrs []string + }{ + // NOT default rego-version + { + note: "v0 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p[x] { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v0 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p[x] { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_parse_error: `if` keyword is required before rule body", + "test.rego:5: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + + // cross-rego-version + { + note: "rego.v1 import, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + p contains x if { + x = "a" + }`, + }, + }, + { + note: "rego.v1 import, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_compile_error: import must not shadow import data.foo", + }, + }, + + // default rego-version + { + note: "v1 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p contains x if { + x = "a" + }`, + }, + }, + { + note: "v1 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:3: rego_compile_error: import must not shadow import data.foo", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + _, err := CompileModules(tc.modules) + + if len(tc.expErrs) > 0 { + for _, expErr := range tc.expErrs { + if err := err.Error(); !strings.Contains(err, expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestCompileModulesWithOpt_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + modules map[string]string + expErrs []string + }{ + // NOT default rego-version + { + note: "v0 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p[x] { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v0 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p[x] { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_parse_error: `if` keyword is required before rule body", + "test.rego:5: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + + // cross-rego-version + { + note: "rego.v1 import, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + p contains x if { + x = "a" + }`, + }, + }, + { + note: "rego.v1 import, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import rego.v1 + + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:5: rego_compile_error: import must not shadow import data.foo", + }, + }, + + // default rego-version + { + note: "v1 module, no v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + p contains x if { + x = "a" + }`, + }, + }, + { + note: "v1 module, v1 compile-time violations", + modules: map[string]string{ + "test.rego": `package test + import data.foo + import data.bar as foo + + p contains x if { + x = "a" + }`, + }, + expErrs: []string{ + "test.rego:3: rego_compile_error: import must not shadow import data.foo", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + _, err := CompileModulesWithOpt(tc.modules, CompileOpts{EnablePrintStatements: true}) + + if len(tc.expErrs) > 0 { + for _, expErr := range tc.expErrs { + if err := err.Error(); !strings.Contains(err, expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} diff --git a/v1/ast/parser.go b/v1/ast/parser.go index d93c120fac..a537d8b67d 100644 --- a/v1/ast/parser.go +++ b/v1/ast/parser.go @@ -30,11 +30,12 @@ var RegoV1CompatibleRef = Ref{VarTerm("rego"), StringTerm("v1")} // RegoVersion defines the Rego syntax requirements for a module. type RegoVersion int -const DefaultRegoVersion = RegoVersion(0) +const DefaultRegoVersion = RegoV1 const ( + RegoUndefined RegoVersion = iota // RegoV0 is the default, original Rego syntax. - RegoV0 RegoVersion = iota + RegoV0 // RegoV0CompatV1 requires modules to comply with both the RegoV0 and RegoV1 syntax (as when 'rego.v1' is imported in a module). // Shortly, RegoV1 compatibility is required, but 'rego.v1' or 'future.keywords' must also be imported. RegoV0CompatV1 @@ -156,8 +157,10 @@ type ParserOptions struct { } // EffectiveRegoVersion returns the effective RegoVersion to use for parsing. -// Deprecated: Use RegoVersion instead. func (po *ParserOptions) EffectiveRegoVersion() RegoVersion { + if po.RegoVersion == RegoUndefined { + return DefaultRegoVersion + } return po.RegoVersion } @@ -314,7 +317,7 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) { allowedFutureKeywords := map[string]tokens.Token{} - if p.po.RegoVersion == RegoV1 { + if p.po.EffectiveRegoVersion() == RegoV1 { // RegoV1 includes all future keywords in the default language definition for k, v := range futureKeywords { allowedFutureKeywords[k] = v @@ -373,7 +376,7 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) { } selected := map[string]tokens.Token{} - if p.po.AllFutureKeywords || p.po.RegoVersion == RegoV1 { + if p.po.AllFutureKeywords || p.po.EffectiveRegoVersion() == RegoV1 { for kw, tok := range allowedFutureKeywords { selected[kw] = tok } @@ -394,7 +397,7 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) { } p.s.s = p.s.s.WithKeywords(selected) - if p.po.RegoVersion == RegoV1 { + if p.po.EffectiveRegoVersion() == RegoV1 { for kw, tok := range allowedFutureKeywords { p.s.s.AddKeyword(kw, tok) } @@ -2710,7 +2713,7 @@ func (p *Parser) regoV1Import(imp *Import) { return } - if p.po.RegoVersion == RegoV1 { + if p.po.EffectiveRegoVersion() == RegoV1 { // We're parsing for Rego v1, where the 'rego.v1' import is a no-op. return } diff --git a/v1/ast/parser_ext.go b/v1/ast/parser_ext.go index 7649c322de..f08c112a72 100644 --- a/v1/ast/parser_ext.go +++ b/v1/ast/parser_ext.go @@ -689,7 +689,12 @@ func parseModule(filename string, stmts []Statement, comments []*Comment, regoCo // The comments slice only holds comments that were not their own statements. mod.Comments = append(mod.Comments, comments...) - mod.regoVersion = regoCompatibilityMode + + if regoCompatibilityMode == RegoUndefined { + mod.regoVersion = DefaultRegoVersion + } else { + mod.regoVersion = regoCompatibilityMode + } for i, stmt := range stmts[1:] { switch stmt := stmt.(type) { diff --git a/v1/ast/parser_ext_test.go b/v1/ast/parser_ext_test.go new file mode 100644 index 0000000000..a844c73659 --- /dev/null +++ b/v1/ast/parser_ext_test.go @@ -0,0 +1,128 @@ +// Copyright 2024 The OPA Authors. All rights reserved. +// Use of this source code is governed by an Apache2 +// license that can be found in the LICENSE file. + +package ast_test + +import ( + "strings" + "testing" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/format" +) + +func TestParseModule_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + mod string + expRules []string + expErrs []string + }{ + { + note: "v0", // NOT default rego-version + mod: `package test +p[x] { + x = "a" +}`, + expErrs: []string{ + "test.rego:2: rego_parse_error: `if` keyword is required before rule body", + "test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "import rego.v1", + mod: `package test +import rego.v1 + +p contains x if { + x = "a" +}`, + expRules: []string{"p"}, + }, + { + note: "v1", // default rego-version + mod: `package test +p contains x if { + x = "a" +}`, + expRules: []string{"p"}, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + m, err := ast.ParseModule("test.rego", tc.mod) + + if len(tc.expErrs) > 0 { + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\ngot:\n\n%s", expErr, err.Error()) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(m.Rules) != len(tc.expRules) { + t.Fatalf("Expected %d rules, got %d", len(tc.expRules), len(m.Rules)) + } + for i, r := range m.Rules { + if r.Head.Name.String() != tc.expRules[i] { + t.Fatalf("Expected rule %q, got %q", tc.expRules[i], r.Head.Name.String()) + } + } + } + }) + } +} + +func TestParseBody_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + body string + expStmts int + assertSame bool + }{ + { + note: "v0", // default rego-version + body: `x := ["a", "b", "c"][i] +`, + expStmts: 1, + assertSame: true, + }, + { + note: "v1", // NOT default rego-version + body: `some x, i in ["a", "b", "c"] +`, + expStmts: 1, + assertSame: true, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + body, err := ast.ParseBody(tc.body) + + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(body) != tc.expStmts { + t.Fatalf("Expected %d statements, got %d:%q\n\n", tc.expStmts, len(body), body) + } + + if tc.assertSame { + formatted, err := format.AstWithOpts(body, format.Opts{RegoVersion: ast.RegoV1}) // every body is v1-compatible + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if strings.Compare(string(formatted), tc.body) != 0 { + t.Fatalf("Expected body to be %q, got %q", tc.body, string(formatted)) + } + } + }) + } +} diff --git a/v1/ast/policy.go b/v1/ast/policy.go index 27a08eaca0..7b568c3928 100644 --- a/v1/ast/policy.go +++ b/v1/ast/policy.go @@ -320,6 +320,11 @@ type ( } ) +// SetModuleRegoVersion sets the RegoVersion for the Module. +func SetModuleRegoVersion(mod *Module, v RegoVersion) { + mod.regoVersion = v +} + // Compare returns an integer indicating whether mod is less than, equal to, // or greater than other. func (mod *Module) Compare(other *Module) int { @@ -784,13 +789,24 @@ func (rule *Rule) Ref() Ref { } func (rule *Rule) String() string { - return rule.stringWithOpts(toStringOpts{}) + regoVersion := DefaultRegoVersion + if rule.Module != nil { + regoVersion = rule.Module.RegoVersion() + } + return rule.stringWithOpts(toStringOpts{regoVersion: regoVersion}) } type toStringOpts struct { regoVersion RegoVersion } +func (o toStringOpts) RegoVersion() RegoVersion { + if o.regoVersion == RegoUndefined { + return DefaultRegoVersion + } + return o.regoVersion +} + func (rule *Rule) stringWithOpts(opts toStringOpts) string { buf := []string{} if rule.Default { @@ -798,7 +814,7 @@ func (rule *Rule) stringWithOpts(opts toStringOpts) string { } buf = append(buf, rule.Head.stringWithOpts(opts)) if !rule.Default { - switch opts.regoVersion { + switch opts.RegoVersion() { case RegoV1, RegoV0CompatV1: buf = append(buf, "if") } @@ -861,7 +877,7 @@ func (rule *Rule) elseString(opts toStringOpts) string { buf = append(buf, value.String()) } - switch opts.regoVersion { + switch opts.RegoVersion() { case RegoV1, RegoV0CompatV1: buf = append(buf, "if") } @@ -1043,7 +1059,7 @@ func (head *Head) stringWithOpts(opts toStringOpts) string { case len(head.Args) != 0: buf.WriteString(head.Args.String()) case len(head.Reference) == 1 && head.Key != nil: - switch opts.regoVersion { + switch opts.RegoVersion() { case RegoV0: buf.WriteRune('[') buf.WriteString(head.Key.String()) diff --git a/v1/ast/policy_test.go b/v1/ast/policy_test.go index fc16584a18..11e1a2acfe 100644 --- a/v1/ast/policy_test.go +++ b/v1/ast/policy_test.go @@ -626,6 +626,60 @@ func TestRuleString(t *testing.T) { } } +func TestRuleString_DefaultRegoVersion(t *testing.T) { + // ast.Rule.String() will respect the rego-version of the ast.Module it is part of. + + tests := []struct { + note string + module string + regoVersion RegoVersion + exp string + }{ + { + note: "v0", + regoVersion: RegoV0, + module: `package a.b.c + +p[x] { x = "a" }`, + exp: `p[x] { x = "a" }`, + }, + { + note: "v1", + regoVersion: RegoV1, + module: `package a.b.c + +p contains x if { x = "a" }`, + exp: `p contains x if { x = "a" }`, + }, + { + note: "default rego-version", + module: `package a.b.c + +p contains x if { x = "a" }`, + exp: `p contains x if { x = "a" }`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + var mod *Module + + if tc.regoVersion == RegoUndefined { + mod = MustParseModule(tc.module) + } else { + mod = MustParseModuleWithOpts(tc.module, ParserOptions{RegoVersion: tc.regoVersion}) + } + + rule := mod.Rules[0] + act := rule.String() + + if act != tc.exp { + t.Fatalf("Expected:\n\n%s\n\nbut got:\n\n%s", tc.exp, act) + } + }) + } +} + func TestRulePath(t *testing.T) { ruleWithMod := func(r string) Ref { mod := module("package pkg\n" + r) @@ -642,9 +696,9 @@ func TestRulePath(t *testing.T) { func TestModuleString(t *testing.T) { + // v1 module input := `package a.b.c -import rego.v1 import data.foo.bar import input.xyz diff --git a/v1/bundle/bundle.go b/v1/bundle/bundle.go index c48df296fb..e4f42b8ded 100644 --- a/v1/bundle/bundle.go +++ b/v1/bundle/bundle.go @@ -711,10 +711,10 @@ func (r *Reader) Read() (Bundle, error) { // Parse modules popts := r.ParserOptions() - popts.RegoVersion = bundle.RegoVersion(popts.RegoVersion) + popts.RegoVersion = bundle.RegoVersion(popts.EffectiveRegoVersion()) for _, mf := range modules { modulePopts := popts - if modulePopts.RegoVersion, err = bundle.RegoVersionForFile(mf.RelativePath, popts.RegoVersion); err != nil { + if modulePopts.RegoVersion, err = bundle.RegoVersionForFile(mf.RelativePath, popts.EffectiveRegoVersion()); err != nil { return bundle, err } r.metrics.Timer(metrics.RegoModuleParse).Start() @@ -1203,6 +1203,10 @@ func (b *Bundle) SetRegoVersion(v ast.RegoVersion) { // If there is no defined version for the given path, the default version def is returned. // If the version does not correspond to ast.RegoV0 or ast.RegoV1, an error is returned. func (b *Bundle) RegoVersionForFile(path string, def ast.RegoVersion) (ast.RegoVersion, error) { + if def == ast.RegoUndefined { + def = ast.DefaultRegoVersion + } + version, err := b.Manifest.numericRegoVersionForFile(path) if err != nil { return def, err @@ -1393,7 +1397,7 @@ func mktree(path []string, value interface{}) (map[string]interface{}, error) { // will have an empty revision except in the special case where a single bundle is provided // (and in that case the bundle is just returned unmodified.) func Merge(bundles []*Bundle) (*Bundle, error) { - return MergeWithRegoVersion(bundles, ast.RegoV0, false) + return MergeWithRegoVersion(bundles, ast.DefaultRegoVersion, false) } // MergeWithRegoVersion creates a merged bundle from the provided bundles, similar to Merge. @@ -1410,6 +1414,10 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat return nil, errors.New("expected at least one bundle") } + if regoVersion == ast.RegoUndefined { + regoVersion = ast.DefaultRegoVersion + } + if len(bundles) == 1 { result := bundles[0] // We respect the bundle rego-version, defaulting to the provided rego version if not set. diff --git a/v1/bundle/bundle_test.go b/v1/bundle/bundle_test.go index 2ad600ff0f..042eae2411 100644 --- a/v1/bundle/bundle_test.go +++ b/v1/bundle/bundle_test.go @@ -170,6 +170,79 @@ func TestReadWithBaseDir(t *testing.T) { } } +func TestRead_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package example + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package example +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package example + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + module := tc.module + files := [][2]string{ + {"test.rego", module}, + } + + buf := archive.MustWriteTarGz(files) + loader := NewTarballLoaderWithBaseURL(buf, "") + br := NewCustomReader(loader) + + bundle, err := br.Read() + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error(s):\n\n%v\n\nbut got nil", tc.expErrs) + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %s", err) + } + + if len(bundle.Modules) != 1 { + t.Fatalf("expected 1 module but got %d", len(bundle.Modules)) + } + } + }) + } +} + func TestReadWithSizeLimit(t *testing.T) { buf := archive.MustWriteTarGz([][2]string{ @@ -1681,6 +1754,8 @@ func TestMergeCorruptManifest(t *testing.T) { func TestMerge(t *testing.T) { + expRegoVersion := ast.DefaultRegoVersion.Int() + cases := []struct { note string bundles []*Bundle @@ -1711,7 +1786,7 @@ func TestMerge(t *testing.T) { Manifest: Manifest{ Revision: "abcdef", Roots: &[]string{""}, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, FileRegoVersions: map[string]int{}, }, Modules: []ModuleFile{ @@ -1749,7 +1824,7 @@ func TestMerge(t *testing.T) { "foo", "bar", }, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, Data: map[string]interface{}{}, }, @@ -1794,7 +1869,7 @@ func TestMerge(t *testing.T) { "logs", "authz", }, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, WasmModules: []WasmModuleFile{ { @@ -1851,7 +1926,7 @@ func TestMerge(t *testing.T) { "foo", "baz", }, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, Modules: []ModuleFile{ { @@ -1900,7 +1975,7 @@ func TestMerge(t *testing.T) { "foo/bar", "baz", }, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, Data: map[string]interface{}{ "foo": map[string]interface{}{ @@ -1936,7 +2011,7 @@ func TestMerge(t *testing.T) { "foo/bar", "baz", }, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, Data: map[string]interface{}{}, }, @@ -1973,7 +2048,7 @@ func TestMerge(t *testing.T) { Data: map[string]interface{}{}, Manifest: Manifest{ Roots: &[]string{"a", "b"}, - RegoVersion: pointTo(0), // Default rego-version + RegoVersion: &expRegoVersion, }, PlanModules: []PlanModuleFile{ { diff --git a/v1/compile/compile.go b/v1/compile/compile.go index 9b545d1e12..96b1f844c1 100644 --- a/v1/compile/compile.go +++ b/v1/compile/compile.go @@ -94,6 +94,7 @@ func New() *Compiler { optimizationLevel: 0, target: TargetRego, debug: debug.Discard(), + regoVersion: ast.DefaultRegoVersion, } } @@ -294,6 +295,10 @@ func addEntrypointsFromAnnotations(c *Compiler, arefs []*ast.AnnotationsRef) err // Build compiles and links the input files and outputs a bundle to the writer. func (c *Compiler) Build(ctx context.Context) error { + if c.regoVersion == ast.RegoUndefined { + return fmt.Errorf("rego-version not set") + } + if err := c.init(); err != nil { return err } diff --git a/v1/compile/compile_test.go b/v1/compile/compile_test.go index de48764daf..dc6bcb0fc7 100644 --- a/v1/compile/compile_test.go +++ b/v1/compile/compile_test.go @@ -27,6 +27,47 @@ import ( "github.com/open-policy-agent/opa/v1/util/test" ) +func TestCompilerV1Module(t *testing.T) { + + files := map[string]string{ + "test.rego": ` + package test + + p contains x if { + x = "a" + }`, + } + + for _, useMemoryFS := range []bool{false, true} { + test.WithTestFS(files, useMemoryFS, func(root string, fsys fs.FS) { + + compiler := New(). + WithFS(fsys). + WithPaths(root) + + err := compiler.Build(context.Background()) + if err != nil { + t.Fatal(err) + } + + // Verify result is just bundle load. + exp, err := loader.NewFileLoader().WithFS(fsys).AsBundle(root) + if err != nil { + panic(err) + } + + err = exp.FormatModules(false) + if err != nil { + t.Fatal(err) + } + + if !compiler.Bundle().Equal(*exp) { + t.Fatalf("Expected:\n\n%v\n\nGot:\n\n%v", compiler.Bundle(), exp) + } + }) + } +} + func TestOrderedStringSet(t *testing.T) { var ss orderedStringSet result := ss.Append("a", "b", "b", "a", "e", "c", "e") @@ -451,11 +492,11 @@ p contains "B" if { } } -func pointTo[T any](x T) *T { - return &x -} - func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { + regoV0 := ast.RegoV0.Int() + regoV1 := ast.RegoV1.Int() + regoDef := ast.RegoV1.Int() + tests := []struct { note string bundles []*bundle.Bundle @@ -474,7 +515,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { Modules: []bundle.ModuleFile{}, }, }, - expGlobalRegoVersion: pointTo(ast.DefaultRegoVersion.Int()), + expGlobalRegoVersion: ®oDef, expFileRegoVersions: map[string]int{}, }, { @@ -483,13 +524,13 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{}, }, }, - expGlobalRegoVersion: pointTo(1), + expGlobalRegoVersion: ®oV1, expFileRegoVersions: map[string]int{}, }, { @@ -511,7 +552,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, }, regoVersion: ast.RegoV1, - expGlobalRegoVersion: pointTo(1), + expGlobalRegoVersion: ®oV1, }, { note: "global rego versions, v1 bundles, v0 provided", @@ -519,7 +560,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -534,7 +575,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"b"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -549,7 +590,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, regoVersion: ast.RegoV0, // global rego-version in bundles are dropped in favor of the provided rego-version - expGlobalRegoVersion: pointTo(0), + expGlobalRegoVersion: ®oV0, expFileRegoVersions: map[string]int{ "/a/test1.rego": 1, "/b/test1.rego": 1, @@ -561,7 +602,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(0), + RegoVersion: ®oV0, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -576,7 +617,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"b"}, - RegoVersion: pointTo(0), + RegoVersion: ®oV0, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -591,7 +632,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, regoVersion: ast.RegoV1, // global rego-version in bundles are dropped in favor of the provided rego-version - expGlobalRegoVersion: pointTo(1), + expGlobalRegoVersion: ®oV1, expFileRegoVersions: map[string]int{ "/a/test1.rego": 0, "/b/test1.rego": 0, @@ -603,7 +644,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(0), + RegoVersion: ®oV0, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -618,7 +659,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"b"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -633,7 +674,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, regoVersion: ast.RegoV0, // global rego-version in bundles are dropped in favor of the provided rego-version - expGlobalRegoVersion: pointTo(0), + expGlobalRegoVersion: ®oV0, expFileRegoVersions: map[string]int{ "/b/test1.rego": 1, }, @@ -644,7 +685,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, }, Data: map[string]interface{}{}, Modules: []bundle.ModuleFile{ @@ -665,7 +706,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"b"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, FileRegoVersions: map[string]int{ "/test1.rego": 0, }, @@ -690,7 +731,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, { Manifest: bundle.Manifest{ - RegoVersion: pointTo(0), + RegoVersion: ®oV0, Roots: &[]string{"c"}, }, Data: map[string]interface{}{}, @@ -714,7 +755,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, regoVersion: ast.RegoV0, // global rego-version in bundles are dropped in favor of the provided rego-version - expGlobalRegoVersion: pointTo(0), + expGlobalRegoVersion: ®oV0, // rego-versions is expected for all modules with different rego-version than the global rego-version expFileRegoVersions: map[string]int{ "/a/test1.rego": 1, @@ -728,7 +769,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"a"}, - RegoVersion: pointTo(0), + RegoVersion: ®oV0, FileRegoVersions: map[string]int{ "a/*": 1, }, @@ -755,7 +796,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { { Manifest: bundle.Manifest{ Roots: &[]string{"b"}, - RegoVersion: pointTo(1), + RegoVersion: ®oV1, FileRegoVersions: map[string]int{ // glob should not affect files with matching path in the other bundle "*/bar/*": 0, @@ -782,7 +823,7 @@ func TestCompilerBundleMergeWithBundleRegoVersion(t *testing.T) { }, }, regoVersion: ast.RegoV0, - expGlobalRegoVersion: pointTo(0), + expGlobalRegoVersion: ®oV0, expFileRegoVersions: map[string]int{ "/a/foo/test.rego": 1, "/a/bar/test.rego": 1, diff --git a/v1/config/config.go b/v1/config/config.go index 2fb27c4f52..09adb556f8 100644 --- a/v1/config/config.go +++ b/v1/config/config.go @@ -17,7 +17,7 @@ import ( "github.com/open-policy-agent/opa/internal/ref" "github.com/open-policy-agent/opa/v1/ast" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) // Config represents the configuration file that OPA can be started with. diff --git a/v1/config/config_test.go b/v1/config/config_test.go index cbf6e29f8f..4ba7c86786 100644 --- a/v1/config/config_test.go +++ b/v1/config/config_test.go @@ -13,7 +13,7 @@ import ( "testing" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) func TestConfigPluginNames(t *testing.T) { diff --git a/v1/doc.go b/v1/doc.go new file mode 100644 index 0000000000..b4991633ba --- /dev/null +++ b/v1/doc.go @@ -0,0 +1,9 @@ +// Copyright 2024 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 v1 implements the v1 API for the Open Policy Agent (OPA). +// The v1 API defaults to enforcing the v1 Rego syntax ([github.com/open-policy-agent/opa/v1/ast.RegoV1]). +// Most packages outside the v1 API are deprecated. These constitute the older v0 API, which defaults to the v0 Rego syntax ([github.com/open-policy-agent/opa/v1/ast.RegoV0]). +// The v0 API is provided as a means to ease transition to OPA 1.0 for 3rd party integrations, see [TODO: LINK TO V0 MIGRATION GUIDE]. +package v1 diff --git a/v1/download/oci_download_unavailable.go b/v1/download/oci_download_unavailable.go index e105d2bd79..ad22fca9be 100644 --- a/v1/download/oci_download_unavailable.go +++ b/v1/download/oci_download_unavailable.go @@ -5,9 +5,9 @@ package download import ( "context" - "github.com/open-policy-agent/opa/ast" - "github.com/open-policy-agent/opa/bundle" - "github.com/open-policy-agent/opa/plugins/rest" + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/bundle" + "github.com/open-policy-agent/opa/v1/plugins/rest" ) func NewOCI(Config, rest.Client, string, string) *OCIDownloader { diff --git a/v1/format/format.go b/v1/format/format.go index b69a563869..c1727099a3 100644 --- a/v1/format/format.go +++ b/v1/format/format.go @@ -33,6 +33,13 @@ type Opts struct { ParserOptions *ast.ParserOptions } +func (o Opts) effectiveRegoVersion() ast.RegoVersion { + if o.RegoVersion == ast.RegoUndefined { + return ast.DefaultRegoVersion + } + return o.RegoVersion +} + // defaultLocationFile is the file name used in `Ast()` for terms // without a location, as could happen when pretty-printing the // results of partial eval. @@ -46,23 +53,29 @@ func Source(filename string, src []byte) ([]byte, error) { } func SourceWithOpts(filename string, src []byte, opts Opts) ([]byte, error) { + regoVersion := opts.effectiveRegoVersion() + var parserOpts ast.ParserOptions if opts.ParserOptions != nil { parserOpts = *opts.ParserOptions } else { - if opts.RegoVersion == ast.RegoV1 { + if regoVersion == ast.RegoV1 { // If the rego version is V1, we need to parse it as such, to allow for future keywords not being imported. // Otherwise, we'll default to the default rego-version. parserOpts.RegoVersion = ast.RegoV1 } } + if parserOpts.RegoVersion == ast.RegoUndefined { + parserOpts.RegoVersion = ast.DefaultRegoVersion + } + module, err := ast.ParseModuleWithOpts(filename, string(src), parserOpts) if err != nil { return nil, err } - if opts.RegoVersion == ast.RegoV0CompatV1 || opts.RegoVersion == ast.RegoV1 { + if regoVersion == ast.RegoV0CompatV1 || regoVersion == ast.RegoV1 { checkOpts := ast.NewRegoCheckOptions() // The module is parsed as v0, so we need to disable checks that will be automatically amended by the AstWithOpts call anyways. checkOpts.RequireIfKeyword = false @@ -154,7 +167,8 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) { o := fmtOpts{} - if opts.RegoVersion == ast.RegoV0CompatV1 || opts.RegoVersion == ast.RegoV1 { + regoVersion := opts.effectiveRegoVersion() + if regoVersion == ast.RegoV0CompatV1 || regoVersion == ast.RegoV1 { o.regoV1 = true o.ifs = true o.contains = true @@ -220,13 +234,13 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) { switch x := x.(type) { case *ast.Module: - if opts.RegoVersion == ast.RegoV1 { + if regoVersion == ast.RegoV1 { x.Imports = filterRegoV1Import(x.Imports) - } else if opts.RegoVersion == ast.RegoV0CompatV1 { + } else if regoVersion == ast.RegoV0CompatV1 { x.Imports = ensureRegoV1Import(x.Imports) } - if opts.RegoVersion == ast.RegoV0CompatV1 || opts.RegoVersion == ast.RegoV1 || moduleIsRegoV1Compatible(x) { + if regoVersion == ast.RegoV0CompatV1 || regoVersion == ast.RegoV1 || moduleIsRegoV1Compatible(x) { x.Imports = future.FilterFutureImports(x.Imports) } else { for kw := range extraFutureKeywordImports { diff --git a/v1/format/format_test.go b/v1/format/format_test.go index 1636b092c5..c12b5ecc72 100644 --- a/v1/format/format_test.go +++ b/v1/format/format_test.go @@ -725,3 +725,144 @@ func prefixWithLineNumbers(bs []byte) []byte { } return []byte(strings.Join(lines, "\n")) } + +func TestSource_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expFormatted string + expErrs []string + }{ + { + note: "v0", // from default rego-version + module: `package test + +p[x] { + x = "a" +}`, + + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1", + module: `package test + +p contains x if { + x = "a" +}`, + expFormatted: `package test + +p contains x if { + x = "a" +} +`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + formatted, err := Source("test.rego", []byte(tc.module)) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%q", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + formattedStr := string(formatted) + if formattedStr != tc.expFormatted { + t.Fatalf("expected %q but got %q", tc.expFormatted, formattedStr) + } + } + }) + } +} + +func TestSourceWithOpts_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + toRegoVersion ast.RegoVersion + module string + expFormatted string + expErrs []string + }{ + { + note: "v0 -> v0", // from default rego-version + toRegoVersion: ast.RegoV0, + module: `package test + +p[x] { + x = "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v0 -> v1", // from default rego-version + toRegoVersion: ast.RegoV1, + module: `package test + +p[x] { + x = "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "v1 -> v1", // from non-default rego-version + toRegoVersion: ast.RegoV1, + module: `package test + +p contains x if { + x = "a" +}`, + expFormatted: `package test + +p contains x if { + x = "a" +} +`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + formatted, err := SourceWithOpts("test.rego", []byte(tc.module), Opts{RegoVersion: tc.toRegoVersion}) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("expected error:\n\n%q\n\nbut got:\n\n%q", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + formattedStr := string(formatted) + if formattedStr != tc.expFormatted { + t.Fatalf("expected %q but got %q", tc.expFormatted, formattedStr) + } + } + }) + } +} diff --git a/v1/ir/encoding/encoding_test.go b/v1/ir/encoding/encoding_test.go index fd31fd4c3d..38395add47 100644 --- a/v1/ir/encoding/encoding_test.go +++ b/v1/ir/encoding/encoding_test.go @@ -12,10 +12,10 @@ import ( func TestRoundTrip(t *testing.T) { + // Note: v1 module c, err := ast.CompileModules(map[string]string{ "test.rego": ` package test - import rego.v1 p if { input.foo == 7 diff --git a/v1/loader/loader_test.go b/v1/loader/loader_test.go index f8d6d46409..cb2ada37f4 100644 --- a/v1/loader/loader_test.go +++ b/v1/loader/loader_test.go @@ -48,6 +48,360 @@ func TestLoadJSON(t *testing.T) { }) } +func TestAll_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package test + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package test + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := All([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestFiltered_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package test + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package test + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + filter := func(string, os.FileInfo, int) bool { + return false + } + + loaded, err := Filtered([]string{moduleFile}, filter) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestRego_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package test + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package test + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := Rego(moduleFile) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Parsed) + } + } + }) + }) + } +} + +func TestAllRegos_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package test + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package test + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := AllRegos([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + +func TestLoadRego_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0", + module: `package test + +p[x] { + x := "a" +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "rego.v1 import", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +}`, + }, + { + note: "v1", // v1 is the default rego-version + module: `package test + +p contains x if { + x := "a" +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "/test.rego": tc.module} + + test.WithTempFS(files, func(rootDir string) { + moduleFile := filepath.Join(rootDir, "test.rego") + loaded, err := NewFileLoader().All([]string{moduleFile}) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected errors but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%s\n\nbut got:\n\n%s", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expected := ast.MustParseModule(files["/test.rego"]) + if !expected.Equal(loaded.Modules[CleanPath(moduleFile)].Parsed) { + t.Fatalf("Expected:\n%v\n\nGot:\n%v", expected, loaded.Modules[moduleFile]) + } + } + }) + }) + } +} + func TestLoadRego(t *testing.T) { files := map[string]string{ diff --git a/v1/plugins/discovery/discovery.go b/v1/plugins/discovery/discovery.go index 9ec5811ca6..e8711d609a 100644 --- a/v1/plugins/discovery/discovery.go +++ b/v1/plugins/discovery/discovery.go @@ -537,6 +537,10 @@ func evaluateBundle(ctx context.Context, id string, info *ast.Term, b *bundleApi compiler := ast.NewCompiler() + if regoVersion := b.RegoVersion(ast.DefaultRegoVersion); regoVersion != ast.RegoUndefined { + compiler = compiler.WithDefaultRegoVersion(regoVersion) + } + if compiler.Compile(modules); compiler.Failed() { return nil, compiler.Errors } diff --git a/v1/plugins/discovery/discovery_test.go b/v1/plugins/discovery/discovery_test.go index c02242b145..59ac1b3b98 100644 --- a/v1/plugins/discovery/discovery_test.go +++ b/v1/plugins/discovery/discovery_test.go @@ -38,7 +38,7 @@ import ( inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test" "github.com/open-policy-agent/opa/v1/topdown/cache" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) const ( diff --git a/v1/plugins/logs/plugin_test.go b/v1/plugins/logs/plugin_test.go index 30385472ca..626b8411aa 100644 --- a/v1/plugins/logs/plugin_test.go +++ b/v1/plugins/logs/plugin_test.go @@ -36,7 +36,7 @@ import ( "github.com/open-policy-agent/opa/v1/topdown/builtins" "github.com/open-policy-agent/opa/v1/topdown/print" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) func TestMain(m *testing.M) { diff --git a/v1/plugins/plugins.go b/v1/plugins/plugins.go index 952a016d48..82813b5e6f 100644 --- a/v1/plugins/plugins.go +++ b/v1/plugins/plugins.go @@ -537,6 +537,7 @@ func (m *Manager) Init(ctx context.Context) error { Bundles: m.initBundles, MaxErrors: m.maxErrors, EnablePrintStatements: m.enablePrintStatements, + ParserOptions: m.parserOptions, }) if err != nil { @@ -939,7 +940,13 @@ func loadCompilerFromStore(ctx context.Context, store storage.Store, txn storage modules[policy] = module } - compiler := ast.NewCompiler().WithEnablePrintStatements(enablePrintStatements) + compiler := ast.NewCompiler(). + WithEnablePrintStatements(enablePrintStatements) + + if popts.RegoVersion != ast.RegoUndefined { + compiler = compiler.WithDefaultRegoVersion(popts.RegoVersion) + } + compiler.Compile(modules) return compiler, nil } diff --git a/v1/plugins/status/metrics.go b/v1/plugins/status/metrics.go index d380b43591..9141ed31c2 100644 --- a/v1/plugins/status/metrics.go +++ b/v1/plugins/status/metrics.go @@ -2,7 +2,7 @@ package status import ( "github.com/open-policy-agent/opa/v1/logging" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" "github.com/prometheus/client_golang/prometheus" ) diff --git a/v1/plugins/status/plugin_test.go b/v1/plugins/status/plugin_test.go index 5fae90b3e2..051ad19629 100644 --- a/v1/plugins/status/plugin_test.go +++ b/v1/plugins/status/plugin_test.go @@ -16,7 +16,6 @@ import ( "testing" "time" - "github.com/open-policy-agent/opa/version" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" @@ -27,6 +26,7 @@ import ( inmem "github.com/open-policy-agent/opa/v1/storage/inmem/test" "github.com/open-policy-agent/opa/v1/util" "github.com/open-policy-agent/opa/v1/util/test" + "github.com/open-policy-agent/opa/v1/version" ) func TestMain(m *testing.M) { diff --git a/v1/rego/rego.go b/v1/rego/rego.go index 5f22a069ee..caa21dec56 100644 --- a/v1/rego/rego.go +++ b/v1/rego/rego.go @@ -629,6 +629,10 @@ type Rego struct { regoVersion ast.RegoVersion } +func (r *Rego) RegoVersion() ast.RegoVersion { + return r.regoVersion +} + // Function represents a built-in function that is callable in Rego. type Function struct { Name string @@ -1281,6 +1285,10 @@ func New(options ...func(r *Rego)) *Rego { if r.target == targetWasm { r.compiler = r.compiler.WithEvalMode(ast.EvalModeIR) } + + if r.regoVersion != ast.RegoUndefined { + r.compiler = r.compiler.WithDefaultRegoVersion(r.regoVersion) + } } if r.store == nil { @@ -2359,7 +2367,8 @@ func (r *Rego) partialResult(ctx context.Context, pCfg *PrepareConfig) (PartialR // Construct module for queries. id := fmt.Sprintf("__partialresult__%s__", ectx.partialNamespace) - module, err := ast.ParseModule(id, "package "+ectx.partialNamespace) + module, err := ast.ParseModuleWithOpts(id, "package "+ectx.partialNamespace, + ast.ParserOptions{RegoVersion: r.regoVersion}) if err != nil { return PartialResult{}, fmt.Errorf("bad partial namespace") } diff --git a/v1/rego/rego_test.go b/v1/rego/rego_test.go index 0304860604..9bab2ed917 100644 --- a/v1/rego/rego_test.go +++ b/v1/rego/rego_test.go @@ -37,6 +37,106 @@ import ( "github.com/open-policy-agent/opa/v1/util/test" ) +func TestRegoEval_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expResult interface{} + expErrs []string + }{ + { + note: "v0 module", // v0 in NOT the default version + module: `package test + +p[x] { + x = ["a", "b", "c"][_] +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + { + note: "import rego.v1", + module: `package test +import rego.v1 + +p contains x if { + some x in ["a", "b", "c"] +}`, + expResult: []string{"a", "b", "c"}, + }, + { + note: "v1 module ", // v1 is the default version + module: `package test + +p contains x if { + some x in ["a", "b", "c"] +}`, + expResult: []string{"a", "b", "c"}, + }, + { + note: "v1 module, v1 compile-time violations", // v1 is the default version + module: `package test +import data.foo +import data.bar as foo + +p contains x if { + some x in ["a", "b", "c"] +}`, + expErrs: []string{ + "test.rego:3: rego_compile_error: import must not shadow import data.foo", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + test.WithTempFS(files, func(root string) { + ctx := context.Background() + + pq, err := New( + Load([]string{root}, nil), + Query("data.test.p"), + ).PrepareForEval(ctx) + + if tc.expErrs != nil { + if err == nil { + t.Fatalf("Expected error but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain %q but got: %v", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + rs, err := pq.Eval(ctx) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(rs) != 1 { + t.Fatalf("Expected exactly one result but got: %v", rs) + } + + if reflect.DeepEqual(rs[0].Expressions[0].Value, tc.expResult) { + t.Fatalf("Expected %v but got: %v", tc.expResult, rs[0].Expressions[0].Value) + } + } + }) + }) + } +} + func assertEval(t *testing.T, r *Rego, expected string) { t.Helper() rs, err := r.Eval(context.Background()) diff --git a/v1/rego/rego_wasmtarget_test.go b/v1/rego/rego_wasmtarget_test.go index 8b41870c8f..ee774a2390 100644 --- a/v1/rego/rego_wasmtarget_test.go +++ b/v1/rego/rego_wasmtarget_test.go @@ -39,7 +39,7 @@ func TestPrepareAndEvalWithWasmTarget(t *testing.T) { mod := ` package test default p = false - p { + p if { input.x == 1 } ` @@ -87,7 +87,7 @@ func TestPrepareAndEvalWithWasmTargetModulesOnCompiler(t *testing.T) { mod := ` package test default p = false - p { + p if { input.x == data.x.p } ` @@ -161,13 +161,13 @@ func TestEvalWithContextTimeout(t *testing.T) { // but calls the topdown function from the wasm instance's execution. // Also, it uses the topdown.Cancel mechanism for cancellation. cidrExpand := `package p -allow { +allow if { net.cidr_expand("1.0.0.0/1") }` // Also a host function, but uses context.Context for cancellation. httpSend := fmt.Sprintf(`package p -allow { +allow if { http.send({"method": "get", "url": "%s", "raise_error": true}) }`, ts.URL) @@ -175,7 +175,7 @@ allow { // This is a natively-implemented (for the wasm target) function that // takes long. numbersRange := `package p -allow { +allow if { numbers.range(1, 1e8)[_] == 1e8 }` diff --git a/v1/rego/testdata/bundle.tar.gz b/v1/rego/testdata/bundle.tar.gz new file mode 100644 index 0000000000..fb6d07aeeb Binary files /dev/null and b/v1/rego/testdata/bundle.tar.gz differ diff --git a/v1/repl/repl.go b/v1/repl/repl.go index d8c67832fc..f4d8274b2d 100644 --- a/v1/repl/repl.go +++ b/v1/repl/repl.go @@ -18,15 +18,13 @@ import ( "strings" "sync" - "github.com/open-policy-agent/opa/v1/bundle" - "github.com/open-policy-agent/opa/v1/compile" - "github.com/open-policy-agent/opa/version" - "github.com/peterh/liner" "github.com/open-policy-agent/opa/internal/future" pr "github.com/open-policy-agent/opa/internal/presentation" "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/bundle" + "github.com/open-policy-agent/opa/v1/compile" "github.com/open-policy-agent/opa/v1/format" "github.com/open-policy-agent/opa/v1/metrics" "github.com/open-policy-agent/opa/v1/profiler" @@ -34,6 +32,7 @@ import ( "github.com/open-policy-agent/opa/v1/storage" "github.com/open-policy-agent/opa/v1/topdown" "github.com/open-policy-agent/opa/v1/topdown/lineage" + "github.com/open-policy-agent/opa/v1/version" ) // REPL represents an instance of the interactive shell. @@ -125,6 +124,7 @@ func New(store storage.Store, historyPath string, output io.Writer, outputFormat errLimit: errLimit, prettyLimit: defaultPrettyLimit, target: compile.TargetRego, + regoVersion: ast.DefaultRegoVersion, } } diff --git a/v1/repl/repl_test.go b/v1/repl/repl_test.go index 72703f4a78..191c3d7ea8 100644 --- a/v1/repl/repl_test.go +++ b/v1/repl/repl_test.go @@ -1167,7 +1167,116 @@ func TestOneShotJSON(t *testing.T) { } } -func TestOneShotV1Compatible(t *testing.T) { +func TestOneShot_DefaultRegoVersion(t *testing.T) { + type action struct { + line string + expOutput string + expErrs []string + } + + tests := []struct { + note string + actions []action + }{ + { + note: "v1 keywords used", + actions: []action{ + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 keywords not used", + actions: []action{ + { + line: "a[2] { true }", + expErrs: []string{ + "rego_parse_error: `if` keyword is required before rule body", + "rego_parse_error: `contains` keyword is required for partial set rules", + }, + }, + }, + }, + { + note: "v1 keywords imported", + actions: []action{ + { + line: "import future.keywords", + }, + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 compile-time violation", + actions: []action{ + { + line: "b if { data := 1; data == 1 }", + expErrs: []string{ + "rego_compile_error: variables must not shadow data (use a different variable name)", + }, + }, + }, + }, + { + note: "rego.v1 imported", + actions: []action{ + { + line: "import rego.v1", + }, + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1 keywords", + actions: []action{ + { + line: "a contains 2 if { true }", + expOutput: "Rule 'a' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + ctx := context.Background() + store := newTestStore() + var buffer bytes.Buffer + repl := newRepl(store, &buffer) + + for _, action := range tc.actions { + err := repl.OneShot(ctx, action.line) + + if len(action.expErrs) != 0 { + if err == nil { + t.Fatalf("Expected error but got: %s", buffer.String()) + } + + for _, e := range action.expErrs { + if !strings.Contains(err.Error(), e) { + t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", e, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + expectOutput(t, buffer.String(), action.expOutput) + } + } + }) + } +} + +func TestOneShot_RegoVersion(t *testing.T) { type action struct { line string expOutput string @@ -1179,7 +1288,7 @@ func TestOneShotV1Compatible(t *testing.T) { regoVersion ast.RegoVersion }{ { - note: "v0.x, keywords used", + note: "v0, keywords used", regoVersion: ast.RegoV0, actions: []action{ { @@ -1189,7 +1298,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v0.x, keywords not used", + note: "v0, keywords not used", regoVersion: ast.RegoV0, actions: []action{ { @@ -1199,7 +1308,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v0.x, keywords imported", + note: "v0, keywords imported", regoVersion: ast.RegoV0, actions: []action{ { @@ -1212,7 +1321,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v0.x, rego.v1 imported", + note: "v0, rego.v1 imported", regoVersion: ast.RegoV0, actions: []action{ { @@ -1225,7 +1334,17 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v1.0, keywords not used", + note: "v0, v1 compile-time violation", + regoVersion: ast.RegoV0, + actions: []action{ + { + line: "b { data := 1; data == 1 }", + expOutput: "Rule 'b' defined in package repl. Type 'show' to see rules.\n", + }, + }, + }, + { + note: "v1, keywords not used", regoVersion: ast.RegoV1, actions: []action{ { @@ -1238,7 +1357,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v1.0, keywords used, not imported", + note: "v1, keywords used, not imported", regoVersion: ast.RegoV1, actions: []action{ { @@ -1248,7 +1367,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v1.0, keywords used, keywords imported", + note: "v1, keywords used, keywords imported", regoVersion: ast.RegoV1, actions: []action{ { @@ -1261,7 +1380,7 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, { - note: "v1.0, keywords used, rego.v1 imported", + note: "v1, keywords used, rego.v1 imported", regoVersion: ast.RegoV1, actions: []action{ { @@ -1273,6 +1392,18 @@ func TestOneShotV1Compatible(t *testing.T) { }, }, }, + { + note: "v1 compile-time violation", + regoVersion: ast.RegoV1, + actions: []action{ + { + line: "b if { data := 1; data == 1 }", + expErrs: []string{ + "rego_compile_error: variables must not shadow data (use a different variable name)", + }, + }, + }, + }, } for _, tc := range tests { @@ -1307,7 +1438,7 @@ func TestOneShotV1Compatible(t *testing.T) { } } -func TestStoredModuleV1Compatible(t *testing.T) { +func TestStoredModule_RegoVersion(t *testing.T) { tests := []struct { note string regoVersion ast.RegoVersion @@ -1317,7 +1448,7 @@ func TestStoredModuleV1Compatible(t *testing.T) { expErrs []string }{ { - note: "v0.x keywords not used", + note: "v0 keywords not used", regoVersion: ast.RegoV0, module: `package example p[2] { 1 == 1 }`, @@ -1325,7 +1456,7 @@ p[2] { 1 == 1 }`, expOutput: "[\n 2\n]\n", }, { - note: "v0.x, keywords not imported but used", + note: "v0, keywords not imported but used", regoVersion: ast.RegoV0, module: `package example p contains 2 if { 1 == 1 }`, @@ -1336,7 +1467,7 @@ p contains 2 if { 1 == 1 }`, }, }, { - note: "v0.x, keywords imported", + note: "v0, keywords imported", regoVersion: ast.RegoV0, module: `package example import future.keywords @@ -1345,7 +1476,7 @@ p contains 2 if { 1 == 1 }`, expOutput: "[\n 2\n]\n", }, { - note: "v0.x, rego.v1 imported", + note: "v0, rego.v1 imported", regoVersion: ast.RegoV0, module: `package example import rego.v1 @@ -1354,7 +1485,15 @@ p contains 2 if { 1 == 1 }`, expOutput: "[\n 2\n]\n", }, { - note: "v1.0, keywords not used", + note: "v0, v1 compile-time violation", + regoVersion: ast.RegoV0, + module: `package example +p { data := 1; data == 1 }`, + line: "data.example.p", + expOutput: "true\n", + }, + { + note: "v1, keywords not used", regoVersion: ast.RegoV1, module: `package example p[2] { 1 == 1 }`, @@ -1365,7 +1504,7 @@ p[2] { 1 == 1 }`, }, }, { - note: "v1.0, keywords not imported", + note: "v1, keywords not imported", regoVersion: ast.RegoV1, module: `package example p contains 2 if { 1 == 1 }`, @@ -1373,7 +1512,7 @@ p contains 2 if { 1 == 1 }`, expOutput: "[\n 2\n]\n", }, { - note: "v1.0, keywords imported", + note: "v1, keywords imported", regoVersion: ast.RegoV1, module: `package example import future.keywords @@ -1382,7 +1521,7 @@ p contains 2 if { 1 == 1 }`, expOutput: "[\n 2\n]\n", }, { - note: "v1.0, rego.v1 imported", + note: "v1, rego.v1 imported", regoVersion: ast.RegoV1, module: `package example import rego.v1 @@ -1390,6 +1529,16 @@ p contains 2 if { 1 == 1 }`, line: "data.example.p", expOutput: "[\n 2\n]\n", }, + { + note: "v1, v1 compile-time violation", + regoVersion: ast.RegoV1, + module: `package example +p if { data := 1; data == 1 }`, + line: "data.example.p", + expErrs: []string{ + "rego_compile_error: variables must not shadow data (use a different variable name)", + }, + }, } for _, tc := range tests { diff --git a/v1/repl/repl_wasmtarget_test.go b/v1/repl/repl_wasmtarget_test.go index 1b5ed8ff39..02c92ddad2 100644 --- a/v1/repl/repl_wasmtarget_test.go +++ b/v1/repl/repl_wasmtarget_test.go @@ -41,7 +41,7 @@ func TestReplWasmTarget(t *testing.T) { t.Fatalf("Unexpected error: %v", err) } - repl.OneShot(ctx, `p = true { input.foo = "bar" }`) + repl.OneShot(ctx, `p = true if { input.foo = "bar" }`) buffer.Reset() repl.OneShot(ctx, "p") diff --git a/v1/runtime/runtime.go b/v1/runtime/runtime.go index 8e49af39f5..7e9357bf35 100644 --- a/v1/runtime/runtime.go +++ b/v1/runtime/runtime.go @@ -23,8 +23,6 @@ import ( "github.com/fsnotify/fsnotify" "github.com/gorilla/mux" - "github.com/open-policy-agent/opa/v1/storage/inmem" - "github.com/open-policy-agent/opa/version" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace" "go.opentelemetry.io/otel/propagation" @@ -55,8 +53,10 @@ import ( "github.com/open-policy-agent/opa/v1/server" "github.com/open-policy-agent/opa/v1/storage" "github.com/open-policy-agent/opa/v1/storage/disk" + "github.com/open-policy-agent/opa/v1/storage/inmem" "github.com/open-policy-agent/opa/v1/tracing" "github.com/open-policy-agent/opa/v1/util" + "github.com/open-policy-agent/opa/v1/version" ) var ( diff --git a/v1/sdk/opa.go b/v1/sdk/opa.go index 3494863a5c..e309a8365e 100644 --- a/v1/sdk/opa.go +++ b/v1/sdk/opa.go @@ -33,7 +33,7 @@ import ( "github.com/open-policy-agent/opa/v1/topdown/cache" "github.com/open-policy-agent/opa/v1/topdown/print" "github.com/open-policy-agent/opa/v1/util" - "github.com/open-policy-agent/opa/version" + "github.com/open-policy-agent/opa/v1/version" ) // OPA represents an instance of the policy engine. OPA can be started with diff --git a/v1/sdk/opa_test.go b/v1/sdk/opa_test.go index 7ad12282c4..68a0074296 100644 --- a/v1/sdk/opa_test.go +++ b/v1/sdk/opa_test.go @@ -18,7 +18,6 @@ import ( "testing" "time" - "github.com/open-policy-agent/opa/version" "github.com/prometheus/client_golang/prometheus" promdto "github.com/prometheus/client_model/go" @@ -43,8 +42,80 @@ import ( "github.com/open-policy-agent/opa/v1/topdown/builtins" "github.com/open-policy-agent/opa/v1/topdown/lineage" "github.com/open-policy-agent/opa/v1/util/test" + "github.com/open-policy-agent/opa/v1/version" ) +func TestDefaultRegoVersion(t *testing.T) { + + ctx := context.Background() + + server := sdktest.MustNewServer( + sdktest.RawBundles(true), + sdktest.MockBundle("/bundles/bundle.tar.gz", map[string]string{ + // v1 module + "main.rego": ` +package system + +main if { + "a" in p +} + +p contains x if { + x = "a" +} + +str = "foo" + +loopback = input +`, + }), + ) + + defer server.Stop() + + config := fmt.Sprintf(`{ + "services": { + "test": { + "url": %q + } + }, + "bundles": { + "test": { + "resource": "/bundles/bundle.tar.gz" + } + } + }`, server.URL()) + + opa, err := sdk.New(ctx, sdk.Options{ + Config: strings.NewReader(config), + }) + if err != nil { + t.Fatal(err) + } + + defer opa.Stop(ctx) + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{}); err != nil { + t.Fatal(err) + } else if decision, ok := result.Result.(bool); !ok || !decision { + t.Fatal("expected true but got:", decision, ok) + } + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{Path: "/system/str"}); err != nil { + t.Fatal(err) + } else if decision, ok := result.Result.(string); !ok || decision != "foo" { + t.Fatal(`expected "foo" but got:`, decision) + } + + exp := map[string]interface{}{"foo": "bar"} + + if result, err := opa.Decision(ctx, sdk.DecisionOptions{Path: "/system/loopback", Input: map[string]interface{}{"foo": "bar"}}); err != nil { + t.Fatal(err) + } else if !reflect.DeepEqual(result.Result, exp) { + t.Fatalf("expected %v but got %v", exp, result.Result) + } +} + // Plugin creates an empty plugin to test plugin initialization and shutdown type plugin struct { manager *plugins.Manager diff --git a/v1/sdk/test/test.go b/v1/sdk/test/test.go index e14ed66ff7..ddacff741b 100644 --- a/v1/sdk/test/test.go +++ b/v1/sdk/test/test.go @@ -58,12 +58,21 @@ func Ready(ch chan struct{}) func(*Server) error { } } +// ParserOptions sets the ast.ParserOptions to use when parsing modules when preparing bundles. +func ParserOptions(popts ast.ParserOptions) func(*Server) error { + return func(s *Server) error { + s.parserOptions = popts + return nil + } +} + // Server provides a mock HTTP server for testing the SDK and integrations. type Server struct { - server *httptest.Server - ready chan struct{} - bundles map[string]map[string]string - rawBundles bool + server *httptest.Server + ready chan struct{} + bundles map[string]map[string]string + rawBundles bool + parserOptions ast.ParserOptions } // MustNewServer returns a new Server for test purposes or panics if an error occurs. @@ -100,6 +109,10 @@ func RawBundles(raw bool) func(*Server) error { } } +func (s *Server) ParserOptions() ast.ParserOptions { + return s.parserOptions +} + // WithTestBundle adds a bundle to the server at the specified endpoint. func (s *Server) WithTestBundle(endpoint string, policies map[string]string) *Server { s.bundles[endpoint] = policies @@ -121,7 +134,7 @@ func (s *Server) buildBundles(ref string, policies map[string]string) error { // Prepare the modules to include in the bundle. Sort them so bundles are deterministic. modules := make([]bundle.ModuleFile, 0, len(policies)) for url, str := range policies { - module, err := ast.ParseModule(url, str) + module, err := ast.ParseModuleWithOpts(url, str, s.parserOptions) if err != nil { return fmt.Errorf("failed to parse module: %v", err) } @@ -402,7 +415,7 @@ func (s *Server) handleBundles(w http.ResponseWriter, r *http.Request) { return } case strings.HasSuffix(url, ".rego"): - module, err := ast.ParseModule(url, str) + module, err := ast.ParseModuleWithOpts(url, str, s.parserOptions) if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) diff --git a/v1/server/server.go b/v1/server/server.go index 2f1dcb5646..44bf4852be 100644 --- a/v1/server/server.go +++ b/v1/server/server.go @@ -26,7 +26,6 @@ import ( serverDecodingPlugin "github.com/open-policy-agent/opa/v1/plugins/server/decoding" serverEncodingPlugin "github.com/open-policy-agent/opa/v1/plugins/server/encoding" - "github.com/open-policy-agent/opa/version" "github.com/gorilla/mux" "go.opentelemetry.io/otel/attribute" @@ -56,6 +55,7 @@ import ( "github.com/open-policy-agent/opa/v1/topdown/lineage" "github.com/open-policy-agent/opa/v1/tracing" "github.com/open-policy-agent/opa/v1/util" + "github.com/open-policy-agent/opa/v1/version" ) // AuthenticationScheme enumerates the supported authentication schemes. The @@ -1375,7 +1375,7 @@ func (s *Server) v1CompilePost(w http.ResponseWriter, r *http.Request) { return } - request, reqErr := readInputCompilePostV1(body) + request, reqErr := readInputCompilePostV1(body, s.manager.ParserOptions()) if reqErr != nil { writer.Error(w, http.StatusBadRequest, reqErr) return @@ -2245,7 +2245,7 @@ func (s *Server) v1QueryGet(w http.ResponseWriter, r *http.Request) { } qStr := qStrs[len(qStrs)-1] - parsedQuery, err := validateQuery(qStr) + parsedQuery, err := validateQuery(qStr, s.manager.ParserOptions()) if err != nil { switch err := err.(type) { case ast.Errors: @@ -2303,7 +2303,7 @@ func (s *Server) v1QueryPost(w http.ResponseWriter, r *http.Request) { return } qStr := request.Query - parsedQuery, err := validateQuery(qStr) + parsedQuery, err := validateQuery(qStr, s.manager.ParserOptions()) if err != nil { switch err := err.(type) { case ast.Errors: @@ -2703,8 +2703,8 @@ func stringPathToRef(s string) (r ast.Ref) { return r } -func validateQuery(query string) (ast.Body, error) { - return ast.ParseBody(query) +func validateQuery(query string, opts ast.ParserOptions) (ast.Body, error) { + return ast.ParseBodyWithOpts(query, opts) } func getBoolParam(url *url.URL, name string, ifEmpty bool) bool { @@ -2859,7 +2859,7 @@ type compileRequestOptions struct { DisableInlining []string } -func readInputCompilePostV1(reqBytes []byte) (*compileRequest, *types.ErrorV1) { +func readInputCompilePostV1(reqBytes []byte, queryParserOptions ast.ParserOptions) (*compileRequest, *types.ErrorV1) { var request types.CompileRequestV1 err := util.NewJSONDecoder(bytes.NewBuffer(reqBytes)).Decode(&request) @@ -2867,7 +2867,7 @@ func readInputCompilePostV1(reqBytes []byte) (*compileRequest, *types.ErrorV1) { return nil, types.NewErrorV1(types.CodeInvalidParameter, "error(s) occurred while decoding request: %v", err.Error()) } - query, err := ast.ParseBody(request.Query) + query, err := ast.ParseBodyWithOpts(request.Query, queryParserOptions) if err != nil { switch err := err.(type) { case ast.Errors: diff --git a/v1/server/server_test.go b/v1/server/server_test.go index 3070cbb089..17482c3cc9 100644 --- a/v1/server/server_test.go +++ b/v1/server/server_test.go @@ -39,7 +39,6 @@ import ( "time" "github.com/gorilla/mux" - "github.com/open-policy-agent/opa/version" "github.com/open-policy-agent/opa/internal/distributedtracing" "github.com/open-policy-agent/opa/internal/prometheus" @@ -59,6 +58,7 @@ import ( "github.com/open-policy-agent/opa/v1/storage/inmem" "github.com/open-policy-agent/opa/v1/util" "github.com/open-policy-agent/opa/v1/util/test" + "github.com/open-policy-agent/opa/v1/version" prom "github.com/prometheus/client_golang/prometheus" ) @@ -879,23 +879,61 @@ func Test405StatusCodev0(t *testing.T) { func TestCompileV1(t *testing.T) { t.Parallel() - mod := `package test - import rego.v1 + v0mod := `package test + + p { + input.x = 1 + } + + q { + data.a[i] = input.x + } + + default r = true + + r { input.x = 1 } + + custom_func(x) { data.a[i] == x } + + s { custom_func(input.x) } + ` + v1mod := `package test + p if { input.x = 1 } - + q if { data.a[i] = input.x } - + default r = true - + r if { input.x = 1 } - + custom_func(x) if { data.a[i] == x } + + s if { custom_func(input.x) } + ` + v0v1mod := `package test + import rego.v1 + + p if { + input.x = 1 + } + + q if { + data.a[i] = input.x + } + + default r = true + + r if { input.x = 1 } + + custom_func(x) if { data.a[i] == x } + s if { custom_func(input.x) } ` @@ -903,18 +941,59 @@ func TestCompileV1(t *testing.T) { return fmt.Sprintf(`{"result": {"queries": [%v]}}`, string(util.MustMarshalJSON(ast.MustParseBody(s)))) } - expQueryAndSupport := func(q string, m string) string { - return fmt.Sprintf(`{"result": {"queries": [%v], "support": [%v]}}`, string(util.MustMarshalJSON(ast.MustParseBody(q))), string(util.MustMarshalJSON(ast.MustParseModule(m)))) + expError := func(s string) string { + return fmt.Sprintf(`{ + "code": "invalid_parameter", + "errors": [ + %s + ], + "message": "error(s) occurred while compiling module(s)" + }`, s) + } + + expQueryAndSupport := func(q string, m string, rv ast.RegoVersion) string { + opts := ast.ParserOptions{RegoVersion: rv} + return fmt.Sprintf(`{"result": {"queries": [%v], "support": [%v]}}`, + string(util.MustMarshalJSON(ast.MustParseBodyWithOpts(q, opts))), + string(util.MustMarshalJSON(ast.MustParseModuleWithOpts(m, opts)))) } tests := []struct { - note string - trs []tr + note string + trs []tr + regoVersion ast.RegoVersion }{ + { + note: "v1 keyword in query", + trs: []tr{ + {http.MethodPost, "/compile", `{ + "unknowns": ["input"], + "query": "42 in input.x" + }`, 200, expQuery("42 in input.x")}, + }, + }, + { + note: "v1 keyword in query (v0 rego-version)", + regoVersion: ast.RegoV0, + trs: []tr{ + {http.MethodPost, "/compile", `{ + "unknowns": ["input"], + "query": "42 in input.x" + }`, 400, expError(fmt.Sprintf(`{ + "code": "rego_unsafe_var_error", + "location": { + "col": 4, + "file": "", + "row": 1 + }, + "message": "%s" + }`, "var in is unsafe (hint: `import future.keywords.in` to import a future keyword)"))}, + }, + }, { note: "basic", trs: []tr{ - {http.MethodPut, "/policies/test", mod, 200, ""}, + {http.MethodPut, "/policies/test", v1mod, 200, ""}, {http.MethodPost, "/compile", `{ "unknowns": ["input"], "query": "data.test.p = true" @@ -934,7 +1013,7 @@ func TestCompileV1(t *testing.T) { { note: "data", trs: []tr{ - {http.MethodPut, "/policies/test", mod, 200, ""}, + {http.MethodPut, "/policies/test", v1mod, 200, ""}, {http.MethodPost, "/compile", `{ "unknowns": ["data.a"], "input": { @@ -955,23 +1034,75 @@ func TestCompileV1(t *testing.T) { { note: "support", trs: []tr{ - {http.MethodPut, "/policies/test", mod, 200, ""}, + {http.MethodPut, "/policies/test", v1mod, 200, ""}, {http.MethodPost, "/compile", `{ "query": "data.test.r = true" }`, 200, expQueryAndSupport( `data.partial.test.r = true`, `package partial.test - import rego.v1 r if { input.x = 1 } default r = true - `)}, + `, + ast.DefaultRegoVersion)}, + }, + }, + { + note: "support (v0 rego-version)", + regoVersion: ast.RegoV0, + trs: []tr{ + {http.MethodPut, "/policies/test", v0mod, 200, ""}, + {http.MethodPost, "/compile", `{ + "query": "data.test.r = true" + }`, 200, expQueryAndSupport( + `data.partial.test.r = true`, + `package partial.test + + r { input.x = 1 } + default r = true + `, + ast.RegoV0)}, + }, + }, + { + note: "support (v1 rego-version)", + regoVersion: ast.RegoV1, + trs: []tr{ + {http.MethodPut, "/policies/test", v1mod, 200, ""}, + {http.MethodPost, "/compile", `{ + "query": "data.test.r = true" + }`, 200, expQueryAndSupport( + `data.partial.test.r = true`, + `package partial.test + + r if { input.x = 1 } + default r = true + `, + ast.RegoV1)}, + }, + }, + { + note: "support (import rego.v1)", + regoVersion: ast.RegoV0, + trs: []tr{ + {http.MethodPut, "/policies/test", v0v1mod, 200, ""}, + // NOTE: v0 support rules don't get the rego.v1 import applied + {http.MethodPost, "/compile", `{ + "query": "data.test.r = true" + }`, 200, expQueryAndSupport( + `data.partial.test.r = true`, + `package partial.test + + r { input.x = 1 } + default r = true + `, + ast.RegoV0)}, }, }, { note: "function without disableInlining", trs: []tr{ - {http.MethodPut, "/policies/test", mod, 200, ""}, + {http.MethodPut, "/policies/test", v1mod, 200, ""}, {http.MethodPost, "/compile", `{ "unknowns": ["data.a"], "query": "data.test.s = true", @@ -982,7 +1113,7 @@ func TestCompileV1(t *testing.T) { { note: "function with disableInlining", trs: []tr{ - {http.MethodPut, "/policies/test", mod, 200, ""}, + {http.MethodPut, "/policies/test", v1mod, 200, ""}, {http.MethodPost, "/compile", `{ "unknowns": ["data.a"], "query": "data.test.s = true", @@ -991,10 +1122,11 @@ func TestCompileV1(t *testing.T) { }`, 200, expQueryAndSupport( `data.partial.test.s = true`, `package partial.test - import rego.v1 + s if { data.partial.test.custom_func(1) } custom_func(__local0__2) if { data.a[i2] = __local0__2 } - `)}, + `, + ast.DefaultRegoVersion)}, }, }, { @@ -1035,7 +1167,14 @@ func TestCompileV1(t *testing.T) { for _, tc := range tests { t.Run(tc.note, func(t *testing.T) { - executeRequests(t, tc.trs) + if tc.regoVersion != ast.RegoUndefined { + executeRequests(t, tc.trs, variant{ + name: tc.regoVersion.String(), + opts: []any{plugins.WithParserOptions(ast.ParserOptions{RegoVersion: tc.regoVersion})}, + }) + } else { + executeRequests(t, tc.trs) + } }) } } @@ -3897,7 +4036,7 @@ func TestStatusV1MetricsWithSystemAuthzPolicy(t *testing.T) { }(logging.NewNoOpLogger()) prom := prometheus.New(inner, logger, []float64{1e-6, 5e-6, 1e-5, 5e-5, 1e-4, 5e-4, 1e-3, 0.01, 0.1, 1}) - serverOpts := []func(s *Server){func(s *Server) { s.WithAuthorization(AuthorizationBasic) }, func(s *Server) { s.WithMetrics(prom) }} + serverOpts := []any{func(s *Server) { s.WithAuthorization(AuthorizationBasic) }, func(s *Server) { s.WithMetrics(prom) }} f := newFixtureWithStore(t, store, serverOpts...) @@ -4379,50 +4518,93 @@ func TestDecisionLogErrorMessage(t *testing.T) { func TestQueryV1(t *testing.T) { t.Parallel() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - test.WithTempFS(nil, func(root string) { - disk, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{Dir: root}) - if err != nil { - t.Fatal(err) - } - defer disk.Close(ctx) + tests := []struct { + note string + regoVersion ast.RegoVersion + query string + expErr bool + }{ + { + note: "v0", + regoVersion: ast.RegoV0, + query: "a=[1,2,3]%3Ba[i]=x", + }, + { + note: "v0, v1 keywords in query", + regoVersion: ast.RegoV0, + query: "a=[1,2,3]%3Bsome+i,+x+in+a", + expErr: true, + }, + { + note: "v1", + regoVersion: ast.RegoV1, + query: "a=[1,2,3]%3Bsome+i,+x+in+a", + }, + { + note: "default rego-version", // v1 + query: "a=[1,2,3]%3Bsome+i,+x+in+a", + }, + } - f := newFixtureWithStore(t, disk) - get := newReqV1(http.MethodGet, `/query?q=a=[1,2,3]%3Ba[i]=x&metrics`, "") - f.server.Handler.ServeHTTP(f.recorder, get) + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + test.WithTempFS(nil, func(root string) { + disk, err := disk.New(ctx, logging.NewNoOpLogger(), nil, disk.Options{Dir: root}) + if err != nil { + t.Fatal(err) + } + defer disk.Close(ctx) - if f.recorder.Code != 200 { - t.Fatalf("Expected success but got %v", f.recorder) - } + var opts []any + if tc.regoVersion != ast.RegoUndefined { + opts = append(opts, plugins.WithParserOptions(ast.ParserOptions{RegoVersion: tc.regoVersion})) + } - var expected types.QueryResponseV1 - err = util.UnmarshalJSON([]byte(`{ + f := newFixtureWithStore(t, disk, opts...) + get := newReqV1(http.MethodGet, fmt.Sprintf(`/query?q=%s&metrics`, tc.query), "") + f.server.Handler.ServeHTTP(f.recorder, get) + + if tc.expErr { + if f.recorder.Code != 400 { + t.Fatalf("Expected error but got %v", f.recorder) + } + } else { + if f.recorder.Code != 200 { + t.Fatalf("Expected success but got %v", f.recorder) + } + + var expected types.QueryResponseV1 + err = util.UnmarshalJSON([]byte(`{ "result": [{"a":[1,2,3],"i":0,"x":1},{"a":[1,2,3],"i":1,"x":2},{"a":[1,2,3],"i":2,"x":3}] }`), &expected) - if err != nil { - panic(err) - } + if err != nil { + panic(err) + } - var result types.QueryResponseV1 - err = util.UnmarshalJSON(f.recorder.Body.Bytes(), &result) - if err != nil { - t.Fatalf("Unexpected error while unmarshalling result: %v", err) - } + var result types.QueryResponseV1 + err = util.UnmarshalJSON(f.recorder.Body.Bytes(), &result) + if err != nil { + t.Fatalf("Unexpected error while unmarshalling result: %v", err) + } - assertMetricsExist(t, result.Metrics, []string{ - "counter_disk_read_keys", - "timer_rego_query_compile_ns", - "timer_rego_query_eval_ns", - // "timer_server_handler_ns", // TODO(sr): we're not consistent about timing this? - "timer_disk_read_ns", + assertMetricsExist(t, result.Metrics, []string{ + "counter_disk_read_keys", + "timer_rego_query_compile_ns", + "timer_rego_query_eval_ns", + // "timer_server_handler_ns", // TODO(sr): we're not consistent about timing this? + "timer_disk_read_ns", + }) + + result.Metrics = nil + if !reflect.DeepEqual(result, expected) { + t.Fatalf("Expected:\n\n%v\n\nbut got:\n\n%v", expected, result) + } + } + }) }) - - result.Metrics = nil - if !reflect.DeepEqual(result, expected) { - t.Fatalf("Expected %v but got: %v", expected, result) - } - }) + } } func TestBadQueryV1(t *testing.T) { @@ -5122,9 +5304,17 @@ func newFixtureWithConfig(t *testing.T, config string, opts ...func(*Server)) *f } } -func newFixtureWithStore(t *testing.T, store storage.Store, opts ...func(*Server)) *fixture { +func newFixtureWithStore(t *testing.T, store storage.Store, opts ...any) *fixture { ctx := context.Background() - m, err := plugins.New([]byte{}, "test", store) + + var mOpts []func(*plugins.Manager) + for _, opt := range opts { + if opt, ok := opt.(func(*plugins.Manager)); ok { + mOpts = append(mOpts, opt) + } + } + + m, err := plugins.New([]byte{}, "test", store, mOpts...) if err != nil { panic(err) } @@ -5137,9 +5327,13 @@ func newFixtureWithStore(t *testing.T, store storage.Store, opts ...func(*Server WithAddresses([]string{"localhost:8182"}). WithStore(store). WithManager(m) + for _, opt := range opts { - opt(server) + if opt, ok := opt.(func(*Server)); ok { + opt(server) + } } + server, err = server.Init(ctx) if err != nil { panic(err) @@ -5229,6 +5423,16 @@ type variant struct { func executeRequests(t *testing.T, reqs []tr, variants ...variant) { t.Helper() + + if len(variants) == 0 { + f := newFixture(t) + for i, req := range reqs { + if err := f.v1(req.method, req.path, req.body, req.code, req.resp); err != nil { + t.Errorf("Unexpected response on request %d: %v", i+1, err) + } + } + } + for _, v := range variants { t.Run(v.name, func(t *testing.T) { f := newFixture(t, v.opts...) diff --git a/v1/tester/runner.go b/v1/tester/runner.go index 1568366cdc..5df0a2d5ca 100644 --- a/v1/tester/runner.go +++ b/v1/tester/runner.go @@ -126,15 +126,24 @@ type Runner struct { filter string target string // target type (wasm, rego, etc.) customBuiltins []*Builtin + defaultRegoVersion ast.RegoVersion } // NewRunner returns a new runner. func NewRunner() *Runner { return &Runner{ - timeout: 5 * time.Second, + timeout: 5 * time.Second, + defaultRegoVersion: ast.DefaultRegoVersion, } } +// SetDefaultRegoVersion sets the default Rego version to use when compiling modules. +// Not applicable if a custom [ast.Compiler] is set via [SetCompiler]. +func (r *Runner) SetDefaultRegoVersion(v ast.RegoVersion) *Runner { + r.defaultRegoVersion = v + return r +} + // SetCompiler sets the compiler used by the runner. func (r *Runner) SetCompiler(compiler *ast.Compiler) *Runner { r.compiler = compiler @@ -303,7 +312,8 @@ func (r *Runner) runTests(ctx context.Context, txn storage.Transaction, enablePr r.compiler = ast.NewCompiler(). WithCapabilities(capabilities). - WithEnablePrintStatements(enablePrintStatements) + WithEnablePrintStatements(enablePrintStatements). + WithDefaultRegoVersion(r.defaultRegoVersion) } // rewrite duplicate test_* rule names as we compile modules @@ -647,6 +657,10 @@ func LoadBundles(args []string, filter loader.Filter) (map[string]*bundle.Bundle // LoadBundlesWithRegoVersion will load the given args as bundles, either tarball or directory is OK. // Bundles are parsed in accordance with the given RegoVersion. func LoadBundlesWithRegoVersion(args []string, filter loader.Filter, regoVersion ast.RegoVersion) (map[string]*bundle.Bundle, error) { + if regoVersion == ast.RegoUndefined { + regoVersion = ast.DefaultRegoVersion + } + bundles := map[string]*bundle.Bundle{} for _, bundleDir := range args { b, err := loader.NewFileLoader(). diff --git a/v1/tester/runner_test.go b/v1/tester/runner_test.go index efc41c62fb..6f4cf24aba 100644 --- a/v1/tester/runner_test.go +++ b/v1/tester/runner_test.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "reflect" + "strings" "testing" "time" @@ -16,6 +17,7 @@ import ( "github.com/open-policy-agent/opa/v1/cover" "github.com/open-policy-agent/opa/v1/rego" "github.com/open-policy-agent/opa/v1/storage" + "github.com/open-policy-agent/opa/v1/storage/inmem" "github.com/open-policy-agent/opa/v1/tester" "github.com/open-policy-agent/opa/v1/topdown" "github.com/open-policy-agent/opa/v1/types" @@ -703,3 +705,175 @@ func TestRunnerWithBuiltinErrors(t *testing.T) { }) } } + +func TestLoad_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module string + expErrs []string + }{ + { + note: "v0 module", // NOT default rego-version + module: `package test + +p[x] { + x = "a" +} + +test_p { + p["a"] +}`, + expErrs: []string{ + "test.rego:3: rego_parse_error: `if` keyword is required before rule body", + "test.rego:3: rego_parse_error: `contains` keyword is required for partial set rules", + "test.rego:7: rego_parse_error: `if` keyword is required before rule body", + }, + }, + { + note: "import rego.v1", + module: `package test +import rego.v1 + +p contains x if { + x := "a" +} + +test_p if { + "a" in p +}`, + }, + { + note: "v1 module", // default rego-version + module: `package test + +p contains x if { + x := "a" +} + +test_p if { + "a" in p +}`, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + files := map[string]string{ + "test.rego": tc.module, + } + + test.WithTempFS(files, func(root string) { + modules, store, err := tester.Load([]string{root}, nil) + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if modules == nil { + t.Fatalf("Expected modules to be non-nil") + } + + if store == nil { + t.Fatalf("Expected store to be non-nil") + } + } + }) + }) + } +} + +func TestRun_DefaultRegoVersion(t *testing.T) { + tests := []struct { + note string + module ast.Module + expErrs []string + }{ + { + note: "no v1 violations", + module: ast.Module{ + Package: ast.MustParsePackage(`package test`), + Rules: []*ast.Rule{ + ast.MustParseRule(`p[x] { x = "a" }`), + ast.MustParseRule(`test_p { p["a"] }`), + }, + }, + }, + { + note: "v1 violations", + module: ast.Module{ + Package: ast.MustParsePackage(`package test`), + Imports: ast.MustParseImports(` + import data.foo + import data.bar as foo + `), + Rules: []*ast.Rule{ + ast.MustParseRule(`p[x] { x = "a" }`), + ast.MustParseRule(`test_p { p["a"] }`), + }, + }, + expErrs: []string{ + "rego_compile_error: import must not shadow import data.foo", + }, + }, + } + + for _, tc := range tests { + t.Run(tc.note, func(t *testing.T) { + ctx := context.Background() + + modules := map[string]*ast.Module{ + "test": &tc.module, + } + + store := inmem.New() + txn := storage.NewTransactionOrDie(ctx, store) + defer store.Abort(ctx, txn) + + runner := tester.NewRunner(). + SetStore(store). + SetModules(modules). + SetTimeout(10 * time.Second) + + ch, err := runner.RunTests(ctx, txn) + + if len(tc.expErrs) > 0 { + if err == nil { + t.Fatalf("Expected error but got nil") + } + + for _, expErr := range tc.expErrs { + if !strings.Contains(err.Error(), expErr) { + t.Fatalf("Expected error to contain:\n\n%q\n\nbut got:\n\n%v", expErr, err) + } + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + var rs []*tester.Result + for r := range ch { + rs = append(rs, r) + } + + if len(rs) != 1 { + t.Fatalf("Expected exactly one result but got: %v", rs) + } + + if rs[0].Fail { + t.Fatalf("Expected test to pass but it failed") + } + } + }) + } +} diff --git a/v1/topdown/query.go b/v1/topdown/query.go index 338089ba54..6029ee7212 100644 --- a/v1/topdown/query.go +++ b/v1/topdown/query.go @@ -481,7 +481,11 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support [] } } - for i := range support { + for i, m := range support { + if regoVersion := q.compiler.DefaultRegoVersion(); regoVersion != ast.RegoUndefined { + ast.SetModuleRegoVersion(m, q.compiler.DefaultRegoVersion()) + } + sort.Slice(support[i].Rules, func(j, k int) bool { return support[i].Rules[j].Compare(support[i].Rules[k]) < 0 }) diff --git a/v1/topdown/testdata/.gitignore b/v1/topdown/testdata/.gitignore new file mode 100644 index 0000000000..e7fe15b29b --- /dev/null +++ b/v1/topdown/testdata/.gitignore @@ -0,0 +1,4 @@ +*.srl +*.cnf +csr.pem +ca-key.pem diff --git a/v1/version/version.go b/v1/version/version.go new file mode 100644 index 0000000000..3f1e4329cd --- /dev/null +++ b/v1/version/version.go @@ -0,0 +1,49 @@ +// Copyright 2016 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 version contains version information that is set at build time. +package version + +import ( + "runtime" + "runtime/debug" +) + +// Version is the canonical version of OPA. +var Version = "0.71.0-dev" + +// GoVersion is the version of Go this was built with +var GoVersion = runtime.Version() + +// Platform is the runtime OS and architecture of this OPA binary +var Platform = runtime.GOOS + "/" + runtime.GOARCH + +// Additional version information that is displayed by the "version" command and used to +// identify the version of running instances of OPA. +var ( + Vcs = "" + 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" + } +} diff --git a/v1/version/wasm.go b/v1/version/wasm.go new file mode 100644 index 0000000000..c274a7827a --- /dev/null +++ b/v1/version/wasm.go @@ -0,0 +1,13 @@ +// 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 version + +import "github.com/open-policy-agent/opa/internal/rego/opa" + +// WasmRuntimeAvailable indicates if a wasm runtime is available in this OPA. +func WasmRuntimeAvailable() bool { + _, err := opa.LookupEngine("wasm") + return err == nil +} diff --git a/version/doc.go b/version/doc.go new file mode 100644 index 0000000000..5635dedc93 --- /dev/null +++ b/version/doc.go @@ -0,0 +1,8 @@ +// Copyright 2024 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. + +// Deprecated: This package is intended for older projects transitioning from OPA v0.x and will remain for the lifetime of OPA v1.x, but its use is not recommended. +// For newer features and behaviours, such as defaulting to the Rego v1 syntax, use the corresponding components in the [github.com/open-policy-agent/opa/v1] package instead. +// See https://www.openpolicyagent.org/docs/latest/v0-compatibility/ for more information. +package version diff --git a/version/version.go b/version/version.go index 3f1e4329cd..bb64d8172c 100644 --- a/version/version.go +++ b/version/version.go @@ -6,44 +6,22 @@ package version import ( - "runtime" - "runtime/debug" + v1 "github.com/open-policy-agent/opa/v1/version" ) // Version is the canonical version of OPA. -var Version = "0.71.0-dev" +var Version = v1.Version // GoVersion is the version of Go this was built with -var GoVersion = runtime.Version() +var GoVersion = v1.GoVersion // Platform is the runtime OS and architecture of this OPA binary -var Platform = runtime.GOOS + "/" + runtime.GOARCH +var Platform = v1.Platform // Additional version information that is displayed by the "version" command and used to // identify the version of running instances of OPA. var ( - Vcs = "" - Timestamp = "" - Hostname = "" + Vcs = v1.Vcs + Timestamp = v1.Timestamp + Hostname = v1.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" - } -} diff --git a/version/wasm.go b/version/wasm.go index c274a7827a..9f68bbbb18 100644 --- a/version/wasm.go +++ b/version/wasm.go @@ -4,10 +4,11 @@ package version -import "github.com/open-policy-agent/opa/internal/rego/opa" +import ( + v1 "github.com/open-policy-agent/opa/v1/version" +) // WasmRuntimeAvailable indicates if a wasm runtime is available in this OPA. func WasmRuntimeAvailable() bool { - _, err := opa.LookupEngine("wasm") - return err == nil + return v1.WasmRuntimeAvailable() }