mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
compile: add support for "any value at all", as IS NOT NULL (#7998)
* compile: support "equal to whatever" as "IS NOT NULL" Fixes https://github.com/open-policy-agent/opa/issues/7996. This only deals with SQL targets for starters. This issue also came out of the discussion with @huntkalio. * compile: expand support for "_ = <unknown>" to prisma It seems like the translation is pretty straightforward here, so let's include it. Reference: https://www.prisma.io/docs/orm/prisma-client/queries/filtering-and-sorting#filter-for-non-null-fields * docs/compile: mention fragment addition Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
@@ -99,6 +99,31 @@ include if input.fruits.name != input.fruits.colour
|
||||
SQL target: `WHERE name <> colour`
|
||||
:::
|
||||
|
||||
:::info "Is Anything"
|
||||
For SQL and UCAST/Prisma, it's valid to assert that a field exists by unifying it with a wildcard:
|
||||
```rego
|
||||
package filters
|
||||
|
||||
include if input.fruits.price = _
|
||||
```
|
||||
SQL target: `WHERE name IS NOT NULL`
|
||||
|
||||
A more common way to do this would be function definition shorthands, like
|
||||
```rego
|
||||
package filters
|
||||
|
||||
include if {
|
||||
some pat in data.filter.patterns
|
||||
matches(pat, input.fruit.name)
|
||||
}
|
||||
|
||||
matches("*", _) # "*" matches everything
|
||||
matches(x, x) # exact match
|
||||
```
|
||||
|
||||
Here, the first `matches` definition would yield an expression like `_ = input.fruit.name` in the partial evaluation results.
|
||||
:::
|
||||
|
||||
|
||||
## Built-in Functions
|
||||
|
||||
|
||||
@@ -392,6 +392,18 @@ func TestCompileHappyPathE2E(t *testing.T) {
|
||||
expRows: []fruitRow{apple, banana, cherry},
|
||||
prisma: true,
|
||||
},
|
||||
{
|
||||
name: "exists",
|
||||
policy: `include if input.fruits.price = _`,
|
||||
expRows: []fruitRow{apple, banana, cherry},
|
||||
prisma: true,
|
||||
},
|
||||
{
|
||||
name: "exists (with var)",
|
||||
policy: `include if { some v; v = input.fruits.price }`,
|
||||
expRows: []fruitRow{apple, banana, cherry},
|
||||
prisma: true,
|
||||
},
|
||||
{
|
||||
name: "simple startswith",
|
||||
policy: `include if startswith(input.fruits.name, "app")`,
|
||||
|
||||
@@ -189,13 +189,25 @@ func checkBuiltins(c *checker, e *ast.Expr, _ []*ast.Module) *ast.Error {
|
||||
}
|
||||
default: // lhs or rhs needs to be ground scalar, or, if twoRefsOK is true, unknown input refs
|
||||
// TODO(sr): collections might work, too, let's fix this later
|
||||
found := false
|
||||
for i := range 2 {
|
||||
if ast.IsScalar(e.Operand(i).Value) {
|
||||
found = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if !found && !(twoRefsOK && unknownRefs == 2) { // nolint:staticcheck
|
||||
if op0 == ast.Equality.Name && unknownRefs == 1 { // one unknown, the other side non-scalar
|
||||
for i := range 2 {
|
||||
switch e.Operand(i).Value.(type) {
|
||||
case ast.Var, ast.Ref: // OK
|
||||
if err0 := c.constraints.AssertFeature("existence-ref"); err0 != nil {
|
||||
return err(loc, "existence of field: %s", err0.Error())
|
||||
}
|
||||
default:
|
||||
return err(loc, "both rhs and lhs non-scalar/non-ground")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !twoRefsOK || unknownRefs != 2 { // nolint:staticcheck
|
||||
return err(loc, "both rhs and lhs non-scalar/non-ground")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func NewConstraints(typ, variant string) (*Constraint, error) {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported variant for %s: %s", typ, variant)
|
||||
}
|
||||
c.Features.Add("not", "field-ref")
|
||||
c.Features.Add("not", "field-ref", "existence-ref")
|
||||
case "ucast":
|
||||
switch v := strings.ToLower(variant); v {
|
||||
case "all":
|
||||
@@ -88,7 +88,7 @@ func NewConstraints(typ, variant string) (*Constraint, error) {
|
||||
c.Builtins = allBuiltins
|
||||
case "prisma":
|
||||
c.Variant = v
|
||||
c.Features.Add("not")
|
||||
c.Features.Add("not", "existence-ref")
|
||||
c.Builtins = allBuiltins
|
||||
case "linq":
|
||||
c.Variant = "LINQ" // normalize spelling
|
||||
|
||||
@@ -107,6 +107,12 @@ func toFieldNode(op string, r ast.Ref, v ast.Value, opts *Opts, refOK bool) *uca
|
||||
Field: f,
|
||||
}
|
||||
}
|
||||
case ast.Var:
|
||||
if op == ast.Equality.Name { // _ = <unknown>
|
||||
op = "ne" // this will end up as `unknown IS NOT NULL`
|
||||
} else { // we used to run into "var needs evaluation" below, let's just return nil here
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
var err error
|
||||
value, err = ast.ValueToInterface(v, nil)
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestPostPartialChecks(t *testing.T) {
|
||||
errors []Error
|
||||
mappings map[string]any
|
||||
skip string
|
||||
result map[string]any
|
||||
result any
|
||||
}{
|
||||
{
|
||||
note: "happy path",
|
||||
@@ -552,6 +552,40 @@ include if user == input.fruits.user`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // NOTE(sr): only supported for SQL-ish targets and prisma (see below)
|
||||
note: "equality with var",
|
||||
rego: `include if input.fruits.colour = _`,
|
||||
target: "application/vnd.opa.ucast.linq+json",
|
||||
errors: []Error{
|
||||
{
|
||||
Code: "pe_fragment_error",
|
||||
Message: `existence of field: unsupported feature "existence-ref" for UCAST (LINQ)`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "equality with var (sql)",
|
||||
rego: `include if input.fruits.colour = _`,
|
||||
target: "application/vnd.opa.sql.mysql+json",
|
||||
result: "WHERE fruits.colour IS NOT NULL",
|
||||
},
|
||||
{
|
||||
note: "equality with var (sql), reversed",
|
||||
rego: `include if _ = input.fruits.colour`,
|
||||
target: "application/vnd.opa.sql.mysql+json",
|
||||
result: "WHERE fruits.colour IS NOT NULL",
|
||||
},
|
||||
{
|
||||
note: "equality with var (prisma)",
|
||||
rego: `include if input.fruits.colour = _`,
|
||||
target: "application/vnd.opa.ucast.prisma+json",
|
||||
result: map[string]any{
|
||||
"type": "field",
|
||||
"field": "fruits.colour",
|
||||
"operator": "ne",
|
||||
"value": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "not a call/term",
|
||||
rego: `include if input.fruits.colour`,
|
||||
|
||||
Reference in New Issue
Block a user