mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
ast+format: introduce new keywords for rule heads: if and contains
`contains` provides an alternative way to declare partial sets:
p contains x {
x := { "foo": "bar"
}
which is the same as
p[x] {
x := { "foo": "bar"
}
The keyword is enabled by importing `future.keywords.contains`, and
when it _is enabled_, the format will be used for all partial sets in
that file for pretty-printing.
`if` is a new keyword allowing for more readable rule definitions:
The syntax is
NAME [if] { EXPR [EXPR...] }
and the is a shorthand allows dropping the braces around the expression
if there is only one:
NAME if EXPR
For example, this allows expressions like
allow if not deny
f(xs) if every x in xs { x != "foo" }
The one exception here are partial sets: they cannot use `if` UNLESS
they use `contains`:
p[x] { x := "foo" } # valid
p contains x { x := "bar" } # valid
p contains x if { x := "bar" } # valid
p[x] if { x := "foo" } # invalid
This is because we want to interpret that differently (as an object
rule defining `p.foo = true`) in the near future.
The formatter works in the same way: if `future.keywords.if` is imported, it
will be used where it can be used.
We don't want to be too eager when it comes to introducing syntactic sugar.
So this will be rewritten, because head and body expression are on the same
line:
p := 5 if { time.day_of_week() == "Monday" }
# => p := 5 if time.day_of_week() == "Monday"
but this won't:
p := 5 if {
time.day_of_week() == "Monday"
}
The rationale here is that if the policy author decided that they want this on
an extra line, we won't mess with it.
This also sidesteps the need to check if both the head and the single body
expression have a comment.
This change includes various docs updates. Notable exceptions are the GK docs,
since it will take a while for these keywords to be come available there; and
the frontpage: merging a PR would update the frontpage immediately, and we
don't want to show something there that isn't available in the latest release.
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
committed by
Stephan Renatus
parent
9d4fc06f4d
commit
04a3523b22
+6
-2
@@ -3518,8 +3518,12 @@ func getGlobals(pkg *Package, rules []Var, imports []*Import) map[Var]*usedRef {
|
||||
}
|
||||
|
||||
// Populate globals with imports.
|
||||
for _, i := range imports {
|
||||
globals[i.Name()] = &usedRef{ref: i.Path.Value.(Ref)}
|
||||
for _, imp := range imports {
|
||||
path := imp.Path.Value.(Ref)
|
||||
if FutureRootDocument.Equal(path[0]) {
|
||||
continue // ignore future imports
|
||||
}
|
||||
globals[imp.Name()] = &usedRef{ref: path}
|
||||
}
|
||||
|
||||
return globals
|
||||
|
||||
@@ -67,6 +67,8 @@ const (
|
||||
Semicolon
|
||||
|
||||
Every
|
||||
Contains
|
||||
If
|
||||
)
|
||||
|
||||
var strings = [...]string{
|
||||
@@ -115,6 +117,8 @@ var strings = [...]string{
|
||||
Dot: ".",
|
||||
Semicolon: ";",
|
||||
Every: "every",
|
||||
Contains: "contains",
|
||||
If: "if",
|
||||
}
|
||||
|
||||
var keywords = map[string]Token{
|
||||
|
||||
+64
-12
@@ -543,7 +543,8 @@ func (p *Parser) parseRules() []*Rule {
|
||||
return nil
|
||||
}
|
||||
|
||||
if rule.Head = p.parseHead(rule.Default); rule.Head == nil {
|
||||
usesContains := false
|
||||
if rule.Head, usesContains = p.parseHead(rule.Default); rule.Head == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -556,13 +557,52 @@ func (p *Parser) parseRules() []*Rule {
|
||||
return []*Rule{&rule}
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.LBrace {
|
||||
hasIf := false
|
||||
if p.s.tok == tokens.If {
|
||||
hasIf = true
|
||||
}
|
||||
|
||||
if hasIf && !usesContains && rule.Head.Key != nil && rule.Head.Value == nil {
|
||||
p.illegal("invalid for partial set rule %s (use `contains`)", rule.Head.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case hasIf:
|
||||
p.scan()
|
||||
s := p.save()
|
||||
if expr := p.parseLiteral(); expr != nil {
|
||||
// NOTE(sr): set literals are never false or undefined, so parsing this as
|
||||
// p if { true }
|
||||
// ^^^^^^^^ set of one element, `true`
|
||||
// isn't valid.
|
||||
isSetLiteral := false
|
||||
if t, ok := expr.Terms.(*Term); ok {
|
||||
_, isSetLiteral = t.Value.(Set)
|
||||
}
|
||||
// expr.Term is []*Term or Every
|
||||
if !isSetLiteral {
|
||||
rule.Body.Append(expr)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// parsing as literal didn't work out, expect '{ BODY }'
|
||||
p.restore(s)
|
||||
fallthrough
|
||||
|
||||
case p.s.tok == tokens.LBrace:
|
||||
p.scan()
|
||||
if rule.Body = p.parseBody(tokens.RBrace); rule.Body == nil {
|
||||
return nil
|
||||
}
|
||||
p.scan()
|
||||
} else {
|
||||
|
||||
case usesContains:
|
||||
rule.Body = NewBody(NewExpr(BooleanTerm(true).SetLocation(rule.Location)).SetLocation(rule.Location))
|
||||
return []*Rule{&rule}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -667,7 +707,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
|
||||
return &rule
|
||||
}
|
||||
|
||||
func (p *Parser) parseHead(defaultRule bool) *Head {
|
||||
func (p *Parser) parseHead(defaultRule bool) (*Head, bool) {
|
||||
|
||||
var head Head
|
||||
head.SetLoc(p.s.Loc())
|
||||
@@ -689,13 +729,13 @@ func (p *Parser) parseHead(defaultRule bool) *Head {
|
||||
if p.s.tok != tokens.RParen {
|
||||
head.Args = p.parseTermList(tokens.RParen, nil)
|
||||
if head.Args == nil {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
p.scan()
|
||||
|
||||
if p.s.tok == tokens.LBrack {
|
||||
return nil
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,13 +754,23 @@ func (p *Parser) parseHead(defaultRule bool) *Head {
|
||||
p.scan()
|
||||
}
|
||||
|
||||
if p.s.tok == tokens.Unify {
|
||||
switch p.s.tok {
|
||||
case tokens.Contains:
|
||||
p.scan()
|
||||
head.Key = p.parseTermInfixCall()
|
||||
if head.Key == nil {
|
||||
p.illegal("expected rule key term (e.g., %s contains <VALUE> { ... })", head.Name)
|
||||
}
|
||||
|
||||
return &head, true
|
||||
case tokens.Unify:
|
||||
p.scan()
|
||||
head.Value = p.parseTermInfixCall()
|
||||
if head.Value == nil {
|
||||
p.illegal("expected rule value term (e.g., %s[%s] = <VALUE> { ... })", head.Name, head.Key)
|
||||
}
|
||||
} else if p.s.tok == tokens.Assign {
|
||||
|
||||
case tokens.Assign:
|
||||
s := p.save()
|
||||
p.scan()
|
||||
head.Assign = true
|
||||
@@ -744,7 +794,7 @@ func (p *Parser) parseHead(defaultRule bool) *Head {
|
||||
head.Value = BooleanTerm(true).SetLocation(head.Location)
|
||||
}
|
||||
|
||||
return &head
|
||||
return &head, false
|
||||
}
|
||||
|
||||
func (p *Parser) parseBody(end tokens.Token) Body {
|
||||
@@ -1243,7 +1293,7 @@ func (p *Parser) parseTerm() *Term {
|
||||
term = p.parseNumber()
|
||||
case tokens.String:
|
||||
term = p.parseString()
|
||||
case tokens.Ident:
|
||||
case tokens.Ident, tokens.Contains: // NOTE(sr): contains anywhere BUT in rule heads gets no special treatment
|
||||
term = p.parseVar()
|
||||
case tokens.LBrack:
|
||||
term = p.parseArray()
|
||||
@@ -2269,8 +2319,10 @@ func convertYAMLMapKeyTypes(x interface{}, path []string) (interface{}, error) {
|
||||
// futureKeywords is the source of truth for future keywords that will
|
||||
// eventually become standard keywords inside of Rego.
|
||||
var futureKeywords = map[string]tokens.Token{
|
||||
"in": tokens.In,
|
||||
"every": tokens.Every,
|
||||
"in": tokens.In,
|
||||
"every": tokens.Every,
|
||||
"contains": tokens.Contains,
|
||||
"if": tokens.If,
|
||||
}
|
||||
|
||||
func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]tokens.Token) {
|
||||
|
||||
+240
-2
@@ -1200,7 +1200,7 @@ func TestImport(t *testing.T) {
|
||||
func TestFutureImports(t *testing.T) {
|
||||
assertParseErrorContains(t, "future", "import future", "invalid import, must be `future.keywords`")
|
||||
assertParseErrorContains(t, "future.a", "import future.a", "invalid import, must be `future.keywords`")
|
||||
assertParseErrorContains(t, "unknown keyword", "import future.keywords.xyz", "unexpected keyword, must be one of [every in]")
|
||||
assertParseErrorContains(t, "unknown keyword", "import future.keywords.xyz", "unexpected keyword, must be one of [contains every if in]")
|
||||
assertParseErrorContains(t, "all keyword import + alias", "import future.keywords as xyz", "`future` imports cannot be aliased")
|
||||
assertParseErrorContains(t, "keyword import + alias", "import future.keywords.in as xyz", "`future` imports cannot be aliased")
|
||||
|
||||
@@ -1591,6 +1591,244 @@ func TestRule(t *testing.T) {
|
||||
assertParseError(t, "invalid rule body no newline", `p { a b c }`)
|
||||
}
|
||||
|
||||
func TestRuleContains(t *testing.T) {
|
||||
opts := ParserOptions{FutureKeywords: []string{"contains"}}
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
rule string
|
||||
exp *Rule
|
||||
}{
|
||||
{
|
||||
note: "simple",
|
||||
rule: `p contains "x" { true }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("x")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "no body",
|
||||
rule: `p contains "x"`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("x")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "set with var element",
|
||||
rule: `deny contains msg { msg := "nonono" }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("deny"), VarTerm("msg")),
|
||||
Body: MustParseBody(`msg := "nonono"`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "set with object elem",
|
||||
rule: `deny contains {"allow": false, "msg": msg} { msg := "nonono" }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("deny"), MustParseTerm(`{"allow": false, "msg": msg}`)),
|
||||
Body: MustParseBody(`msg := "nonono"`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
assertParseRule(t, tc.note, tc.rule, tc.exp, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleIf(t *testing.T) {
|
||||
opts := ParserOptions{FutureKeywords: []string{"contains", "if", "every"}}
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
rule string
|
||||
exp *Rule
|
||||
}{
|
||||
{
|
||||
note: "complete",
|
||||
rule: `p if { true }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete, normal body",
|
||||
rule: `p if { x := 10; x > y }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: MustParseBody(`x := 10; x > y`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete+else, normal bodies, assign",
|
||||
rule: `p := "yes" if { 10 > y } else := "no" { 10 <= y }`,
|
||||
exp: &Rule{
|
||||
Head: &Head{
|
||||
Name: Var("p"),
|
||||
Value: StringTerm("yes"),
|
||||
Assign: true,
|
||||
},
|
||||
Body: MustParseBody(`10 > y`),
|
||||
Else: &Rule{
|
||||
Head: &Head{
|
||||
Name: Var("p"),
|
||||
Value: StringTerm("no"),
|
||||
Assign: true,
|
||||
},
|
||||
Body: MustParseBody(`10 <= y`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete, shorthand",
|
||||
rule: `p if true`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete+not, shorthand",
|
||||
rule: `p if not q`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: MustParseBody(`not q`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete+else, shorthand",
|
||||
rule: `p if 1 > 2 else = 42 { 2 > 1 }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: MustParseBody(`1 > 2`),
|
||||
Else: &Rule{
|
||||
Head: &Head{
|
||||
Name: Var("p"),
|
||||
Value: NumberTerm("42"),
|
||||
},
|
||||
Body: MustParseBody(`2 > 1`),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "complete+call, shorthand",
|
||||
rule: `p if count(q) > 0`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), nil, BooleanTerm(true)),
|
||||
Body: MustParseBody(`count(q) > 0`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "function, shorthand",
|
||||
rule: `f(x) = y if y := x + 1`,
|
||||
exp: &Rule{
|
||||
Head: &Head{
|
||||
Name: Var("f"),
|
||||
Args: []*Term{VarTerm("x")},
|
||||
Value: VarTerm("y"),
|
||||
},
|
||||
Body: MustParseBody(`y := x + 1`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "function+every, shorthand",
|
||||
rule: `f(xs) if every x in xs { x != 0 }`,
|
||||
exp: &Rule{
|
||||
Head: &Head{
|
||||
Name: Var("f"),
|
||||
Args: []*Term{VarTerm("xs")},
|
||||
Value: BooleanTerm(true),
|
||||
},
|
||||
Body: MustParseBodyWithOpts(`every x in xs { x != 0 }`, opts),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "object",
|
||||
rule: `p["foo"] = "bar" if { true }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("foo"), StringTerm("bar")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "object, shorthand",
|
||||
rule: `p["foo"] = "bar" if true`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("foo"), StringTerm("bar")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "object with vars",
|
||||
rule: `p[x] = y if {
|
||||
x := "foo"
|
||||
y := "bar"
|
||||
}`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), VarTerm("x"), VarTerm("y")),
|
||||
Body: MustParseBody(`x := "foo"; y := "bar"`),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "set",
|
||||
rule: `p contains "foo" if { true }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("foo")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "set, shorthand",
|
||||
rule: `p contains "foo" if true`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), StringTerm("foo")),
|
||||
Body: NewBody(NewExpr(BooleanTerm(true))),
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "set+var+shorthand",
|
||||
rule: `p contains x if { x := "foo" }`,
|
||||
exp: &Rule{
|
||||
Head: NewHead(Var("p"), VarTerm("x")),
|
||||
Body: MustParseBody(`x := "foo"`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
assertParseRule(t, tc.note, tc.rule, tc.exp, opts)
|
||||
})
|
||||
}
|
||||
|
||||
errors := []struct {
|
||||
note string
|
||||
rule string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
note: "partial set+if, shorthand",
|
||||
rule: `p[x] if x := 1`,
|
||||
err: "rego_parse_error: unexpected if keyword: invalid for partial set rule p (use `contains`)",
|
||||
},
|
||||
{
|
||||
note: "partial set+if",
|
||||
rule: `p[x] if { x := 1 }`,
|
||||
err: "rego_parse_error: unexpected if keyword: invalid for partial set rule p (use `contains`)",
|
||||
},
|
||||
}
|
||||
for _, tc := range errors {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
assertParseErrorContains(t, tc.note, tc.rule, tc.err, opts)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuleElseKeyword(t *testing.T) {
|
||||
mod := `package test
|
||||
|
||||
@@ -4004,7 +4242,7 @@ func assertParseOne(t *testing.T, msg string, input string, correct func(interfa
|
||||
return
|
||||
}
|
||||
if len(stmts) != 1 {
|
||||
t.Errorf("Error on test \"%s\": parse error on %s: expected exactly one statement, got %d", msg, input, len(stmts))
|
||||
t.Errorf("Error on test \"%s\": parse error on %s: expected exactly one statement, got %d: %v", msg, input, len(stmts), stmts)
|
||||
return
|
||||
}
|
||||
correct(stmts[0])
|
||||
|
||||
@@ -4058,7 +4058,9 @@
|
||||
}
|
||||
],
|
||||
"future_keywords": [
|
||||
"contains",
|
||||
"every",
|
||||
"if",
|
||||
"in"
|
||||
],
|
||||
"wasm_abi_versions": [
|
||||
|
||||
+75
-43
@@ -108,18 +108,30 @@ Rego (pronounced "ray-go") is purpose-built for expressing policies over complex
|
||||
hierarchical data structures. For detailed information on Rego see the [Policy
|
||||
Language](policy-language) documentation.
|
||||
|
||||
> 💡 The examples below are interactive! If you edit the input data above
|
||||
{{< info >}}
|
||||
💡 The examples below are interactive! If you edit the input data above
|
||||
containing servers, networks, and ports, the output will change below.
|
||||
Similarly, if you edit the queries or rules in the examples below the output
|
||||
will change. As you read through this section, try changing the input, queries,
|
||||
and rules and observe the difference in output.
|
||||
>
|
||||
> 💻 They can also be run locally on your machine using the [`opa eval` command, here are setup instructions.](#running-opa)
|
||||
|
||||
💻 They can also be run locally on your machine using the [`opa eval` command, here are setup instructions.](#running-opa)
|
||||
|
||||
Note that the examples in this section try to represent the best practices.
|
||||
As such, they make use of keywords that are meant to become standard keywords
|
||||
at some point in time, but have been introduced gradually. These _future
|
||||
keywords_ can be enabled using
|
||||
|
||||
```live:eg/import:module:read_only
|
||||
import future.keywords
|
||||
```
|
||||
{{< /info >}}
|
||||
|
||||
### References
|
||||
|
||||
```live:example/refs:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
When OPA evaluates policies it binds data provided in the query to a global
|
||||
@@ -156,6 +168,7 @@ input.deadbeef
|
||||
|
||||
```live:example/exprs:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
To produce policy decisions in Rego you write expressions against input and
|
||||
@@ -214,6 +227,7 @@ input.servers[0].protocols[0] == "telnet"
|
||||
|
||||
```live:example/vars:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
You can store values in intermediate variables using the `:=` (assignment)
|
||||
@@ -265,6 +279,7 @@ x != y # y has not been assigned a value
|
||||
|
||||
```live:example/iter:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
Like other declarative languages (e.g., SQL), iteration in Rego happens
|
||||
@@ -386,19 +401,17 @@ It introduces new bindings to the evaluation of the rest of the rule body.
|
||||
Using `some`, we can express the rules introduced above in different ways:
|
||||
|
||||
```live:example/iter/some1:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
public_network[net.id] { # net.id is in the public_network set if...
|
||||
some net in input.networks # some network exists and..
|
||||
net.public # it is public.
|
||||
public_network contains net.id if {
|
||||
some net in input.networks # some network exists and..
|
||||
net.public # it is public.
|
||||
}
|
||||
|
||||
shell_accessible[server.id] {
|
||||
shell_accessible contains server.id if {
|
||||
some server in input.servers
|
||||
"telnet" in server.protocols
|
||||
}
|
||||
|
||||
shell_accessible[server.id] {
|
||||
shell_accessible contains server.id if {
|
||||
some server in input.servers
|
||||
"ssh" in server.protocols
|
||||
}
|
||||
@@ -417,9 +430,7 @@ Expanding on the examples above, `every` allows us to succinctly express that
|
||||
a condition holds for all elements of a domain.
|
||||
|
||||
```live:example/iter/every2:module:merge_down
|
||||
import future.keywords.every
|
||||
|
||||
no_telnet_exposed {
|
||||
no_telnet_exposed if {
|
||||
every server in input.servers {
|
||||
every protocol in server.protocols {
|
||||
"telnet" != protocol
|
||||
@@ -427,17 +438,17 @@ no_telnet_exposed {
|
||||
}
|
||||
}
|
||||
|
||||
no_telnet_exposed_alt { # alternative: every + not-in
|
||||
no_telnet_exposed_alt if { # alternative: every + not-in
|
||||
every server in input.servers {
|
||||
not "telnet" in server.protocols
|
||||
}
|
||||
}
|
||||
|
||||
no_telnet_exposed_alt2 { # alternative: not + rule + some
|
||||
no_telnet_exposed_alt2 if { # alternative: not + rule + some
|
||||
not any_telnet_exposed
|
||||
}
|
||||
|
||||
any_telnet_exposed {
|
||||
any_telnet_exposed if {
|
||||
some server in input.servers
|
||||
"telnet" in server.protocols
|
||||
}
|
||||
@@ -473,24 +484,27 @@ For all the details, see [Every Keyword](policy-language/#every-keyword).
|
||||
Rego lets you encapsulate and re-use logic with rules. Rules are just if-then
|
||||
logic statements. Rules can either be "complete" or "partial".
|
||||
|
||||
```live:example/complete:module:hidden
|
||||
package example.rules
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
#### Complete Rules
|
||||
|
||||
Complete rules are if-then statements that assign a single value to a variable.
|
||||
For example:
|
||||
|
||||
```live:example/complete/1:module:openable
|
||||
package example.rules
|
||||
|
||||
any_public_networks := true { # is true if...
|
||||
net := input.networks[_] # some network exists and..
|
||||
net.public # it is public.
|
||||
any_public_networks := true if {
|
||||
some net in input.networks # some network exists and..
|
||||
net.public # it is public.
|
||||
}
|
||||
```
|
||||
|
||||
Every rule consists of a _head_ and a _body_. In Rego we say the rule head is
|
||||
true _if_ the rule body is true for some set of variable assignments. In the
|
||||
example above `any_public_networks := true` is the head and `net :=
|
||||
input.networks[_]; net.public` is the body.
|
||||
Every rule consists of a _head_ and a _body_. In Rego we say the rule head
|
||||
is true _if_ the rule body is true for some set of variable assignments. In
|
||||
the example above `any_public_networks := true` is the head and `some net in
|
||||
input.networks; net.public` is the body.
|
||||
|
||||
You can query for the value generated by rules just like any other value:
|
||||
|
||||
@@ -515,11 +529,9 @@ data.example.rules.any_public_networks
|
||||
If you omit the `= <value>` part of the rule head the value defaults to `true`.
|
||||
You could rewrite the example above as follows without changing the meaning:
|
||||
|
||||
```live:example/complete_elided:module:read_only,openable
|
||||
package example.rules
|
||||
|
||||
any_public_networks {
|
||||
net := input.networks[_]
|
||||
```live:example/complete/elided:module:read_only,openable
|
||||
any_public_networks if {
|
||||
some net in input.networks
|
||||
net.public
|
||||
}
|
||||
```
|
||||
@@ -563,15 +575,18 @@ any_public_networks
|
||||
|
||||
#### Partial Rules
|
||||
|
||||
```live:example/partial_set:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
Partial rules are if-then statements that generate a set of values and
|
||||
assign that set to a variable. For example:
|
||||
|
||||
```live:example/partial_set:module:openable
|
||||
package example.rules
|
||||
|
||||
public_network[net.id] { # net.id is in the public_network set if...
|
||||
net := input.networks[_] # some network exists and...
|
||||
net.public # it is public.
|
||||
```live:example/partial_set/1:module:openable
|
||||
public_network contains net.id if {
|
||||
some net in input.networks # some network exists and..
|
||||
net.public # it is public.
|
||||
}
|
||||
```
|
||||
|
||||
@@ -579,27 +594,44 @@ In the example above `public_network[net.id]` is the rule head and `net :=
|
||||
input.networks[_]; net.public` is the rule body. You can query for the entire
|
||||
set of values just like any other value:
|
||||
|
||||
```live:example/partial_set/extent:query:merge_down
|
||||
```live:example/partial_set/1/extent:query:merge_down
|
||||
public_network
|
||||
```
|
||||
```live:example/partial_set/extent:output
|
||||
```live:example/partial_set/1/extent:output
|
||||
```
|
||||
|
||||
You can iterate over the set of values by referencing the set elements with a
|
||||
Iteration over the set of values can be done with the `some ... in ...` expression:
|
||||
|
||||
```live:example/partial_set/1/iteration_some:query:merge_down
|
||||
some net in public_network
|
||||
```
|
||||
```live:example/partial_set/1/iteration_some:output
|
||||
```
|
||||
|
||||
With a literal, or a bound variable, you can check if the value exists in the set
|
||||
via `... in ...`:
|
||||
|
||||
```live:example/partial_set/1/exists_in:query:merge_down
|
||||
"net3" in public_network
|
||||
```
|
||||
```live:example/partial_set/1/exists_in:output
|
||||
```
|
||||
|
||||
You can also iterate over the set of values by referencing the set elements with a
|
||||
variable:
|
||||
|
||||
```live:example/partial_set/iteration:query:merge_down
|
||||
```live:example/partial_set/1/iteration:query:merge_down
|
||||
some n; public_network[n]
|
||||
```
|
||||
```live:example/partial_set/iteration:output
|
||||
```live:example/partial_set/1/iteration:output
|
||||
```
|
||||
|
||||
Lastly, you can check if a value exists in the set using the same syntax:
|
||||
|
||||
```live:example/partial_set/exists:query:merge_down
|
||||
```live:example/partial_set/1/exists:query:merge_down
|
||||
public_network["net3"]
|
||||
```
|
||||
```live:example/partial_set/exists:output
|
||||
```live:example/partial_set/1/exists:output
|
||||
```
|
||||
|
||||
In addition to partially defining sets, You can also partially define key/value
|
||||
|
||||
@@ -14,36 +14,37 @@ Let's start with an example policy that restricts access to an endpoint based on
|
||||
|
||||
```live:bool_example:module:openable
|
||||
package envoy.authz
|
||||
import future.keywords
|
||||
|
||||
import input.attributes.request.http
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
is_token_valid
|
||||
action_allowed
|
||||
}
|
||||
|
||||
is_token_valid {
|
||||
is_token_valid if {
|
||||
token.valid
|
||||
now := time.now_ns() / 1000000000
|
||||
token.payload.nbf <= now
|
||||
now < token.payload.exp
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "GET"
|
||||
token.payload.role == "guest"
|
||||
glob.match("/people/*", ["/"], http.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "GET"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people/*", ["/"], http.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "POST"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http.path)
|
||||
@@ -51,7 +52,7 @@ action_allowed {
|
||||
}
|
||||
|
||||
|
||||
token := {"valid": valid, "payload": payload} {
|
||||
token := {"valid": valid, "payload": payload} if {
|
||||
[_, encoded] := split(http.headers.authorization, " ")
|
||||
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
|
||||
}
|
||||
@@ -110,12 +111,13 @@ If you want, you can also control the HTTP status sent to the upstream or downst
|
||||
|
||||
```live:obj_example:module:openable
|
||||
package envoy.authz
|
||||
import future.keywords
|
||||
|
||||
import input.attributes.request.http
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
is_token_valid
|
||||
action_allowed
|
||||
}
|
||||
@@ -127,37 +129,35 @@ request_headers_to_remove := ["one-auth-header", "another-auth-header"]
|
||||
|
||||
response_headers_to_add["x-foo"] := "bar"
|
||||
|
||||
status_code := 200 {
|
||||
status_code := 200 if {
|
||||
allow
|
||||
} else := 401 {
|
||||
not is_token_valid
|
||||
} else := 403 {
|
||||
true
|
||||
}
|
||||
} else := 403
|
||||
|
||||
body := "Authentication Failed" { status_code == 401 }
|
||||
body := "Unauthorized Request" { status_code == 403 }
|
||||
body := "Authentication Failed" if status_code == 401
|
||||
body := "Unauthorized Request" if status_code == 403
|
||||
|
||||
is_token_valid {
|
||||
is_token_valid if {
|
||||
token.valid
|
||||
now := time.now_ns() / 1000000000
|
||||
token.payload.nbf <= now
|
||||
now < token.payload.exp
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "GET"
|
||||
token.payload.role == "guest"
|
||||
glob.match("/people/*", ["/"], http.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "GET"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people/*", ["/"], http.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
action_allowed if {
|
||||
http.method == "POST"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http.path)
|
||||
@@ -165,7 +165,7 @@ action_allowed {
|
||||
}
|
||||
|
||||
|
||||
token := {"valid": valid, "payload": payload} {
|
||||
token := {"valid": valid, "payload": payload} if {
|
||||
[_, encoded] := split(http.headers.authorization, " ")
|
||||
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
|
||||
}
|
||||
@@ -434,12 +434,11 @@ access the path `/people`.
|
||||
|
||||
```live:parsed_path_example:module:read_only
|
||||
package envoy.authz
|
||||
import future.keywords
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
input.parsed_path == ["people"]
|
||||
}
|
||||
allow if input.parsed_path == ["people"]
|
||||
```
|
||||
|
||||
The `parsed_query` field in the input is also generated from the `path` field in the HTTP request. This field provides
|
||||
@@ -448,10 +447,11 @@ the HTTP URL query as a map of string array. The below sample policy allows anyo
|
||||
|
||||
```live:parsed_query_example:module:read_only
|
||||
package envoy.authz
|
||||
import future.keywords
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
input.parsed_path == ["people"]
|
||||
input.parsed_query.lang == ["en"]
|
||||
input.parsed_query.id == ["1", "2"]
|
||||
@@ -464,10 +464,11 @@ can then be used in a policy as shown below.
|
||||
|
||||
```live:parsed_body_example:module:read_only
|
||||
package envoy.authz
|
||||
import future.keywords
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
input.parsed_body.firstname == "Charlie"
|
||||
input.parsed_body.lastname == "Opa"
|
||||
}
|
||||
|
||||
@@ -45,50 +45,50 @@ The `quick_start.yaml` manifest defines the following resources:
|
||||
|
||||
```live:example:module:openable
|
||||
package istio.authz
|
||||
|
||||
|
||||
import future.keywords
|
||||
|
||||
import input.attributes.request.http as http_request
|
||||
import input.parsed_path
|
||||
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
parsed_path[0] == "health"
|
||||
http_request.method == "GET"
|
||||
|
||||
allow if {
|
||||
parsed_path[0] == "health"
|
||||
http_request.method == "GET"
|
||||
}
|
||||
|
||||
allow {
|
||||
roles_for_user[r]
|
||||
required_roles[r]
|
||||
|
||||
allow if {
|
||||
some r in roles_for_user
|
||||
r in required_roles
|
||||
}
|
||||
|
||||
roles_for_user[r] {
|
||||
r := user_roles[user_name][_]
|
||||
|
||||
roles_for_user contains r if {
|
||||
some r in user_roles[user_name]
|
||||
}
|
||||
|
||||
required_roles[r] {
|
||||
perm := role_perms[r][_]
|
||||
perm.method == http_request.method
|
||||
perm.path == http_request.path
|
||||
|
||||
required_roles contains r if {
|
||||
some perm in role_perms[r]
|
||||
perm.method == http_request.method
|
||||
perm.path == http_request.path
|
||||
}
|
||||
|
||||
user_name := parsed {
|
||||
[_, encoded] := split(http_request.headers.authorization, " ")
|
||||
[parsed, _] := split(base64url.decode(encoded), ":")
|
||||
|
||||
user_name := parsed if {
|
||||
[_, encoded] := split(http_request.headers.authorization, " ")
|
||||
[parsed, _] := split(base64url.decode(encoded), ":")
|
||||
}
|
||||
|
||||
|
||||
user_roles := {
|
||||
"alice": ["guest"],
|
||||
"bob": ["admin"]
|
||||
"alice": ["guest"],
|
||||
"bob": ["admin"],
|
||||
}
|
||||
|
||||
|
||||
role_perms := {
|
||||
"guest": [
|
||||
{"method": "GET", "path": "/productpage"},
|
||||
],
|
||||
"admin": [
|
||||
{"method": "GET", "path": "/productpage"},
|
||||
{"method": "GET", "path": "/api/v1/products"},
|
||||
],
|
||||
"guest": [{"method": "GET", "path": "/productpage"}],
|
||||
"admin": [
|
||||
{"method": "GET", "path": "/productpage"},
|
||||
{"method": "GET", "path": "/api/v1/products"},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -125,44 +125,46 @@ employee with the same `firstname` as himself.
|
||||
```live:example:module:openable
|
||||
package envoy.authz
|
||||
|
||||
import future.keywords
|
||||
|
||||
import input.attributes.request.http as http_request
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
is_token_valid
|
||||
action_allowed
|
||||
allow if {
|
||||
is_token_valid
|
||||
action_allowed
|
||||
}
|
||||
|
||||
is_token_valid {
|
||||
token.valid
|
||||
now := time.now_ns() / 1000000000
|
||||
token.payload.nbf <= now
|
||||
now < token.payload.exp
|
||||
is_token_valid if {
|
||||
token.valid
|
||||
now := time.now_ns() / 1000000000
|
||||
token.payload.nbf <= now
|
||||
now < token.payload.exp
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
http_request.method == "GET"
|
||||
token.payload.role == "guest"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
action_allowed if {
|
||||
http_request.method == "GET"
|
||||
token.payload.role == "guest"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
http_request.method == "GET"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
action_allowed if {
|
||||
http_request.method == "GET"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
}
|
||||
|
||||
action_allowed {
|
||||
http_request.method == "POST"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
|
||||
action_allowed if {
|
||||
http_request.method == "POST"
|
||||
token.payload.role == "admin"
|
||||
glob.match("/people", ["/"], http_request.path)
|
||||
lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub)
|
||||
}
|
||||
|
||||
token := {"valid": valid, "payload": payload} {
|
||||
[_, encoded] := split(http_request.headers.authorization, " ")
|
||||
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
|
||||
token := {"valid": valid, "payload": payload} if {
|
||||
[_, encoded] := split(http_request.headers.authorization, " ")
|
||||
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
+155
-130
@@ -7,6 +7,7 @@ toc: true
|
||||
|
||||
```live:eg:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
OPA is purpose built for reasoning about information represented in structured
|
||||
@@ -23,6 +24,17 @@ Rego queries are assertions on data stored in OPA. These queries can be used to
|
||||
define policies that enumerate instances of data that violate the expected state
|
||||
of the system.
|
||||
|
||||
{{< info >}}
|
||||
The examples in this section try to represent the best practices. As such, they
|
||||
make use of keywords that are meant to become standard keywords at some point in
|
||||
time, but have been introduced gradually. These _future keywords_ can be enabled
|
||||
using
|
||||
|
||||
```live:eg/import:module:read_only
|
||||
import future.keywords
|
||||
```
|
||||
{{< /info >}}
|
||||
|
||||
## Why use Rego?
|
||||
|
||||
Use Rego for defining policy that is easy to read and write.
|
||||
@@ -77,10 +89,10 @@ rect == {"height": 4, "width": 2}
|
||||
```live:eg/rect/compare:output
|
||||
```
|
||||
|
||||
You can define a new concept using a rule. For example, `v` below is true if the equality expression is true.
|
||||
You can define a new concept using a rule. For example, `v` below is true if the equality expression is true.
|
||||
|
||||
```live:eg/undefined:module
|
||||
v { "hello" == "world" }
|
||||
v if "hello" == "world"
|
||||
```
|
||||
|
||||
If we evaluate `v`, the result is `undefined` because the body of the rule never
|
||||
@@ -109,13 +121,25 @@ v != true
|
||||
We can define rules in terms of [Variables](#variables) as well:
|
||||
|
||||
```live:eg/rules:module
|
||||
t { x := 42; y := 41; x > y }
|
||||
t if { x := 42; y := 41; x > y }
|
||||
```
|
||||
|
||||
The formal syntax uses the semicolon character `;` to separate expressions. Rule
|
||||
bodies can separate expressions with newlines and omit the semicolon:
|
||||
|
||||
```live:eg/rules/newlines:module:read_only
|
||||
t2 if {
|
||||
x := 42
|
||||
y := 41
|
||||
x > y
|
||||
}
|
||||
```
|
||||
|
||||
Note that the future keyword `if` is optional. We could have written `v` and `t2` like this:
|
||||
|
||||
```live:eg/rules/newlines_no_if:module:read_only
|
||||
v { "hello" == "world" }
|
||||
|
||||
t2 {
|
||||
x := 42
|
||||
y := 41
|
||||
@@ -154,7 +178,10 @@ sites := [{"name": "prod"}, {"name": "smoke1"}, {"name": "dev"}]
|
||||
```
|
||||
And
|
||||
```live:eg/references/basic:module
|
||||
r { sites[_].name == "prod" }
|
||||
r if {
|
||||
some site in sites
|
||||
site.name == "prod"
|
||||
}
|
||||
```
|
||||
|
||||
The rule `r` above asserts that there exists (at least) one document within `sites` where the `name` attribute equals `"prod"`.
|
||||
@@ -170,7 +197,10 @@ r
|
||||
We can generalize the example above with a rule that defines a set document instead of a boolean document:
|
||||
|
||||
```live:eg/references/helper:module
|
||||
q[name] { name := sites[_].name }
|
||||
q contains name if {
|
||||
some site in sites
|
||||
name := site.name
|
||||
}
|
||||
```
|
||||
|
||||
The value of `q` is a set of names
|
||||
@@ -184,7 +214,7 @@ q
|
||||
We can re-write the rule `r` from above to make use of `q`. We will call the new rule `p`:
|
||||
|
||||
```live:eg/references/helper/composed:module
|
||||
p { q["prod"] }
|
||||
p if q["prod"]
|
||||
```
|
||||
|
||||
Querying `p` will have the same result:
|
||||
@@ -370,7 +400,10 @@ sites := [
|
||||
{"name": "dev"}
|
||||
]
|
||||
|
||||
q[name] { name := sites[_].name }
|
||||
q contains name if {
|
||||
some site in sites
|
||||
name := site.name
|
||||
}
|
||||
```
|
||||
|
||||
In this case, we evaluate `q` with a variable `x` (which is not bound to a value). As a result, the query returns all of the values for `x` and all of the values for `q[x]`, which are always the same because `q` is a set.
|
||||
@@ -582,7 +615,7 @@ Array Comprehensions build array values out of sub-queries. Array Comprehensions
|
||||
For example, the following rule defines an object where the keys are application names and the values are hostnames of servers where the application is deployed. The hostnames of servers are represented as an array.
|
||||
|
||||
```live:eg/data/array_comprehension:module
|
||||
app_to_hostnames[app_name] := hostnames {
|
||||
app_to_hostnames[app_name] := hostnames if {
|
||||
app := apps[_]
|
||||
app_name := app.name
|
||||
hostnames := [hostname | name := app.servers[_]
|
||||
@@ -670,9 +703,12 @@ The sample code in this section make use of the data defined in [Examples](#exam
|
||||
|
||||
The following rule defines a set containing the hostnames of all servers:
|
||||
|
||||
```live:eg/data/rules:module
|
||||
hostnames[name] { name := sites[_].servers[_].hostname }
|
||||
```live:eg/data/rules:module:read_only
|
||||
hostnames contains name if {
|
||||
name := sites[_].servers[_].hostname
|
||||
}
|
||||
```
|
||||
Note that the (future) keyword `if` is optional here.
|
||||
|
||||
When we query for the content of `hostnames` we see the same data as we would if we queried using the `sites[_].servers[_].hostname` reference directly:
|
||||
|
||||
@@ -701,7 +737,7 @@ Third, the `name := sites[_].servers[_].hostname` expression binds the value of
|
||||
Rules that define objects are very similar to rules that define sets.
|
||||
|
||||
```live:eg/data/rule_objects:module
|
||||
apps_by_hostname[hostname] := app {
|
||||
apps_by_hostname[hostname] := app if {
|
||||
some i
|
||||
server := sites[_].servers[_]
|
||||
hostname := server.hostname
|
||||
@@ -731,12 +767,12 @@ For example, we can write a rule that abstracts over our `servers` and
|
||||
`containers` data as `instances`:
|
||||
|
||||
```live:eg/data/incremental_rule:module
|
||||
instances[instance] {
|
||||
instances contains instance if {
|
||||
server := sites[_].servers[_]
|
||||
instance := {"address": server.hostname, "name": server.name}
|
||||
}
|
||||
|
||||
instances[instance] {
|
||||
instances contains instance if {
|
||||
container := containers[_]
|
||||
instance := {"address": container.ipaddress, "name": container.name}
|
||||
}
|
||||
@@ -746,7 +782,7 @@ If the head of the rule is same, we can chain multiple rule bodies together to
|
||||
obtain the same result. We don't recommend using this form anymore.
|
||||
|
||||
```live:eg/data/incremental_rule_nr:module:read_only
|
||||
instances[instance] {
|
||||
instances contains instance if {
|
||||
server := sites[_].servers[_]
|
||||
instance := {"address": server.hostname, "name": server.name}
|
||||
} {
|
||||
@@ -795,10 +831,10 @@ power_users := {"alice", "bob", "fred"}
|
||||
restricted_users := {"bob", "kim"}
|
||||
|
||||
# Power users get 32GB memory.
|
||||
max_memory := 32 { power_users[user] }
|
||||
max_memory := 32 if power_users[user]
|
||||
|
||||
# Restricted users get 4GB memory.
|
||||
max_memory := 4 { restricted_users[user] }
|
||||
max_memory := 4 if restricted_users[user]
|
||||
```
|
||||
|
||||
Error:
|
||||
@@ -825,7 +861,7 @@ Rego supports user-defined functions that can be called with the same semantics
|
||||
For example, the following function will return the result of trimming the spaces from a string and then splitting it by periods.
|
||||
|
||||
```live:eg/basic_function:module:merge_down
|
||||
trim_and_split(s) := x {
|
||||
trim_and_split(s) := x if {
|
||||
t := trim(s, " ")
|
||||
x := split(t, ".")
|
||||
}
|
||||
@@ -839,7 +875,7 @@ trim_and_split(" foo.bar ")
|
||||
Functions may have an arbitrary number of inputs, but exactly one output. Function arguments may be any kind of term. For example, suppose we have the following function:
|
||||
|
||||
```live:eg/function_input:module:read_only
|
||||
foo([x, {"bar": y}]) := z {
|
||||
foo([x, {"bar": y}]) := z if {
|
||||
z := {x: y}
|
||||
}
|
||||
```
|
||||
@@ -853,21 +889,25 @@ The following calls would produce the logical mappings given:
|
||||
| ``z := foo(["5", {"bar": [1, 2, 3, ["foo", "bar"]]}])`` | ``"5"`` | ``[1, 2, 3, ["foo", "bar"]]`` |
|
||||
|
||||
|
||||
If you need multiple outputs, write your functions so that the output is an array, object or set containing your results. If the output term is omitted, it is equivalent to having the output term be the literal `true`. That is, the function declarations below are equivalent:
|
||||
```live:eg/function_output_unset:module:read_only
|
||||
f(x) {
|
||||
x == "foo"
|
||||
}
|
||||
If you need multiple outputs, write your functions so that the output is an array, object or set
|
||||
containing your results. If the output term is omitted, it is equivalent to having the output term
|
||||
be the literal `true`. Furthermore, `if` can be used to write shorter definitions. That is, the
|
||||
function declarations below are equivalent:
|
||||
|
||||
f(x) = true {
|
||||
x == "foo"
|
||||
}
|
||||
```live:eg/function_output_unset:module:read_only
|
||||
f(x) { x == "foo" }
|
||||
f(x) if { x == "foo" }
|
||||
f(x) if x == "foo"
|
||||
|
||||
f(x) := true { x == "foo" }
|
||||
f(x) := true if { x == "foo" }
|
||||
f(x) := true if x == "foo"
|
||||
```
|
||||
|
||||
The outputs of user functions have some additional limitations, namely that they must resolve to a single value. If you write a function that has multiple possible bindings for an output variable, you will get a conflict error:
|
||||
|
||||
```live:eg/function_single_output:module:merge_down
|
||||
p(x) := y {
|
||||
p(x) := y if {
|
||||
y := x[_]
|
||||
}
|
||||
```
|
||||
@@ -882,10 +922,10 @@ It is possible in Rego to define a function more than once, to achieve a conditi
|
||||
Functions can be defined incrementally.
|
||||
|
||||
```live:eg/double_function_define:module
|
||||
q(1, x) := y {
|
||||
q(1, x) := y if {
|
||||
y := x
|
||||
}
|
||||
q(2, x) := y {
|
||||
q(2, x) := y if {
|
||||
y := x*4
|
||||
}
|
||||
```
|
||||
@@ -905,11 +945,11 @@ q(2, 2)
|
||||
A given function call will execute all functions that match the signature given. If a call matches multiple functions, they must produce the same output, or else a conflict error will occur:
|
||||
|
||||
```live:eg/double_function_define_diff_out:module
|
||||
r(1, x) := y {
|
||||
r(1, x) := y if {
|
||||
y := x
|
||||
}
|
||||
|
||||
r(x, 2) := y {
|
||||
r(x, 2) := y if {
|
||||
y := x*4
|
||||
}
|
||||
```
|
||||
@@ -922,7 +962,7 @@ r(1, 2)
|
||||
|
||||
On the other hand, if a call matches no functions, then the result is undefined.
|
||||
```live:eg/double_function_define_undefined:module
|
||||
s(x, 2) := y {
|
||||
s(x, 2) := y if {
|
||||
y := x * 4
|
||||
}
|
||||
```
|
||||
@@ -952,7 +992,7 @@ For safety, a variable appearing in a negated expression must also appear in ano
|
||||
The simplest use of negation involves only scalar values or variables and is equivalent to complementing the operator:
|
||||
|
||||
```live:eg/simple_negation:module
|
||||
t {
|
||||
t if {
|
||||
greeting := "hello"
|
||||
not greeting == "goodbye"
|
||||
}
|
||||
@@ -971,21 +1011,24 @@ Negation is required to check whether some value *does not* exist in a collectio
|
||||
For example, we can write a rule that defines a document containing names of apps not deployed on the `"prod"` site:
|
||||
|
||||
```live:eg/data/negation:module
|
||||
prod_servers[name] {
|
||||
site := sites[_]
|
||||
prod_servers contains name if {
|
||||
some site in sites
|
||||
site.name == "prod"
|
||||
name := site.servers[_].name
|
||||
some server in site.servers
|
||||
name := server.name
|
||||
}
|
||||
|
||||
apps_in_prod[name] {
|
||||
app := apps[_]
|
||||
server := app.servers[_]
|
||||
prod_servers[server]
|
||||
apps_in_prod contains name if {
|
||||
some site in sites
|
||||
some app in apps
|
||||
name := app.name
|
||||
some server in app.servers
|
||||
prod_servers[server]
|
||||
}
|
||||
|
||||
apps_not_in_prod[name] {
|
||||
name := apps[_].name
|
||||
apps_not_in_prod contains name if {
|
||||
some app in apps
|
||||
name := app.name
|
||||
not apps_in_prod[name]
|
||||
}
|
||||
```
|
||||
@@ -1013,7 +1056,7 @@ The most expressive way to state this in Rego is using the `every` keyword:
|
||||
```live:eg/data/every_alternative:module:read_only
|
||||
import future.keywords.every
|
||||
|
||||
no_bitcoin_miners_using_every {
|
||||
no_bitcoin_miners_using_every if {
|
||||
every app in apps {
|
||||
app.name != "bitcoin-miner"
|
||||
}
|
||||
@@ -1042,13 +1085,9 @@ quantifier.
|
||||
For example:
|
||||
|
||||
```live:eg/data/correct_negation:module
|
||||
import future.keywords.in
|
||||
no_bitcoin_miners_using_negation if not any_bitcoin_miners
|
||||
|
||||
no_bitcoin_miners_using_negation {
|
||||
not any_bitcoin_miners
|
||||
}
|
||||
|
||||
any_bitcoin_miners {
|
||||
any_bitcoin_miners if {
|
||||
some app in apps
|
||||
app.name == "bitcoin-miner"
|
||||
}
|
||||
@@ -1076,7 +1115,7 @@ A common mistake is to try encoding the policy with a rule named `no_bitcoin_min
|
||||
like so:
|
||||
|
||||
```live:eg/data/incorrect_no_bitcoin:module:read_only
|
||||
no_bitcoin_miners {
|
||||
no_bitcoin_miners if {
|
||||
app := apps[_]
|
||||
app.name != "bitcoin-miner" # THIS IS NOT CORRECT.
|
||||
}
|
||||
@@ -1087,9 +1126,7 @@ keyword, because the rule is true whenever there is SOME app that is not a
|
||||
bitcoin-miner:
|
||||
|
||||
```live:eg/data/incorrect_no_bitcoin_some:module
|
||||
import future.keywords.in
|
||||
|
||||
no_bitcoin_miners {
|
||||
no_bitcoin_miners if {
|
||||
some app in apps
|
||||
app.name != "bitcoin-miner"
|
||||
}
|
||||
@@ -1113,7 +1150,7 @@ Alternatively, we can implement the same kind of logic inside a single rule
|
||||
using [Comprehensions](#comprehensions).
|
||||
|
||||
```live:eg/data/comprehesion_alternative:module:read_only
|
||||
no_bitcoin_miners_using_comprehension {
|
||||
no_bitcoin_miners_using_comprehension if {
|
||||
bitcoin_miners := {app | some app in apps; app.name == "bitcoin-miner"}
|
||||
count(bitcoin_miners) == 0
|
||||
}
|
||||
@@ -1191,11 +1228,11 @@ Modules use the same syntax to declare dependencies on [Base and Virtual Documen
|
||||
|
||||
```live:import_data:module:read_only
|
||||
package opa.examples
|
||||
import future.keywords.in
|
||||
import future.keywords # uses 'in' and 'contains' and 'if'
|
||||
|
||||
import data.servers
|
||||
|
||||
http_servers[server] {
|
||||
http_servers contains server if {
|
||||
some server in servers
|
||||
"http" in server.protocols
|
||||
}
|
||||
@@ -1205,28 +1242,28 @@ Similarly, modules can declare dependencies on query arguments by specifying an
|
||||
|
||||
```live:import_input:module:read_only
|
||||
package opa.examples
|
||||
import future.keywords.in
|
||||
import future.keywords
|
||||
|
||||
import input.user
|
||||
import input.method
|
||||
|
||||
# allow alice to perform any operation.
|
||||
allow { user == "alice" }
|
||||
allow if user == "alice"
|
||||
|
||||
# allow bob to perform read-only operations.
|
||||
allow {
|
||||
allow if {
|
||||
user == "bob"
|
||||
method == "GET"
|
||||
}
|
||||
|
||||
# allows users assigned a "dev" role to perform read-only operations.
|
||||
allow {
|
||||
allow if {
|
||||
method == "GET"
|
||||
input.user in data.roles["dev"]
|
||||
}
|
||||
|
||||
# allows user catherine access on Saturday and Sunday
|
||||
allow {
|
||||
allow if {
|
||||
user == "catherine"
|
||||
day := time.weekday(time.now_ns())
|
||||
day in ["Saturday", "Sunday"]
|
||||
@@ -1237,11 +1274,11 @@ Imports can include an optional `as` keyword to handle namespacing issues:
|
||||
|
||||
```live:import_namespacing:module:read_only
|
||||
package opa.examples
|
||||
import future.keywords.in
|
||||
import future.keywords
|
||||
|
||||
import data.servers as my_servers
|
||||
|
||||
http_servers[server] {
|
||||
http_servers contains server if {
|
||||
some server in my_servers
|
||||
"http" in server.protocols
|
||||
}
|
||||
@@ -1264,10 +1301,10 @@ the "west" region that contain "db" in their name. The first element in the
|
||||
tuple is the site index and the second element is the server index.
|
||||
|
||||
```live:eg/data/some:module
|
||||
tuples[[i, j]] {
|
||||
tuples contains [i, j] if {
|
||||
some i, j
|
||||
sites[i].region == "west"
|
||||
server := sites[i].servers[j] # note: 'server' is local because it's declared with :=
|
||||
server := sites[i].servers[j] # note: 'server' is local because it's declared with :=
|
||||
contains(server.name, "db")
|
||||
}
|
||||
```
|
||||
@@ -1319,7 +1356,7 @@ variable names.
|
||||
```live:eg/data/every0:module:merge_down
|
||||
import future.keywords.every
|
||||
|
||||
names_with_dev {
|
||||
names_with_dev if {
|
||||
some site in sites
|
||||
site.name == "dev"
|
||||
|
||||
@@ -1353,18 +1390,18 @@ scope of the body evaluation:
|
||||
```live:eg/every1:module:merge_down
|
||||
import future.keywords.every
|
||||
|
||||
array_domain {
|
||||
array_domain if {
|
||||
every i, x in [1, 2, 3] { x-i == 1 } # array domain
|
||||
}
|
||||
|
||||
object_domain {
|
||||
object_domain if {
|
||||
every k, v in {"foo": "bar", "fox": "baz" } { # object domain
|
||||
startswith(k, "f")
|
||||
startswith(v, "b")
|
||||
}
|
||||
}
|
||||
|
||||
set_domain {
|
||||
set_domain if {
|
||||
every x in {1, 2, 3} { x != 4 } # set domain
|
||||
}
|
||||
```
|
||||
@@ -1380,15 +1417,13 @@ import future.keywords.every
|
||||
xs := [2, 2, 4, 8]
|
||||
larger_than_one(x) := x > 1
|
||||
|
||||
rule_every {
|
||||
rule_every if {
|
||||
every x in xs { larger_than_one(x) }
|
||||
}
|
||||
|
||||
not_less_or_equal_one {
|
||||
not lte_one
|
||||
}
|
||||
not_less_or_equal_one if not lte_one
|
||||
|
||||
lte_one {
|
||||
lte_one if {
|
||||
some x in xs
|
||||
not larger_than_one(x)
|
||||
}
|
||||
@@ -1470,17 +1505,17 @@ will see the unmodified value. The exception to this rule is when multiple
|
||||
`with` keywords are in-scope like below:
|
||||
|
||||
```live:multiple_with:module:read_only
|
||||
inner := [x, y] {
|
||||
inner := [x, y] if {
|
||||
x := input.foo
|
||||
y := input.bar
|
||||
}
|
||||
|
||||
middle := [a, b] {
|
||||
middle := [a, b] if {
|
||||
a := inner with input.foo as 100
|
||||
b := input
|
||||
}
|
||||
|
||||
outer := result {
|
||||
outer := result if {
|
||||
result := middle with input as {"foo": 200, "bar": 300}
|
||||
}
|
||||
```
|
||||
@@ -1498,66 +1533,60 @@ Replacement functions can call the function they're replacing **without causing
|
||||
recursion**.
|
||||
See the following example:
|
||||
|
||||
```live:with_builtins:module:read_only
|
||||
package opa.examples
|
||||
import future.keywords.in
|
||||
|
||||
```live:eg/with_builtins:module:read_only
|
||||
f(x) := count(x)
|
||||
|
||||
mock_count(x) := 0 { "x" in x }
|
||||
mock_count(x) := count(x) { not "x" in x }
|
||||
mock_count(x) := 0 if "x" in x
|
||||
mock_count(x) := count(x) if not "x" in x
|
||||
```
|
||||
|
||||
```live:with_builtins/1:query:merge_down
|
||||
```live:eg/with_builtins/1:query:merge_down
|
||||
f([1, 2, 3]) with count as mock_count
|
||||
```
|
||||
```live:with_builtins/1:output
|
||||
```live:eg/with_builtins/1:output
|
||||
```
|
||||
|
||||
```live:with_builtins/2:query:merge_down
|
||||
```live:eg/with_builtins/2:query:merge_down
|
||||
f(["x", "y", "z"]) with count as mock_count
|
||||
```
|
||||
```live:with_builtins/2:output
|
||||
```live:eg/with_builtins/2:output
|
||||
```
|
||||
|
||||
Each replacement function evaluation will start a new scope: it's valid to use
|
||||
`with <builtin1> as ...` in the body of the replacement function -- for example:
|
||||
|
||||
```live:with_builtins_nested:module:read_only
|
||||
package opa.examples
|
||||
import future.keywords.in
|
||||
|
||||
f(x) := count(x) {
|
||||
```live:eg/with_builtins_nested:module:read_only
|
||||
f(x) := count(x) if {
|
||||
rule_using_concat with concat as "foo,bar"
|
||||
}
|
||||
|
||||
mock_count(x) := 0 { "x" in x }
|
||||
mock_count(x) := count(x) { not "x" in x }
|
||||
mock_count(x) := 0 if "x" in x
|
||||
mock_count(x) := count(x) if not "x" in x
|
||||
|
||||
rule_using_concat {
|
||||
rule_using_concat if {
|
||||
concat(",", input.x) == "foo,bar"
|
||||
}
|
||||
```
|
||||
```live:with_builtins_nested/1:query:merge_down
|
||||
```live:eg/with_builtins_nested/1:query:merge_down
|
||||
f(["x", "y", "z"]) with count as mock_count with input.x as ["baz"]
|
||||
```
|
||||
```live:with_builtins_nested/1:output
|
||||
```live:eg/with_builtins_nested/1:output
|
||||
```
|
||||
|
||||
Note that function replacement via `with` does not affect the evaluation of
|
||||
the function arguments: if `input.x` is undefined, the replacement of `concat`
|
||||
does not change the result of the evaluation:
|
||||
|
||||
```live:with_builtins_nested/2:query:merge_down
|
||||
```live:eg/with_builtins_nested/2:query:merge_down
|
||||
count(input.x) with count as 3 with input.x as ["x"]
|
||||
```
|
||||
```live:with_builtins_nested/2:output
|
||||
```live:eg/with_builtins_nested/2:output
|
||||
```
|
||||
|
||||
```live:with_builtins_nested/3:query:merge_down
|
||||
```live:eg/with_builtins_nested/3:query:merge_down
|
||||
count(input.x) with count as 3 with input as {}
|
||||
```
|
||||
```live:with_builtins_nested/3:output:expect_undefined
|
||||
```live:eg/with_builtins_nested/3:output:expect_undefined
|
||||
```
|
||||
|
||||
## Default Keyword
|
||||
@@ -1571,14 +1600,12 @@ For example:
|
||||
```live:eg/default:module
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
input.user == "bob"
|
||||
input.method == "GET"
|
||||
}
|
||||
|
||||
allow {
|
||||
input.user == "alice"
|
||||
}
|
||||
allow if input.user == "alice"
|
||||
```
|
||||
|
||||
When the `allow` document is queried, the return value will be either `true` or `false`.
|
||||
@@ -1620,7 +1647,7 @@ The ``else`` keyword is useful if you are porting policies into Rego from an
|
||||
order-sensitive system like IPTables.
|
||||
|
||||
```live:eg/else:module
|
||||
authorize := "allow" {
|
||||
authorize := "allow" if {
|
||||
input.user == "superuser" # allow 'superuser' to perform any operation.
|
||||
} else := "deny" {
|
||||
input.path[0] == "admin" # disallow 'admin' operations...
|
||||
@@ -1687,7 +1714,7 @@ The membership operator `in` lets you check if an element is part of a collectio
|
||||
```live:eg/member1:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
p := [x, y, z] {
|
||||
p := [x, y, z] if {
|
||||
x := 3 in [1, 2, 3] # array
|
||||
y := 3 in {1, 2, 3} # set
|
||||
z := 3 in {"foo": 1, "bar": 3} # object
|
||||
@@ -1703,7 +1730,7 @@ taken to be the key (object) or index (array), respectively:
|
||||
```live:eg/member1c:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
p := [x, y] {
|
||||
p := [x, y] if {
|
||||
x := "foo", "bar" in {"foo": "bar"} # key, val with object
|
||||
y := 2, "baz" in ["foo", "bar", "baz"] # key, val with array
|
||||
}
|
||||
@@ -1718,16 +1745,16 @@ arguments -- compare:
|
||||
```live:eg/member1d:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
p := x {
|
||||
p := x if {
|
||||
x := { 0, 2 in [2] }
|
||||
}
|
||||
q := x {
|
||||
q := x if {
|
||||
x := { (0, 2 in [2]) }
|
||||
}
|
||||
w := x {
|
||||
w := x if {
|
||||
x := g((0, 2 in [2]))
|
||||
}
|
||||
z := x {
|
||||
z := x if {
|
||||
x := f(0, 2 in [2])
|
||||
}
|
||||
|
||||
@@ -1743,9 +1770,7 @@ member of an array:
|
||||
```live:eg/member1a:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
deny {
|
||||
not "admin" in input.user.roles
|
||||
}
|
||||
deny if not "admin" in input.user.roles
|
||||
|
||||
test_deny {
|
||||
deny with input.user.roles as ["operator", "user"]
|
||||
@@ -1760,7 +1785,7 @@ when called in non-collection arguments:
|
||||
```live:eg/member1b:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
q := x {
|
||||
q := x if {
|
||||
x := 3 in "three"
|
||||
}
|
||||
```
|
||||
@@ -1795,11 +1820,11 @@ p[x] {
|
||||
some x, "r" in ["a", "r", "r", "a", "y"] # key variable, value constant
|
||||
}
|
||||
|
||||
q[x] = y {
|
||||
q[x] = y if {
|
||||
some x, y in ["a", "r", "r", "a", "y"] # both variables
|
||||
}
|
||||
|
||||
r[y] = x {
|
||||
r[y] = x if {
|
||||
some x, y in {"foo": "bar", "baz": "quz"}
|
||||
}
|
||||
```
|
||||
@@ -1811,10 +1836,10 @@ Any argument to the `some` variant can be a composite, non-ground value:
|
||||
```live:eg/member4:module:merge_down
|
||||
import future.keywords.in
|
||||
|
||||
p[x] = y {
|
||||
p[x] = y if {
|
||||
some x, {"foo": y} in [{"foo": 100}, {"bar": 200}]
|
||||
}
|
||||
p[x] = y {
|
||||
p[x] = y if {
|
||||
some {"bar": x}, {"foo": y} in {{"bar": "b"}: {"foo": "f"}}
|
||||
}
|
||||
```
|
||||
@@ -1832,7 +1857,7 @@ The assignment operator (`:=`) is used to assign values to variables. Variables
|
||||
```live:eg/assignment1:module:read_only
|
||||
x := 100
|
||||
|
||||
p {
|
||||
p if {
|
||||
x := 1 # declare local variable 'x' and assign value 1
|
||||
x != 100 # true because 'x' refers to local variable
|
||||
}
|
||||
@@ -1842,12 +1867,12 @@ Assigned variables are not allowed to appear before the assignment in the
|
||||
query. For example, the following policy will not compile:
|
||||
|
||||
```live:eg/assignment2:module:merge_down
|
||||
p {
|
||||
p if {
|
||||
x != 100
|
||||
x := 1 # error because x appears earlier in the query.
|
||||
}
|
||||
|
||||
q {
|
||||
q if {
|
||||
x := 1
|
||||
x := 2 # error because x is assigned twice.
|
||||
}
|
||||
@@ -1860,7 +1885,7 @@ A simple form of destructuring can be used to unpack values from arrays and assi
|
||||
```live:eg/assignment3:module:read_only
|
||||
address := ["3 Abbey Road", "NW8 9AY", "London", "England"]
|
||||
|
||||
in_london {
|
||||
in_london if {
|
||||
[_, _, city, country] := address
|
||||
city == "London"
|
||||
country == "England"
|
||||
@@ -1875,7 +1900,7 @@ in_london {
|
||||
Comparison checks if two values are equal within a rule. If the left or right hand side contains a variable that has not been assigned a value, the compiler throws an error.
|
||||
|
||||
```live:eg/comparison1:module:merge_down
|
||||
p {
|
||||
p if {
|
||||
x := 100
|
||||
x == 100 # true because x refers to the local variable
|
||||
}
|
||||
@@ -1885,7 +1910,7 @@ p {
|
||||
|
||||
```live:eg/comparison2:module:merge_down
|
||||
y := 100
|
||||
q {
|
||||
q if {
|
||||
y == 100 # true because y refers to the global variable
|
||||
}
|
||||
```
|
||||
@@ -1893,7 +1918,7 @@ q {
|
||||
```
|
||||
|
||||
```live:eg/comparison3:module:merge_down
|
||||
r {
|
||||
r if {
|
||||
z == 100 # compiler error because z has not been assigned a value
|
||||
}
|
||||
```
|
||||
@@ -1920,7 +1945,7 @@ sites[i].servers[j].name = apps[k].servers[m]
|
||||
As opposed to when assignment (`:=`) is used, the order of expressions in a rule does not affect the document’s content.
|
||||
|
||||
```live:eg/expression_order:module
|
||||
s {
|
||||
s if {
|
||||
x > y
|
||||
y = 41
|
||||
x = 42
|
||||
@@ -2003,13 +2028,13 @@ logic. If error handling is required, the built-in function call can be negated
|
||||
to test for undefined. For example:
|
||||
|
||||
```live:eg/errors:module:merge_down
|
||||
allow {
|
||||
allow if {
|
||||
io.jwt.verify_hs256(input.token, "secret")
|
||||
[_, payload, _] := io.jwt.decode(input.token)
|
||||
payload.role == "admin"
|
||||
}
|
||||
|
||||
reason["invalid JWT supplied as input"] {
|
||||
reason contains "invalid JWT supplied as input" if {
|
||||
not io.jwt.decode(input.token)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -103,6 +103,11 @@ some [x, "b", z] in a_set
|
||||
|
||||
## Iteration
|
||||
|
||||
```live:iteration:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
### Arrays
|
||||
|
||||
```live:iteration/arrays:query:read_only
|
||||
@@ -182,13 +187,13 @@ not any_not_match
|
||||
```
|
||||
|
||||
```live:iteration/forall:module:read_only
|
||||
any_match {
|
||||
set[x]
|
||||
any_match if {
|
||||
some x in set
|
||||
f(x)
|
||||
}
|
||||
|
||||
any_not_match {
|
||||
set[x]
|
||||
any_not_match if {
|
||||
some x in set
|
||||
not f(x)
|
||||
}
|
||||
```
|
||||
@@ -197,6 +202,11 @@ any_not_match {
|
||||
|
||||
In the examples below `...` represents one or more conditions.
|
||||
|
||||
```live:rules:module:hidden
|
||||
package example
|
||||
import future.keywords
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
```live:rules/constants:module:read_only
|
||||
@@ -212,7 +222,9 @@ c := a | b
|
||||
p := true { ... }
|
||||
|
||||
# OR
|
||||
p if { ... }
|
||||
|
||||
# OR
|
||||
p { ... }
|
||||
```
|
||||
|
||||
@@ -220,8 +232,8 @@ p { ... }
|
||||
|
||||
```live:rules/cond:module:read_only
|
||||
default a := 1
|
||||
a := 5 { ... }
|
||||
a := 100 { ... }
|
||||
a := 5 if { ... }
|
||||
a := 100 if { ... }
|
||||
```
|
||||
|
||||
### Incremental
|
||||
@@ -231,29 +243,33 @@ a := 100 { ... }
|
||||
a_set[x] { ... }
|
||||
a_set[y] { ... }
|
||||
|
||||
# alternatively, with future.keywords
|
||||
a_set contains x if { ... }
|
||||
a_set contains y if { ... }
|
||||
|
||||
# a_map will contain key->value pairs x->y and w->z
|
||||
a_map[x] := y { ... }
|
||||
a_map[w] := z { ... }
|
||||
a_map[x] := y if { ... }
|
||||
a_map[w] := z if { ... }
|
||||
```
|
||||
|
||||
### Ordered (Else)
|
||||
|
||||
```live:rules/ordered:module:read_only
|
||||
default a := 1
|
||||
a := 5 { ... }
|
||||
a := 5 if { ... }
|
||||
else := 10 { ... }
|
||||
```
|
||||
|
||||
### Functions (Boolean)
|
||||
|
||||
```live:rules/funcs:module:read_only
|
||||
f(x, y) {
|
||||
f(x, y) if {
|
||||
...
|
||||
}
|
||||
|
||||
# OR
|
||||
|
||||
f(x, y) := true {
|
||||
f(x, y) := true if {
|
||||
...
|
||||
}
|
||||
```
|
||||
@@ -261,9 +277,9 @@ f(x, y) := true {
|
||||
### Functions (Conditionals)
|
||||
|
||||
```live:rules/condfuncs:module:read_only
|
||||
f(x) := "A" { x >= 90 }
|
||||
f(x) := "B" { x >= 80; x < 90 }
|
||||
f(x) := "C" { x >= 70; x < 80 }
|
||||
f(x) := "A" if { x >= 90 }
|
||||
f(x) := "B" if { x >= 80; x < 90 }
|
||||
f(x) := "C" if { x >= 70; x < 80 }
|
||||
```
|
||||
|
||||
## Tests
|
||||
@@ -1055,7 +1071,11 @@ package = "package" ref
|
||||
import = "import" ref [ "as" var ]
|
||||
policy = { rule }
|
||||
rule = [ "default" ] rule-head { rule-body }
|
||||
rule-head = var [ "(" rule-args ")" ] [ "[" term "]" ] [ ( ":=" | "=" ) term ]
|
||||
rule-head = var ( rule-head-set | rule-head-obj | rule-head-func | rule-head-comp )
|
||||
rule-head-comp = [ ( ":=" | "=" ) term ]") [ "if" ]
|
||||
rule-head-obj = [ "[" term "]" ] [ ( ":=" | "=" ) term ]) [ "if" ]
|
||||
rule-head-func = [ "(" rule-args ")" ] [ ( ":=" | "=" ) term ]) [ "if" ]
|
||||
rule-head-set = "contains" term [ "if" ] | "[" term "]"
|
||||
rule-args = term { "," term }
|
||||
rule-body = [ "else" [ ( ":=" | "=" ) term ] ] "{" query "}"
|
||||
query = literal { ( ";" | ( [CR] LF ) ) literal }
|
||||
@@ -1090,6 +1110,8 @@ non-empty-set = "{" term { "," term } "}"
|
||||
empty-set = "set(" ")"
|
||||
```
|
||||
|
||||
Note that the grammar corresponds to Rego with all future keywords enabled.
|
||||
|
||||
The grammar defined above makes use of the following syntax. See [the Wikipedia page on EBNF](https://en.wikipedia.org/wiki/Extended_Backus–Naur_Form) for more details:
|
||||
|
||||
```
|
||||
|
||||
@@ -14,6 +14,17 @@ framework that you can use to write _tests_ for your policies. By writing
|
||||
tests for your policies you can speed up the development process of new rules
|
||||
and reduce the amount of time it takes to modify rules as requirements evolve.
|
||||
|
||||
{{< info >}}
|
||||
The examples in this section try to represent the best practices. As such, they
|
||||
make use of keywords that are meant to become standard keywords at some point in
|
||||
time, but have been introduced gradually. These _future keywords_ can be enabled
|
||||
using
|
||||
|
||||
```live:eg/import:module:read_only
|
||||
import future.keywords
|
||||
```
|
||||
{{< /info >}}
|
||||
|
||||
## Getting Started
|
||||
|
||||
Let's use an example to get started. The file below implements a simple
|
||||
@@ -24,13 +35,14 @@ profile.
|
||||
|
||||
```live:example:module:read_only,openable
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
input.path == ["users"]
|
||||
input.method == "POST"
|
||||
}
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
input.path == ["users", input.user_id]
|
||||
input.method == "GET"
|
||||
}
|
||||
@@ -43,19 +55,19 @@ To test this policy, we will create a separate Rego file that contains test case
|
||||
```live:example/test:module:read_only
|
||||
package authz
|
||||
|
||||
test_post_allowed {
|
||||
test_post_allowed if {
|
||||
allow with input as {"path": ["users"], "method": "POST"}
|
||||
}
|
||||
|
||||
test_get_anonymous_denied {
|
||||
test_get_anonymous_denied if {
|
||||
not allow with input as {"path": ["users"], "method": "GET"}
|
||||
}
|
||||
|
||||
test_get_user_allowed {
|
||||
test_get_user_allowed if {
|
||||
allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"}
|
||||
}
|
||||
|
||||
test_get_another_user_denied {
|
||||
test_get_another_user_denied if {
|
||||
not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
|
||||
}
|
||||
```
|
||||
@@ -113,7 +125,7 @@ name is prefixed with `test_`.
|
||||
```live:example_format:module:read_only
|
||||
package mypackage
|
||||
|
||||
test_some_descriptive_name {
|
||||
test_some_descriptive_name if {
|
||||
# test logic
|
||||
}
|
||||
```
|
||||
@@ -145,22 +157,16 @@ by zero condition) the test result is marked as an `ERROR`. Tests prefixed with
|
||||
package example
|
||||
|
||||
# This test will pass.
|
||||
test_ok {
|
||||
true
|
||||
}
|
||||
test_ok if true
|
||||
|
||||
# This test will fail.
|
||||
test_failure {
|
||||
1 == 2
|
||||
}
|
||||
test_failure if 1 == 2
|
||||
|
||||
# This test will error.
|
||||
test_error {
|
||||
1 / 0
|
||||
}
|
||||
test_error if 1 / 0
|
||||
|
||||
# This test will be skipped.
|
||||
todo_test_missing_implementation {
|
||||
todo_test_missing_implementation if {
|
||||
allow with data.roles as ["not", "implemented"]
|
||||
}
|
||||
```
|
||||
@@ -249,7 +255,7 @@ Below is a simple policy that depends on the data document.
|
||||
|
||||
```live:with_keyword:module:read_only,openable
|
||||
package authz
|
||||
import future.keywords.in
|
||||
import future.keywords
|
||||
|
||||
allow {
|
||||
some x in data.policies
|
||||
@@ -257,9 +263,7 @@ allow {
|
||||
matches_role(input.role)
|
||||
}
|
||||
|
||||
matches_role(my_role) {
|
||||
input.user in data.roles[my_role]
|
||||
}
|
||||
matches_role(my_role) if input.user in data.roles[my_role]
|
||||
```
|
||||
|
||||
Below is the Rego file to test the above policy.
|
||||
@@ -268,11 +272,12 @@ Below is the Rego file to test the above policy.
|
||||
|
||||
```live:with_keyword/tests:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
policies := [{"name": "test_policy"}]
|
||||
roles := {"admin": ["alice"]}
|
||||
|
||||
test_allow_with_data {
|
||||
test_allow_with_data if {
|
||||
allow with input as {"user": "alice", "role": "admin"}
|
||||
with data.policies as policies
|
||||
with data.roles as roles
|
||||
@@ -294,22 +299,20 @@ Below is an example to replace a **rule without arguments**.
|
||||
|
||||
```live:with_keyword_rules:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
allow1 {
|
||||
allow2
|
||||
}
|
||||
allow1 if allow2
|
||||
|
||||
allow2 {
|
||||
2 == 1
|
||||
}
|
||||
allow2 if 2 == 1
|
||||
```
|
||||
|
||||
**authz_test.rego**:
|
||||
|
||||
```live:with_keyword_rules/tests:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
test_replace_rule {
|
||||
test_replace_rule if {
|
||||
allow1 with allow2 as true
|
||||
}
|
||||
```
|
||||
@@ -327,10 +330,11 @@ Here is an example to replace a rule's **built-in function** with a user-defined
|
||||
|
||||
```live:with_keyword_builtins:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
import data.jwks.cert
|
||||
|
||||
allow {
|
||||
allow if {
|
||||
[true, _, _] = io.jwt.decode_verify(input.headers["x-token"], {"cert": cert, "iss": "corp.issuer.com"})
|
||||
}
|
||||
```
|
||||
@@ -339,13 +343,12 @@ allow {
|
||||
|
||||
```live:with_keyword_builtins/tests:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
mock_decode_verify("my-jwt", _) := [true, {}, {}]
|
||||
mock_decode_verify(x, _) := [false, {}, {}] {
|
||||
x != "my-jwt"
|
||||
}
|
||||
mock_decode_verify(x, _) := [false, {}, {}] if x != "my-jwt"
|
||||
|
||||
test_allow {
|
||||
test_allow if {
|
||||
allow
|
||||
with input.headers["x-token"] as "my-jwt"
|
||||
with data.jwks.cert as "mock-cert"
|
||||
@@ -363,7 +366,7 @@ PASS: 1/1
|
||||
In simple cases, a function can also be replaced with a value, as in
|
||||
|
||||
```live:with_keyword_builtins/tests/value:module:read_only
|
||||
test_allow_value {
|
||||
test_allow_value if {
|
||||
allow
|
||||
with input.headers["x-token"] as "my-jwt"
|
||||
with data.jwks.cert as "mock-cert"
|
||||
@@ -381,12 +384,13 @@ function by a built-in function.
|
||||
|
||||
```live:with_keyword_funcs:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
replace_rule {
|
||||
replace_rule if {
|
||||
replace(input.label)
|
||||
}
|
||||
|
||||
replace(label) {
|
||||
replace(label) if {
|
||||
label == "test_label"
|
||||
}
|
||||
```
|
||||
@@ -395,8 +399,9 @@ replace(label) {
|
||||
|
||||
```live:with_keyword_funcs/tests:module:read_only
|
||||
package authz
|
||||
import future.keywords
|
||||
|
||||
test_replace_rule {
|
||||
test_replace_rule if {
|
||||
replace_rule with input.label as "does-not-matter" with replace as true
|
||||
}
|
||||
```
|
||||
|
||||
+70
-27
@@ -12,6 +12,7 @@ import (
|
||||
"sort"
|
||||
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/internal/future"
|
||||
)
|
||||
|
||||
// Opts lets you control the code formatting via `AstWithOpts()`.
|
||||
@@ -74,6 +75,17 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
// present.
|
||||
extraFutureKeywordImports := map[string]struct{}{}
|
||||
|
||||
// When the future keyword "contains" is imported, all the pretty-printed
|
||||
// modules will use that format for partial sets.
|
||||
// NOTE(sr): For ref-head rules, this will be the default behaviour, since
|
||||
// we need "contains" to disambiguate complete rules from partial sets.
|
||||
useContainsKW := false
|
||||
|
||||
// Same logic applies as for "contains": if `future.keywords.if` (or all
|
||||
// future keywords) is imported, we'll render rules that can use `if` with
|
||||
// `if`.
|
||||
useIf := false
|
||||
|
||||
// Preprocess the AST. Set any required defaults and calculate
|
||||
// values required for printing the formatted output.
|
||||
ast.WalkNodes(x, func(x ast.Node) bool {
|
||||
@@ -92,6 +104,17 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
case n.IsEvery():
|
||||
extraFutureKeywordImports["every"] = struct{}{}
|
||||
}
|
||||
|
||||
case *ast.Import:
|
||||
switch {
|
||||
case future.IsAllFutureKeywords(n):
|
||||
useContainsKW = true
|
||||
useIf = true
|
||||
case future.IsFutureKeyword(n, "contains"):
|
||||
useContainsKW = true
|
||||
case future.IsFutureKeyword(n, "if"):
|
||||
useIf = true
|
||||
}
|
||||
}
|
||||
|
||||
if opts.IgnoreLocations || x.Loc() == nil {
|
||||
@@ -109,15 +132,20 @@ func AstWithOpts(x interface{}, opts Opts) ([]byte, error) {
|
||||
for kw := range extraFutureKeywordImports {
|
||||
x.Imports = ensureFutureKeywordImport(x.Imports, kw)
|
||||
}
|
||||
w.writeModule(x)
|
||||
w.writeModule(x, useContainsKW, useIf)
|
||||
case *ast.Package:
|
||||
w.writePackage(x, nil)
|
||||
case *ast.Import:
|
||||
w.writeImports([]*ast.Import{x}, nil)
|
||||
case *ast.Rule:
|
||||
w.writeRule(x, false, nil)
|
||||
w.writeRule(x, false /* isElse */, useContainsKW, useIf, nil)
|
||||
case *ast.Head:
|
||||
w.writeHead(x, false, false, nil)
|
||||
w.writeHead(x,
|
||||
false, // isDefault
|
||||
false, // isExpandedConst
|
||||
useContainsKW,
|
||||
useIf,
|
||||
nil)
|
||||
case ast.Body:
|
||||
w.writeBody(x, nil)
|
||||
case *ast.Expr:
|
||||
@@ -186,7 +214,7 @@ type writer struct {
|
||||
delay bool
|
||||
}
|
||||
|
||||
func (w *writer) writeModule(module *ast.Module) {
|
||||
func (w *writer) writeModule(module *ast.Module, useContainsKW, useIf bool) {
|
||||
var pkg *ast.Package
|
||||
var others []interface{}
|
||||
var comments []*ast.Comment
|
||||
@@ -225,7 +253,7 @@ func (w *writer) writeModule(module *ast.Module) {
|
||||
imports, others = gatherImports(others)
|
||||
comments = w.writeImports(imports, comments)
|
||||
rules, others = gatherRules(others)
|
||||
comments = w.writeRules(rules, comments)
|
||||
comments = w.writeRules(rules, useContainsKW, useIf, comments)
|
||||
}
|
||||
|
||||
for i, c := range comments {
|
||||
@@ -255,16 +283,16 @@ func (w *writer) writeComments(comments []*ast.Comment) {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *writer) writeRules(rules []*ast.Rule, comments []*ast.Comment) []*ast.Comment {
|
||||
func (w *writer) writeRules(rules []*ast.Rule, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
|
||||
for _, rule := range rules {
|
||||
comments = w.insertComments(comments, rule.Location)
|
||||
comments = w.writeRule(rule, false, comments)
|
||||
comments = w.writeRule(rule, false, useContainsKW, useIf, comments)
|
||||
w.blankLine()
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment) []*ast.Comment {
|
||||
func (w *writer) writeRule(rule *ast.Rule, isElse, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
|
||||
if rule == nil {
|
||||
return comments
|
||||
}
|
||||
@@ -283,13 +311,30 @@ func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment)
|
||||
// pretend that the rule has no body in this case.
|
||||
isExpandedConst := rule.Body.Equal(ast.NewBody(ast.NewExpr(ast.BooleanTerm(true)))) && rule.Else == nil
|
||||
|
||||
comments = w.writeHead(rule.Head, rule.Default, isExpandedConst, comments)
|
||||
comments = w.writeHead(rule.Head, rule.Default, isExpandedConst, useContainsKW, useIf, comments)
|
||||
|
||||
// this excludes partial sets UNLESS `contains` is used
|
||||
partialSetException := useContainsKW || rule.Head.Value != nil
|
||||
|
||||
if (len(rule.Body) == 0 || isExpandedConst) && !isElse {
|
||||
w.endLine()
|
||||
return comments
|
||||
}
|
||||
|
||||
if useIf && partialSetException && !isElse {
|
||||
w.write(" if")
|
||||
if len(rule.Body) == 1 {
|
||||
if rule.Body[0].Location.Row == rule.Head.Location.Row {
|
||||
w.write(" ")
|
||||
comments = w.writeExpr(rule.Body[0], comments)
|
||||
w.endLine()
|
||||
if rule.Else != nil {
|
||||
comments = w.writeElse(rule, useContainsKW, useIf, comments)
|
||||
}
|
||||
return comments
|
||||
}
|
||||
}
|
||||
}
|
||||
w.write(" {")
|
||||
w.endLine()
|
||||
w.up()
|
||||
@@ -312,12 +357,12 @@ func (w *writer) writeRule(rule *ast.Rule, isElse bool, comments []*ast.Comment)
|
||||
w.startLine()
|
||||
w.write("}")
|
||||
if rule.Else != nil {
|
||||
comments = w.writeElse(rule, comments)
|
||||
comments = w.writeElse(rule, useContainsKW, useIf, comments)
|
||||
}
|
||||
return comments
|
||||
}
|
||||
|
||||
func (w *writer) writeElse(rule *ast.Rule, comments []*ast.Comment) []*ast.Comment {
|
||||
func (w *writer) writeElse(rule *ast.Rule, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
|
||||
// If there was nothing else on the line before the "else" starts
|
||||
// then preserve this style of else block, otherwise it will be
|
||||
// started as an "inline" else eg:
|
||||
@@ -378,10 +423,10 @@ func (w *writer) writeElse(rule *ast.Rule, comments []*ast.Comment) []*ast.Comme
|
||||
rule.Else.Head.Value.Location = rule.Else.Head.Location
|
||||
}
|
||||
|
||||
return w.writeRule(rule.Else, true, comments)
|
||||
return w.writeRule(rule.Else, true, useContainsKW, useIf, comments)
|
||||
}
|
||||
|
||||
func (w *writer) writeHead(head *ast.Head, isDefault bool, isExpandedConst bool, comments []*ast.Comment) []*ast.Comment {
|
||||
func (w *writer) writeHead(head *ast.Head, isDefault, isExpandedConst, useContainsKW, useIf bool, comments []*ast.Comment) []*ast.Comment {
|
||||
w.write(head.Name.String())
|
||||
if len(head.Args) > 0 {
|
||||
w.write("(")
|
||||
@@ -393,9 +438,14 @@ func (w *writer) writeHead(head *ast.Head, isDefault bool, isExpandedConst bool,
|
||||
w.write(")")
|
||||
}
|
||||
if head.Key != nil {
|
||||
w.write("[")
|
||||
comments = w.writeTerm(head.Key, comments)
|
||||
w.write("]")
|
||||
if useContainsKW && head.Value == nil {
|
||||
w.write(" contains ")
|
||||
comments = w.writeTerm(head.Key, comments)
|
||||
} else { // no `if` for p[x] notation
|
||||
w.write("[")
|
||||
comments = w.writeTerm(head.Key, comments)
|
||||
w.write("]")
|
||||
}
|
||||
}
|
||||
if head.Value != nil && (head.Key != nil || ast.Compare(head.Value, ast.BooleanTerm(true)) != 0 || isExpandedConst || isDefault) {
|
||||
if head.Assign {
|
||||
@@ -1255,23 +1305,16 @@ func (w *writer) down() {
|
||||
}
|
||||
|
||||
func ensureFutureKeywordImport(imps []*ast.Import, kw string) []*ast.Import {
|
||||
allKeywords := ast.MustParseTerm("future.keywords")
|
||||
kwPath := keyword(kw)
|
||||
every := keyword("every")
|
||||
for _, imp := range imps {
|
||||
if allKeywords.Equal(imp.Path) ||
|
||||
imp.Path.Equal(kwPath) ||
|
||||
(imp.Path.Equal(every) && kw == "in") { // "every" implies "in", so we don't need to add both
|
||||
if future.IsAllFutureKeywords(imp) ||
|
||||
future.IsFutureKeyword(imp, kw) ||
|
||||
(future.IsFutureKeyword(imp, "every") && kw == "in") { // "every" implies "in", so we don't need to add both
|
||||
return imps
|
||||
}
|
||||
}
|
||||
imp := &ast.Import{
|
||||
Path: kwPath,
|
||||
Path: ast.MustParseTerm("future.keywords." + kw),
|
||||
}
|
||||
imp.Location = defaultLocation(imp)
|
||||
return append(imps, imp)
|
||||
}
|
||||
|
||||
func keyword(kw string) *ast.Term {
|
||||
return ast.MustParseTerm("future.keywords." + kw)
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ p {
|
||||
|
||||
import future.keywords
|
||||
|
||||
p {
|
||||
p if {
|
||||
every k, v in [1, 2] { k != v }
|
||||
}`,
|
||||
},
|
||||
@@ -290,7 +290,7 @@ p {
|
||||
|
||||
import future.keywords
|
||||
|
||||
p {
|
||||
p if {
|
||||
every k, v in [1, 2] { k != v }
|
||||
}`,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package test.contains
|
||||
import future.keywords.contains
|
||||
|
||||
p contains "foo" { true }
|
||||
|
||||
deny contains msg {
|
||||
msg := "foo"
|
||||
}
|
||||
deny[msg] {msg := "bar" }
|
||||
|
||||
# partial objects unchanged
|
||||
o[k] = v { k := "ok"; v := "nok" }
|
||||
@@ -0,0 +1,19 @@
|
||||
package test.contains
|
||||
|
||||
import future.keywords.contains
|
||||
|
||||
p contains "foo"
|
||||
|
||||
deny contains msg {
|
||||
msg := "foo"
|
||||
}
|
||||
|
||||
deny contains msg {
|
||||
msg := "bar"
|
||||
}
|
||||
|
||||
# partial objects unchanged
|
||||
o[k] = v {
|
||||
k := "ok"
|
||||
v := "nok"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords
|
||||
|
||||
q[x] = y if {
|
||||
y := 10
|
||||
x := "ten"
|
||||
}
|
||||
|
||||
q[x] = y { # not using if
|
||||
y := 11
|
||||
x := "eleven"
|
||||
}
|
||||
|
||||
r[x] { # no if here before
|
||||
x := "set"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords
|
||||
|
||||
q[x] = y if {
|
||||
y := 10
|
||||
x := "ten"
|
||||
}
|
||||
|
||||
q[x] = y if { # not using if
|
||||
y := 11
|
||||
x := "eleven"
|
||||
}
|
||||
|
||||
r contains x if { # no if here before
|
||||
x := "set"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
p if 1 > 0 # shorthand
|
||||
|
||||
p if { 1 > 0 } # longhand one line
|
||||
|
||||
p if {
|
||||
1 > 0 # longhand two lines
|
||||
}
|
||||
|
||||
# same without the comment
|
||||
p if {
|
||||
1 > 0
|
||||
}
|
||||
|
||||
p if { # comment one
|
||||
1 > 0 # comment two
|
||||
}
|
||||
|
||||
q[x] = y if {
|
||||
y := 10
|
||||
x := "ten"
|
||||
}
|
||||
|
||||
q[x] = y { # not using if
|
||||
y := 11
|
||||
x := "eleven"
|
||||
}
|
||||
|
||||
r[x] { x := "set" } # no if here before
|
||||
@@ -0,0 +1,34 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
p if 1 > 0 # shorthand
|
||||
|
||||
p if 1 > 0 # longhand one line
|
||||
|
||||
p if {
|
||||
1 > 0 # longhand two lines
|
||||
}
|
||||
|
||||
# same without the comment
|
||||
p if {
|
||||
1 > 0
|
||||
}
|
||||
|
||||
p if { # comment one
|
||||
1 > 0 # comment two
|
||||
}
|
||||
|
||||
q[x] = y if {
|
||||
y := 10
|
||||
x := "ten"
|
||||
}
|
||||
|
||||
q[x] = y if { # not using if
|
||||
y := 11
|
||||
x := "eleven"
|
||||
}
|
||||
|
||||
r[x] { # no if here before
|
||||
x := "set"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
p := 1 if { 1 > 0 }
|
||||
else := 2
|
||||
|
||||
q := 1 if { 1 > 0 } else := 2 { 2 > 1 }
|
||||
|
||||
q := 1 if {
|
||||
1 > 0
|
||||
2 > 1
|
||||
} else := 2 { 2 > 1 }
|
||||
@@ -0,0 +1,22 @@
|
||||
package test.if
|
||||
|
||||
import future.keywords.if
|
||||
|
||||
p := 1 if 1 > 0
|
||||
|
||||
else := 2 {
|
||||
true
|
||||
}
|
||||
|
||||
q := 1 if 1 > 0
|
||||
|
||||
else := 2 {
|
||||
2 > 1
|
||||
}
|
||||
|
||||
q := 1 if {
|
||||
1 > 0
|
||||
2 > 1
|
||||
} else := 2 {
|
||||
2 > 1
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package p
|
||||
import future.keywords
|
||||
import input.foo
|
||||
|
||||
r {
|
||||
r if {
|
||||
1 in [1]
|
||||
0, 1 in [1]
|
||||
}
|
||||
|
||||
@@ -18,3 +18,20 @@ func FilterFutureImports(imps []*ast.Import) []*ast.Import {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// IsAllFutureKeywords returns true if the passed *ast.Import is `future.keywords`
|
||||
func IsAllFutureKeywords(imp *ast.Import) bool {
|
||||
path := imp.Path.Value.(ast.Ref)
|
||||
return len(path) == 2 &&
|
||||
ast.FutureRootDocument.Equal(path[0]) &&
|
||||
path[1].Equal(ast.StringTerm("keywords"))
|
||||
}
|
||||
|
||||
// IsFutureKeyword returns true if the passed *ast.Import is `future.keywords.{kw}`
|
||||
func IsFutureKeyword(imp *ast.Import, kw string) bool {
|
||||
path := imp.Path.Value.(ast.Ref)
|
||||
return len(path) == 3 &&
|
||||
ast.FutureRootDocument.Equal(path[0]) &&
|
||||
path[1].Equal(ast.StringTerm("keywords")) &&
|
||||
path[2].Equal(ast.StringTerm(kw))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
cases:
|
||||
- data:
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
p {
|
||||
contains("fireplace", "repl")
|
||||
}
|
||||
note: containskeyword/base case
|
||||
query: data.test.p = x
|
||||
want_result:
|
||||
- x: true
|
||||
- data:
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.contains
|
||||
|
||||
p {
|
||||
contains("fireplace", "repl")
|
||||
}
|
||||
note: containskeyword/with unused kw import
|
||||
query: data.test.p = x
|
||||
want_result:
|
||||
- x: true
|
||||
- data:
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.contains
|
||||
|
||||
p contains "x" {
|
||||
contains("fireplace", "repl")
|
||||
}
|
||||
note: containskeyword/with kw and builtin used
|
||||
query: data.test.p = x
|
||||
want_result:
|
||||
- x: [x]
|
||||
- data:
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.contains
|
||||
|
||||
p contains "x"
|
||||
note: containskeyword/empty body
|
||||
query: data.test.p = x
|
||||
want_result:
|
||||
- x: [x]
|
||||
- data:
|
||||
modules:
|
||||
- |
|
||||
package test
|
||||
import future.keywords.contains
|
||||
|
||||
p contains msg {
|
||||
msg := "nono"
|
||||
}
|
||||
p contains msg {
|
||||
msg := "nonono"
|
||||
}
|
||||
note: containskeyword/ordinary deny rule
|
||||
query: data.test.p = x
|
||||
want_result:
|
||||
- x:
|
||||
- nono
|
||||
- nonono
|
||||
Reference in New Issue
Block a user