-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PrepareForEval error when using partial evaluation: "rego_unsafe_var_error: expression is unsafe" #4766
Comments
Thanks for the detailed report! Having a look, here's what the compiler does to your modules when running PrepareForEval with partial eval: package partial.iam.internal # PE support module
check_access[__local7__1] {
__local49__ = input.subject.roles[_]
every __local0__, __local1__ in __local49__ {
not data.iam.value_missing(__local1__, __local16__, "bar")
}
__local7__1 = input.resource.resources[resource_idx1].identifier
} package iam.internal # internal.rego
check_access[__local7__] {
__local25__ = input.subject.roles[_]
__local26__ = input.resource.resources[resource_idx]
data.resourceID.policyID.allow(__local25__, __local26__)
__local7__ = input.resource.resources[resource_idx].identifier
} package resourceID.policyID # policy.rego
foo(__local42__, __local43__, __local44__) = true {
__local23__ = __local42__
__local24__ = __local23__
__local50__ = __local24__
__local51__ = __local50__
__local52__ = __local51__
every __local45__, __local46__ in __local52__ {
not data.iam.value_missing(__local46__, __local43__, __local44__)
}
}
allow(__local47__, __local48__) = true {
data.resourceID.policyID.foo(__local47__, __local48__, "bar")
} package iam # iam.rego
value_missing(__local13__, __local14__, __local27__) = true {
object.get(__local13__, __local27__, "", __local22__)
__local3__ = __local22__
__local3__ = ""
} package partial # PE result
__result__ = _ {
data.partial.iam.internal.check_access = _
} 🔍 Looks like we're losing our Interestingly, the same is not true for running PE upfront via
Just the first steps. We'll need to look further into this. |
🤔 I think the "missing imports" are a red herring. The modules have already been parsed, so the import doesn't need to be there... Anyways, commenting out the first eval, to avoid potential crossed wires, running only q, err := r.PrepareForEval(ctx, rego.WithPartialEval())
if err != nil {
fmt.Printf("PrepareForEval with partial: %s\n", err.Error())
return
}
_, err = q.Eval(ctx, input)
if err != nil {
fmt.Printf("Eval with partial: %s\n", err.Error())
return
} in main.go, we'll get this error:
foo(outer_role, resource, key) {
every role in outer_role {
not data.iam.value_missing(role, resource, key) # HERE
}
} OHH wait I believe I've found it: package partial.iam.internal # PE support module
check_access[__local7__1] {
__local49__ = input.subject.roles[_]
every __local0__, __local1__ in __local49__ {
not data.iam.value_missing(__local1__, __local16__, "bar")
}
__local7__1 = input.resource.resources[resource_idx1].identifier
} I'm not sure about the location and all that, but So the problem has to do with And looking at the support module in my previous comment more closely, it exhibits the same problem: check_access[__local4__1] {
every __local8__, __local9__ in input.subject.roles[_] {
not data.iam.value_missing(__local9__, __local6__, "bar") # <<< __local6__ is unsafe
}
__local4__1 = input.resource.resources[resource_idx1].identifier
} |
ℹ️ minimal example package test
import future.keywords.every
p {
f(input)
}
f(x) {
every y in [1] {
a := x
1 == y
}
}
☝️ |
Thanks for looking into this ! I'm not sure if it makes a difference but one thing to note is the policies here aren't exactly what we're using. In actual usage we're consuming all arguments in the fn analogous to |
@jguenther-va With the branch of that PR ☝️ your main.go runs through without errors. If you could take a look, and perhaps try it with your real-world policies, that would be great. |
@srenatus it does fix the error in the main.go above but unfortunately it doesn't fix all instances of "unsafe expression" we're seeing from our actual policies. There's 2 places we had been using I will see if I can reproduce the same situation in main.go again here, thank you |
@srenatus this seems to reproduce it again (with these changes to It's not exactly how our policies are actually defined/pseudocode, so it probably doesn't make much sense to read but:
iam.rego
package iam
import future.keywords.every
baz(resource, key) = r {
r := key
}
bazs(resource, keys) = r {
r := [key | key := baz(resource, keys[_])]
}
bar(role, resource, role_attr_key, resource_attr_keys) {
role_attr_keys := bazs(resource, role_attr_key)
role_attrs := object.get(role, role_attr_keys, [""])
resource_attrs := object.get(resource.attributes, resource_attr_keys, [""])
every resource_attr in resource_attrs {
resource_attr == role_attrs[_]
}
} policy.rego
package resourceID.policyID
allow(role, resource) {
data.iam.bar(role, resource, "foo", "bar")
} |
@jguenther-va thanks for being persistent. I'll have another look with that second case 🔍 |
So this one seems unrelated to the previous one. When reordering this rule body for safety, check_access[__local16__1] {
__local6__4 = [ __local5__4 |
__local24__4 = __local4__4[_]
data.iam.baz(input.resource.resources[resource_idx1], __local24__4, __local19__4)
__local5__4 = __local19__4
__local4__4 = "foo"
]
object.get(input.subject.roles[_], __local6__4, [""], __local21__3)
object.get(input.resource.resources[resource_idx1].attributes, "bar", [""], __local54__)
every __local28__, __local29__ in __local54__ {
__local29__ = __local21__3[_]
}
__local16__1 = input.resource.resources[resource_idx1].identifier
} it fails, complaining that the every expression wasn't safe because of However, this bit looks puzzling: __local6__4 = [ __local5__4 |
__local24__4 = __local4__4[_]
data.iam.baz(input.resource.resources[resource_idx1], __local24__4, __local19__4)
__local5__4 = __local19__4
__local4__4 = "foo"
] I don't see how this would ever be satisfiable: When using __local6__4 = [__local5__4 |
__local24__4 = __local4__4[_]
data.iam.baz(input.resource.resources[resource_idx1], __local24__4, __local19__4)
__local5__4 = __local19__4
__local4__4 = ["foo"]
] which is OK. But the error persists. |
📝 object.get(input.subject.roles[_], __local6__4, [""], __local21__3)
object.get(input.resource.resources[resource_idx1].attributes, "bar", [""], __local54__)
__local16__1 = input.resource.resources[resource_idx1].identifier
__local6__4 = [__local5__4 |
__local24__4 = __local4__4[_]
data.iam.baz(input.resource.resources[resource_idx1], __local24__4, __local19__4)
__local5__4 = __local19__4
__local4__4 = ["foo"]
] then It's missing that because when the output vars of the call are checked, we get nothing: it'll recognize that |
So no patch yet, but I'm closing in on the problem. This is a very productive issue, thanks for that 👍 😄 |
Please let me know if it would help to see the actual policies we're using (can share privately). I made sure the error is the exact same after trimming it down and anonymizing it, but I'm not sure if that could have changed something unintentionally--there are several rules in actual usage that aren't in the policies above. Thanks again ! |
Thanks a bunch. Even if it was a wrongly-trimmed policy, it's been putting the spotlight on a real bug. Let's keep this dance going, when I get a fix pushed, you can run it against your real policies... If it still doesn't work out, I'll happily have a look at your policies. 🔍 |
Our previous approach, ordering for closures first, and taking the growing set of output variables of the reordered body into account in a second step, did not work out for some examples: object.get(input.subject.roles[_], comp, [""], output) comp = [ 1 | true ] every y in [2] { y in output } Here, the closure of `every` would have not checkout out because we've never registered the output variable `output` -- since the first call to object.get is unsafe without `comp`, too. Now, the two stages have been merged. It's got surprisingly little fallout, one test case had to be adjusted, which I believe to be a minor case, too. Fixes the second part of open-policy-agent#4766. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
Hello there! I've just opened a second PR, #4801, to address the second bug we've cornered here. I've pushed both commits to an extra branch for experimenting, and I might be missing something -- it's been a while -- but |
Our previous approach, ordering for closures first, and taking the growing set of output variables of the reordered body into account in a second step, did not work out for some examples: object.get(input.subject.roles[_], comp, [""], output) comp = [ 1 | true ] every y in [2] { y in output } Here, the closure of `every` would have not checkout out because we've never registered the output variable `output` -- since the first call to object.get is unsafe without `comp`, too. Now, the two stages have been merged. It's got surprisingly little fallout, one test case had to be adjusted, which I believe to be a minor case, too. Fixes the second part of open-policy-agent#4766. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
@srenatus on the
not sure what the difference is here that you're not seeing that error, just double checked and the only change after the original PR description was the 2 policy files mentioned in this comment edit: if I try the branch in that second PR this is the error I get (may just be because it doesn't have the fix from the first PR though?)
|
This is consistent with not having [ ] around the "foo" argument, see the last parts of #4766 (comment) |
@srenatus whoops my bad, just checked and the fix from many thanks ! |
Our previous approach, ordering for closures first, and taking the growing set of output variables of the reordered body into account in a second step, did not work out for some examples: object.get(input.subject.roles[_], comp, [""], output) comp = [ 1 | true ] every y in [2] { y in output } Here, the closure of `every` would have not checkout out because we've never registered the output variable `output` -- since the first call to object.get is unsafe without `comp`, too. Now, the two stages have been merged. It's got surprisingly little fallout, one test case had to be adjusted, which I believe to be a minor case, too. Fixes the second part of #4766. Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
✔️ all merged |
Short description
With OPA go library versions
v0.39.0
andv0.41.0
, when we use theevery
keyword we're seeing an unexpected error fromPrepareForEval
, but only when we useWithPartialEval
:As far as we knew this error never came up when we were evaluating the
rego.Rego
object directly. It started happening when we moved over to usingPrepareForEval
.For reproduction steps, policies, and example go code that reproduces the problem, see below.
Note that it seems to have something to do with the structure of modules/packages that we use--if I just put everything in the same file I can't seem to reproduce the problem.
Code
main.go
go.mod
Policies
iam.rego
internal.rego
policy.rego
Steps To Reproduce
main.go
,go.mod
, and all.rego
files as named and with the above contentsgo run .
PrepareForEval()
but only underWithPartialEval()
:Expected behavior
We would expect that
PrepareForEval()
completes without error usingWithPartialEval()
, i.e. the above script runs without producing any output.Additional context
We've successfully worked around this issue by avoiding the use of the
every
keyword and instead using the "not-some-not" pattern mentioned in the docs, which results in Rego policies that do what we need them to do but are harder to read.The text was updated successfully, but these errors were encountered: