diff --git a/cmd/deps.go b/cmd/deps.go index 8e96142e8e..43fdd1d042 100644 --- a/cmd/deps.go +++ b/cmd/deps.go @@ -53,8 +53,7 @@ Given a policy like this: package policy - import future.keywords.if - import future.keywords.in + import rego.v1 allow if is_admin diff --git a/cmd/test.go b/cmd/test.go index 5de77a60d3..36b863601f 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -442,7 +442,7 @@ Example policy (example/authz.rego): package authz - import future.keywords.if + import rego.v1 allow if { input.path == ["users"] @@ -458,25 +458,27 @@ Example test (example/authz_test.rego): package authz_test + import rego.v1 + import data.authz.allow - test_post_allowed { + test_post_allowed if { allow with input as {"path": ["users"], "method": "POST"} } - test_get_denied { + test_get_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"} } - todo_test_user_allowed_http_client_data { + todo_test_user_allowed_http_client_data if { false # Remember to test this later! } diff --git a/docs/README.md b/docs/README.md index 5def14df6c..7267f952eb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -173,8 +173,10 @@ In this module: ```live:rule_body:module package example -u { - "foo" == "foo" +import rego.v1 + +u if { + "foo" == "foo" } ``` @@ -200,12 +202,14 @@ Here's what a more complex set of blocks could look like: ``````markdown ```live:eg:module:hidden package example + +import rego.v1 ``` We can define a scalar rule: ```live:eg/string:module -string = "hello" +string := "hello" ``` Which generates the document you'd expect: @@ -216,7 +220,7 @@ Which generates the document you'd expect: We can then define a rule that uses the value of `string`: ```live:eg/string/rule:module -r { input.value == string } +r if { input.value == string } ``` And query it with some input: @@ -241,7 +245,7 @@ In that example, the output for `eg/string` is evaluated with only the module: ``` package example -string = "hello" +string := "hello" ``` Whereas the `eg/string/rule` output is evaluated with the module: @@ -249,9 +253,11 @@ Whereas the `eg/string/rule` output is evaluated with the module: ``` package example -string = "hello" +import rego.v1 + +string := "hello" -r { input.value == string } +r if input.value == string ``` as well the given query and input. diff --git a/docs/content/_index.md b/docs/content/_index.md index 21325b83d9..1111859a41 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -127,7 +127,7 @@ at some point in time, but have been introduced gradually. ```live:example/refs:module:hidden package example -import future.keywords +import rego.v1 ``` When OPA evaluates policies it binds data provided in the query to a global @@ -164,7 +164,7 @@ input.deadbeef ```live:example/exprs:module:hidden package example -import future.keywords +import rego.v1 ``` To produce policy decisions in Rego you write expressions against input and @@ -223,7 +223,7 @@ input.servers[0].protocols[0] == "telnet" ```live:example/vars:module:hidden package example -import future.keywords +import rego.v1 ``` You can store values in intermediate variables using the `:=` (assignment) @@ -275,7 +275,7 @@ x != y # y has not been assigned a value ```live:example/iter:module:hidden package example -import future.keywords +import rego.v1 ``` Like other declarative languages (e.g., SQL), iteration in Rego happens @@ -378,7 +378,7 @@ to express _FOR SOME_ and _FOR ALL_ more explicitly. {{< info >}} To ensure backwards-compatibility, the keywords discussed below introduced slowly. In the first stage, users can opt-in to using the new keywords via a special import: -`import future.keywords.every` introduces the `every` keyword described here. +`import rego.v1` or, alternatively, `import future.keywords.every` introduces the `every` keyword described here. (Importing `every` means also importing `in` without an extra `import` statement.) At some point in the future, the keyword will become _standard_, and the import will @@ -482,7 +482,7 @@ logic statements. Rules can either be "complete" or "partial". ```live:example/complete:module:hidden package example.rules -import future.keywords +import rego.v1 ``` #### Complete Rules @@ -573,7 +573,7 @@ any_public_networks ```live:example/partial_set:module:hidden package example -import future.keywords +import rego.v1 ``` Partial rules are if-then statements that generate a set of values and @@ -586,8 +586,8 @@ public_network contains net.id if { } ``` -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 +In the example above `public_network contains net.id if` is the rule head and +`some net in 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/1/extent:query:merge_down @@ -649,14 +649,16 @@ protocols: ```live:example/logical_or/complete:module:openable,merge_down package example.logical_or +import rego.v1 + default shell_accessible := false -shell_accessible := true { - input.servers[_].protocols[_] == "telnet" +shell_accessible if { + input.servers[_].protocols[_] == "telnet" } -shell_accessible := true { - input.servers[_].protocols[_] == "ssh" +shell_accessible if { + input.servers[_].protocols[_] == "ssh" } ``` ```live:example/logical_or/complete:input:merge_down @@ -690,14 +692,16 @@ could be modified to generate a set of servers that expose `"telnet"` or ```live:example/logical_or/partial_set:module:openable,merge_down package example.logical_or -shell_accessible[server.id] { - server := input.servers[_] - server.protocols[_] == "telnet" +import rego.v1 + +shell_accessible contains server.id if { + server := input.servers[_] + server.protocols[_] == "telnet" } -shell_accessible[server.id] { - server := input.servers[_] - server.protocols[_] == "ssh" +shell_accessible contains server.id if { + server := input.servers[_] + server.protocols[_] == "ssh" } ``` ```live:example/logical_or/partial_set:input:merge_down @@ -748,23 +752,24 @@ For example: ```live:example/final:module:openable,merge_down package example -import future.keywords.every # "every" implies "in" -allow := true { # allow is true if... +import rego.v1 + +allow if { # allow is true if... count(violation) == 0 # there are zero violations. } -violation[server.id] { # a server is in the violation set if... +violation contains server.id if { # a server is in the violation set if... some server in public_servers # it exists in the 'public_servers' set and... "http" in server.protocols # it contains the insecure "http" protocol. } -violation[server.id] { # a server is in the violation set if... +violation contains server.id if { # a server is in the violation set if... some server in input.servers # it exists in the input.servers collection and... "telnet" in server.protocols # it contains the "telnet" protocol. } -public_servers[server] { # a server exists in the public_servers set if... +public_servers contains server if { # a server exists in the public_servers set if... some server in input.servers # it exists in the input.servers collection and... some port in server.ports # it references a port in the input.ports collection and... @@ -805,8 +810,8 @@ curl -L -o opa https://openpolicyagent.org/downloads/{{< current_version >}}/opa {{< info >}} Windows users can obtain the OPA executable from [here](https://openpolicyagent.org/downloads/{{< current_version >}}/opa_windows_amd64.exe). -The steps below are the same for Windows users except the executable name will be different. -Windows executable file name is opa_windows_amd64.exe, which inclues file extension name 'exe'. The checksums file name is opa_windows_amd64.exe.sha256. +The steps below are the same for Windows users except the executable name will be different. +Windows executable file name is opa_windows_amd64.exe, which inclues file extension name 'exe'. The checksums file name is opa_windows_amd64.exe.sha256. Windows users can obtain the checksums from [here](https://openpolicyagent.org/downloads/{{< current_version >}}/opa_windows_amd64.exe.sha256). {{< /info >}} @@ -880,24 +885,26 @@ For example: ```live:example/using_opa:module:openable,read_only package example +import rego.v1 + default allow := false # unless otherwise defined, allow is false -allow := true { # allow is true if... +allow if { # allow is true if... count(violation) == 0 # there are zero violations. } -violation[server.id] { # a server is in the violation set if... +violation contains server.id if { # a server is in the violation set if... some server public_server[server] # it exists in the 'public_server' set and... server.protocols[_] == "http" # it contains the insecure "http" protocol. } -violation[server.id] { # a server is in the violation set if... +violation contains server.id if { # a server is in the violation set if... server := input.servers[_] # it exists in the input.servers collection and... server.protocols[_] == "telnet" # it contains the "telnet" protocol. } -public_server[server] { # a server exists in the public_server set if... +public_server contains server if { # a server exists in the public_server set if... some i, j server := input.servers[_] # it exists in the input.servers collection and... server.ports[_] == input.ports[i].id # it references a port in the input.ports collection and... diff --git a/docs/content/aws-cloudformation-hooks.md b/docs/content/aws-cloudformation-hooks.md index fbe8051128..11b0d0456e 100644 --- a/docs/content/aws-cloudformation-hooks.md +++ b/docs/content/aws-cloudformation-hooks.md @@ -173,27 +173,27 @@ simple policy to block an S3 Bucket unless it has an `AccessControl` attribute s ```live:example/system:module package system -import future.keywords +import rego.v1 main := { - "allow": count(deny) == 0, - "violations": deny, + "allow": count(deny) == 0, + "violations": deny, } -deny[msg] { - bucket_create_or_update - not bucket_is_private +deny contains msg if { + bucket_create_or_update + not bucket_is_private - msg := sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id]) + msg := sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id]) } -bucket_create_or_update { - input.resource.type == "AWS::S3::Bucket" - input.action in {"CREATE", "UPDATE"} +bucket_create_or_update if { + input.resource.type == "AWS::S3::Bucket" + input.action in {"CREATE", "UPDATE"} } -bucket_is_private { - input.resource.properties.AccessControl == "Private" +bucket_is_private if { + input.resource.properties.AccessControl == "Private" } ``` @@ -203,12 +203,12 @@ rules help alleviate the problem of values potentially being undefined. Compare look correct at a first glance: ```live:example/fail:module -deny[msg] { - bucket_create_or_update +deny contains msg if { + bucket_create_or_update - input.resource.properties.AccessControl != "Private" + input.resource.properties.AccessControl != "Private" - msg := sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id]) + msg := sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id]) } ``` @@ -224,15 +224,15 @@ attributes. An example S3 bucket policy might for example want to check that pub ```live:example/boolean_fail:module:read_only # Wrong: will allow both "true" and "false" values as both are considered "truthy" -block_public_acls { - input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls +block_public_acls if { + input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls } ``` ```live:example/boolean_correct:module:read_only # Correct: will allow only when property set to "true" -block_public_acls { - input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls == "true" +block_public_acls if { + input.resource.properties.PublicAccessBlockConfiguration.BlockPublicAcls == "true" } ``` {{< /danger >}} @@ -360,7 +360,7 @@ take a look at what such a main policy might look like: # package system -import future.keywords +import rego.v1 main := { "allow": count(violations) == 0, @@ -377,11 +377,11 @@ main := { # desirable, one may create a special policy for that by simply appending # "delete" to the package name, e.g. data.aws.s3.bucket.delete # -route := document(lower(component), lower(type)) { +route := document(lower(component), lower(type)) if { ["AWS", component, type] = split(input.resource.type, "::") } -violations[msg] { +violations contains msg if { # Aggregate all deny rules found in routed document some msg in route.deny } @@ -390,19 +390,19 @@ violations[msg] { # Basic input validation to avoid having to do this in each resource policy # -violations["Missing input.resource"] { +violations contains "Missing input.resource" if { not input.resource } -violations["Missing input.resource.type"] { +violations contains "Missing input.resource.type" if { not input.resource.type } -violations["Missing input.resource.id"] { +violations contains "Missing input.resource.id" if { not input.resource.id } -violations["Missing input.action"] { +violations contains "Missing input.action" if { not input.action } @@ -410,11 +410,11 @@ violations["Missing input.action"] { # Helpers # -document(component, type) := data.aws[component][type] { +document(component, type) := data.aws[component][type] if { input.action != "DELETE" } -document(component, type) := data.aws[component][type].delete { +document(component, type) := data.aws[component][type].delete if { input.action == "DELETE" } ``` @@ -436,12 +436,14 @@ We can now modify our original policy to verify S3 bucket resources only: ```live:example/bucket:module package aws.s3.bucket -deny[sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id])] { - not bucket_is_private +import rego.v1 + +deny contains sprintf("S3 Bucket %s 'AccessControl' attribute value must be 'Private'", [input.resource.id]) if { + not bucket_is_private } -bucket_is_private { - input.resource.properties.AccessControl == "Private" +bucket_is_private if { + input.resource.properties.AccessControl == "Private" } ``` @@ -464,10 +466,12 @@ A simple authz policy for checking the bearer token might look something like th ```live:example/authz:module package system.authz +import rego.v1 + default allow := false -allow { - input.identity == "my_secret_token" +allow if { + input.identity == "my_secret_token" } ``` diff --git a/docs/content/cli.md b/docs/content/cli.md index 165e478974..734c649e51 100755 --- a/docs/content/cli.md +++ b/docs/content/cli.md @@ -369,8 +369,7 @@ Given a policy like this: package policy - import future.keywords.if - import future.keywords.in + import rego.v1 allow if is_admin @@ -1022,7 +1021,7 @@ Example policy (example/authz.rego): package authz - import future.keywords.if + import rego.v1 allow if { input.path == ["users"] @@ -1038,25 +1037,27 @@ Example test (example/authz_test.rego): package authz_test + import rego.v1 + import data.authz.allow - test_post_allowed { + test_post_allowed if { allow with input as {"path": ["users"], "method": "POST"} } - test_get_denied { + test_get_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"} } - todo_test_user_allowed_http_client_data { + todo_test_user_allowed_http_client_data if { false # Remember to test this later! } diff --git a/docs/content/comparison-to-other-systems.md b/docs/content/comparison-to-other-systems.md index 95b45cb955..679c22f5f0 100644 --- a/docs/content/comparison-to-other-systems.md +++ b/docs/content/comparison-to-other-systems.md @@ -54,10 +54,12 @@ example RBAC policy shown above. ```live:rbac:module:openable package rbac.authz +import rego.v1 + # user-role assignments user_roles := { "alice": ["engineering", "webdev"], - "bob": ["hr"] + "bob": ["hr"], } # role-permissions assignments @@ -65,12 +67,12 @@ role_permissions := { "engineering": [{"action": "read", "object": "server123"}], "webdev": [{"action": "read", "object": "server123"}, {"action": "write", "object": "server123"}], - "hr": [{"action": "read", "object": "database456"}] + "hr": [{"action": "read", "object": "database456"}], } # logic that implements RBAC. -default allow = false -allow { +default allow := false +allow if { # lookup the list of roles for the user roles := user_roles[input.user] # for each role in that list @@ -124,19 +126,19 @@ statements above.) ```live:rbac/sod:module:openable # Pairs of roles that no user can be assigned to simultaneously sod_roles := [ - ["create-payment", "approve-payment"], - ["create-vendor", "pay-vendor"], + ["create-payment", "approve-payment"], + ["create-vendor", "pay-vendor"], ] # Find all users violating SOD -sod_violation[user] { - some user - # grab one role for a user - role1 := user_roles[user][_] - # grab another role for that same user - role2 := user_roles[user][_] - # check if those roles are forbidden by SOD - sod_roles[_] == [role1, role2] +sod_violation contains user if { + some user + # grab one role for a user + role1 := user_roles[user][_] + # grab another role for that same user + role2 := user_roles[user][_] + # check if those roles are forbidden by SOD + sod_roles[_] == [role1, role2] } ``` @@ -183,44 +185,46 @@ OPA supports ABAC policies as shown below. ```live:abac:module:openable package abac +import rego.v1 + # User attributes user_attributes := { - "alice": {"tenure": 15, "title": "trader"}, - "bob": {"tenure": 5, "title": "analyst"} + "alice": {"tenure": 15, "title": "trader"}, + "bob": {"tenure": 5, "title": "analyst"}, } # Stock attributes ticker_attributes := { - "MSFT": {"exchange": "NASDAQ", "price": 59.20}, - "AMZN": {"exchange": "NASDAQ", "price": 813.64} + "MSFT": {"exchange": "NASDAQ", "price": 59.20}, + "AMZN": {"exchange": "NASDAQ", "price": 813.64}, } -default allow = false +default allow := false # all traders may buy NASDAQ under $2M -allow { - # lookup the user's attributes - user := user_attributes[input.user] - # check that the user is a trader - user.title == "trader" - # check that the stock being purchased is sold on the NASDAQ - ticker_attributes[input.ticker].exchange == "NASDAQ" - # check that the purchase amount is under $2M - input.amount <= 2000000 +allow if { + # lookup the user's attributes + user := user_attributes[input.user] + # check that the user is a trader + user.title == "trader" + # check that the stock being purchased is sold on the NASDAQ + ticker_attributes[input.ticker].exchange == "NASDAQ" + # check that the purchase amount is under $2M + input.amount <= 2000000 } # traders with 10+ years experience may buy NASDAQ under $5M -allow { - # lookup the user's attributes - user := user_attributes[input.user] - # check that the user is a trader - user.title == "trader" - # check that the stock being purchased is sold on the NASDAQ - ticker_attributes[input.ticker].exchange == "NASDAQ" - # check that the user has at least 10 years of experience - user.tenure > 10 - # check that the purchase amount is under $5M - input.amount <= 5000000 +allow if { + # lookup the user's attributes + user := user_attributes[input.user] + # check that the user is a trader + user.title == "trader" + # check that the stock being purchased is sold on the NASDAQ + ticker_attributes[input.ticker].exchange == "NASDAQ" + # check that the user has at least 10 years of experience + user.tenure > 10 + # check that the purchase amount is under $5M + input.amount <= 5000000 } ``` @@ -296,50 +300,52 @@ expect the input to have `principal`, `action`, and `resource` fields. ```live:iam:module:openable package aws -default allow = false +import rego.v1 + +default allow := false # FirstStatement -allow { - principals_match - input.action == "iam:ChangePassword" +allow if { + principals_match + input.action == "iam:ChangePassword" } # SecondStatement -allow { - principals_match - input.action == "s3:ListAllMyBuckets" +allow if { + principals_match + input.action == "s3:ListAllMyBuckets" } # ThirdStatement # Use helpers to handle implicit OR in the AWS policy. # Below all of the 'principals_match', 'actions_match' and 'resources_match' must be true. -allow { - principals_match - actions_match - resources_match +allow if { + principals_match + actions_match + resources_match } # principals_match is true if input.principal matches -principals_match { - input.principal == "alice" +principals_match if { + input.principal == "alice" } # actions_match is true if input.action matches one in the list -actions_match { - # iterate over the actions in the list - actions := ["s3:List.*","s3:Get.*"] - action := actions[_] - # check if input.action matches an action - regex.globs_match(input.action, action) +actions_match if { + # iterate over the actions in the list + actions := ["s3:List.*", "s3:Get.*"] + action := actions[_] + # check if input.action matches an action + regex.globs_match(input.action, action) } # resources_match is true if input.resource matches one in the list -resources_match { - # iterate over the resources in the list - resources := ["arn:aws:s3:::confidential-data","arn:aws:s3:::confidential-data/.*"] - resource := resources[_] - # check if input.resource matches a resource - regex.globs_match(input.resource, resource) +resources_match if { + # iterate over the resources in the list + resources := ["arn:aws:s3:::confidential-data", "arn:aws:s3:::confidential-data/.*"] + resource := resources[_] + # check if input.resource matches a resource + regex.globs_match(input.resource, resource) } ``` @@ -455,13 +461,13 @@ roughly the same as for XACML: attributes of users, actions, and resources. ```live:xacml:module:openable package xacml -import future.keywords +import rego.v1 # METADATA # title: urn:curtiss:ba:taa:taa-1.1 # description: Policy for Business Authorization category TAA-1.1 default permit := false -permit { +permit if { # Check that resource has a "NavigationSystem" entry input.resource["NavigationSystem"] diff --git a/docs/content/docker-authorization.md b/docs/content/docker-authorization.md index 8ce9355eab..7c625c6e25 100644 --- a/docs/content/docker-authorization.md +++ b/docs/content/docker-authorization.md @@ -189,20 +189,22 @@ Now let's change the policy so that it's a bit more useful. ```live:docker_authz_deny_unconfined:module:openable package docker.authz +import rego.v1 + default allow := false -allow { - not deny +allow if { + not deny } -deny { - seccomp_unconfined +deny if { + seccomp_unconfined } -seccomp_unconfined { - # This expression asserts that the string on the right-hand side is equal - # to an element in the array SecurityOpt referenced on the left-hand side. - input.Body.HostConfig.SecurityOpt[_] == "seccomp:unconfined" +seccomp_unconfined if { + # This expression asserts that the string on the right-hand side is equal + # to an element in the array SecurityOpt referenced on the left-hand side. + input.Body.HostConfig.SecurityOpt[_] == "seccomp:unconfined" } ``` @@ -389,27 +391,29 @@ EOF ```live:docker_authz_users:module:read_only,openable package docker.authz +import rego.v1 + default allow := false # allow if the user is granted read/write access. -allow { - user_id := input.Headers["Authz-User"] - user := users[user_id] - not user.readOnly +allow if { + user_id := input.Headers["Authz-User"] + user := users[user_id] + not user.readOnly } # allow if the user is granted read-only access and the request is a GET. -allow { - user_id := input.Headers["Authz-User"] - users[user_id].readOnly - input.Method == "GET" +allow if { + user_id := input.Headers["Authz-User"] + users[user_id].readOnly + input.Method == "GET" } # users defines permissions for the user. In this case, we define a single # attribute 'readOnly' that controls the kinds of commands the user can run. users := { - "bob": {"readOnly": true}, - "alice": {"readOnly": false}, + "bob": {"readOnly": true}, + "alice": {"readOnly": false}, } ``` diff --git a/docs/content/envoy-primer.md b/docs/content/envoy-primer.md index e27dffaa4a..5476e73680 100644 --- a/docs/content/envoy-primer.md +++ b/docs/content/envoy-primer.md @@ -14,47 +14,47 @@ 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 rego.v1 import input.attributes.request.http default allow := false allow if { - is_token_valid - action_allowed + is_token_valid + action_allowed } is_token_valid if { - token.valid - now := time.now_ns() / 1000000000 - token.payload.nbf <= now - now < token.payload.exp + token.valid + now := time.now_ns() / 1000000000 + token.payload.nbf <= now + now < token.payload.exp } action_allowed if { - http.method == "GET" - token.payload.role == "guest" - glob.match("/people/*", ["/"], http.path) + http.method == "GET" + token.payload.role == "guest" + glob.match("/people/*", ["/"], http.path) } action_allowed if { - http.method == "GET" - token.payload.role == "admin" - glob.match("/people/*", ["/"], http.path) + http.method == "GET" + token.payload.role == "admin" + glob.match("/people/*", ["/"], http.path) } action_allowed if { - http.method == "POST" - token.payload.role == "admin" - glob.match("/people", ["/"], http.path) - lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) + http.method == "POST" + token.payload.role == "admin" + glob.match("/people", ["/"], http.path) + lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) } - token := {"valid": valid, "payload": payload} if { - [_, encoded] := split(http.headers.authorization, " ") - [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) + [_, encoded] := split(http.headers.authorization, " ") + [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) } ``` @@ -112,15 +112,16 @@ 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 rego.v1 import input.attributes.request.http default allow := false allow if { - is_token_valid - action_allowed + is_token_valid + action_allowed } headers["x-ext-auth-allow"] := "yes" @@ -131,9 +132,9 @@ request_headers_to_remove := ["one-auth-header", "another-auth-header"] response_headers_to_add["x-foo"] := "bar" status_code := 200 if { - allow -} else := 401 { - not is_token_valid + allow +} else := 401 if { + not is_token_valid } else := 403 body := "Authentication Failed" if status_code == 401 @@ -142,35 +143,34 @@ body := "Unauthorized Request" if status_code == 403 dynamic_metadata := {"foo", "bar"} is_token_valid if { - token.valid - now := time.now_ns() / 1000000000 - token.payload.nbf <= now - now < token.payload.exp + token.valid + now := time.now_ns() / 1000000000 + token.payload.nbf <= now + now < token.payload.exp } action_allowed if { - http.method == "GET" - token.payload.role == "guest" - glob.match("/people/*", ["/"], http.path) + http.method == "GET" + token.payload.role == "guest" + glob.match("/people/*", ["/"], http.path) } action_allowed if { - http.method == "GET" - token.payload.role == "admin" - glob.match("/people/*", ["/"], http.path) + http.method == "GET" + token.payload.role == "admin" + glob.match("/people/*", ["/"], http.path) } action_allowed if { - http.method == "POST" - token.payload.role == "admin" - glob.match("/people", ["/"], http.path) - lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) + http.method == "POST" + token.payload.role == "admin" + glob.match("/people", ["/"], http.path) + lower(input.parsed_body.firstname) != base64url.decode(token.payload.sub) } - token := {"valid": valid, "payload": payload} if { - [_, encoded] := split(http.headers.authorization, " ") - [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) + [_, encoded] := split(http.headers.authorization, " ") + [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) } ``` @@ -439,7 +439,8 @@ access the path `/people`. ```live:parsed_path_example:module:read_only package envoy.authz -import future.keywords + +import rego.v1 default allow := false @@ -452,14 +453,15 @@ 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 + +import rego.v1 default allow := false allow if { - input.parsed_path == ["people"] - input.parsed_query.lang == ["en"] - input.parsed_query.id == ["1", "2"] + input.parsed_path == ["people"] + input.parsed_query.lang == ["en"] + input.parsed_query.id == ["1", "2"] } ``` @@ -469,13 +471,14 @@ can then be used in a policy as shown below. ```live:parsed_body_example:module:read_only package envoy.authz -import future.keywords + +import rego.v1 default allow := false allow if { - input.parsed_body.firstname == "Charlie" - input.parsed_body.lastname == "Opa" + input.parsed_body.firstname == "Charlie" + input.parsed_body.lastname == "Opa" } ``` diff --git a/docs/content/envoy-tutorial-gloo-edge.md b/docs/content/envoy-tutorial-gloo-edge.md index 66e4cefa0a..22122164b9 100644 --- a/docs/content/envoy-tutorial-gloo-edge.md +++ b/docs/content/envoy-tutorial-gloo-edge.md @@ -109,7 +109,7 @@ The following OPA policy will work as follows: ```live:example:module:openable package envoy.authz -import future.keywords +import rego.v1 import input.attributes.request.http as http_request diff --git a/docs/content/envoy-tutorial-istio.md b/docs/content/envoy-tutorial-istio.md index 74fe3c2b38..f12c4f87b8 100644 --- a/docs/content/envoy-tutorial-istio.md +++ b/docs/content/envoy-tutorial-istio.md @@ -49,7 +49,7 @@ The `quick_start.yaml` manifest defines the following resources: ```live:example:module:openable package istio.authz - import future.keywords + import rego.v1 import input.attributes.request.http as http_request import input.parsed_path diff --git a/docs/content/envoy-tutorial-standalone-envoy.md b/docs/content/envoy-tutorial-standalone-envoy.md index 21fa44a886..958e6580bc 100644 --- a/docs/content/envoy-tutorial-standalone-envoy.md +++ b/docs/content/envoy-tutorial-standalone-envoy.md @@ -96,46 +96,46 @@ This tutorial assumes you have some Rego knowledge, in summary the policy below # policy.rego package envoy.authz -import future.keywords.if +import rego.v1 import input.attributes.request.http as http_request default allow := false allow if { - is_token_valid - action_allowed + is_token_valid + action_allowed } is_token_valid if { - token.valid - now := time.now_ns() / 1000000000 - token.payload.nbf <= now - now < token.payload.exp + token.valid + now := time.now_ns() / 1000000000 + token.payload.nbf <= now + now < token.payload.exp } action_allowed if { - http_request.method == "GET" - token.payload.role == "guest" - glob.match("/people", ["/"], http_request.path) + http_request.method == "GET" + token.payload.role == "guest" + glob.match("/people", ["/"], http_request.path) } action_allowed if { - http_request.method == "GET" - token.payload.role == "admin" - glob.match("/people", ["/"], http_request.path) + http_request.method == "GET" + token.payload.role == "admin" + glob.match("/people", ["/"], http_request.path) } 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) + 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} if { - [_, encoded] := split(http_request.headers.authorization, " ") - [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) + [_, encoded] := split(http_request.headers.authorization, " ") + [valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"}) } ``` diff --git a/docs/content/faq.md b/docs/content/faq.md index 3eed5140e8..88543dc846 100644 --- a/docs/content/faq.md +++ b/docs/content/faq.md @@ -407,10 +407,13 @@ Depending on the use case and the integration with OPA that you are using, the s **Default allow**. This style of policy allows every request by default. The rules you write dictate which requests should be rejected. ```rego +package example +import rego.v1 + # entry point is 'deny' default deny := false -deny { ... } -deny { ... } +deny if { ... } +deny if { ... } ``` If you assume all of the rules you write are correct, then you know that every rejection the policy produces should truly be rejected. However, there could be requests that are allowed that you may not truly want allowed, but you simply neglected to write the rule for. For operations, this is often a useful style of policy authoring because it allows you to incrementally tighten the controls for a system from wherever that system starts. For security, this style is less appropriate because it allows unknown bad actions to occur. @@ -418,10 +421,13 @@ If you assume all of the rules you write are correct, then you know that every r **Default deny**. This style of policy rejects every request by default. The rules you write dictate which requests should be allowed. ```rego +package example +import rego.v1 + # entry point is 'allow' default allow := false -allow { ... } -allow { ... } +allow if { ... } +allow if { ... } ``` If you assume your rules are correct, the only requests that are accepted are known to be safe. Any statements you leave out reject requests that in actuality are safe but which you did not know were safe. For operations, these policies are less suitable for incrementally improving the policy posture of a system because the initial policy must explicitly allow all of the behaviors that are necessary for the system to operate correctly. For security, these policies ensure that any request that is allowed is known to be safe (because there is a rule saying it is safe). @@ -429,14 +435,17 @@ If you assume your rules are correct, the only requests that are accepted are kn **Default allow with deny override**. This style of policy rejects every request by default. You write rules that dictate which requests should be allowed, and optionally you write other rules that dictate which of those allowed requests should be rejected. ```rego +package example +import rego.v1 + # entry point is 'authz' default authz := false -authz { +authz if { allow not deny } -allow { ... } -deny { ... } +allow if { ... } +deny if { ... } ``` This hybrid approach to policy authoring combines the two previous styles. These policies allow relatively coarse grained parts of the request space and then carve out of each part what should actually be denied. Any deny statements that you forget lead to security problems; any allow statements you forget lead to operational problems. But since this approach allows you to implement either of the other two, it is a common pattern across use cases. diff --git a/docs/content/graphql-api-authorization.md b/docs/content/graphql-api-authorization.md index b51dd2b2f0..9009601a80 100644 --- a/docs/content/graphql-api-authorization.md +++ b/docs/content/graphql-api-authorization.md @@ -75,79 +75,77 @@ The policy below does all of the above in parts: ```live:example:module:openable package graphqlapi.authz -import future.keywords.in -import future.keywords.every +import rego.v1 -subordinates = {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} +subordinates := {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} query_ast := graphql.parse(input.query, input.schema)[0] # If validation fails, the rules depending on this will be undefined. default allow := false -allow { - employeeByIDQueries != {} - every query in employeeByIDQueries { - allowed_query(query) - } +allow if { + employeeByIDQueries != {} + every query in employeeByIDQueries { + allowed_query(query) + } } # Allow users to see the salaries of their subordinates. (variable case) -allowed_query(q) { - selected_salary(q) - varname := variable_arg(q, "id") - input.variables[varname] in subordinates[input.user] # Do value lookup from the 'variables' object. +allowed_query(q) if { + selected_salary(q) + varname := variable_arg(q, "id") + input.variables[varname] in subordinates[input.user] # Do value lookup from the 'variables' object. } + # Allow users to see the salaries of their subordinates. (constant value case) -allowed_query(q) { - selected_salary(q) - username := constant_string_arg(q, "id") - username in subordinates[input.user] +allowed_query(q) if { + selected_salary(q) + username := constant_string_arg(q, "id") + username in subordinates[input.user] } # Helper rules. # Allow users to get their own salaries. (variable case) -allowed_query(q) { - selected_salary(q) - varname := variable_arg(q, "id") - input.user == input.variables[varname] # Do value lookup from the 'variables' object. +allowed_query(q) if { + selected_salary(q) + varname := variable_arg(q, "id") + input.user == input.variables[varname] # Do value lookup from the 'variables' object. } # Allow users to get their own salaries. (constant value case) -allowed_query(q) { - selected_salary(q) - username := constant_string_arg(q, "id") - input.user == username +allowed_query(q) if { + selected_salary(q) + username := constant_string_arg(q, "id") + input.user == username } - # Helper functions. # Build up an object with all queries of interest as values. -employeeByIDQueries[value] { - some value +employeeByIDQueries contains value if { + some value walk(query_ast, [_, value]) - value.Name == "employeeByID" - count(value.SelectionSet) > 0 # Ensure we latch onto an employeeByID query. + value.Name == "employeeByID" + count(value.SelectionSet) > 0 # Ensure we latch onto an employeeByID query. } # Extract the string value of a constant value argument. -constant_string_arg(value, argname) := arg.Value.Raw { - some arg in value.Arguments - arg.Name == argname - arg.Value.Kind == 3 +constant_string_arg(value, argname) := arg.Value.Raw if { + some arg in value.Arguments + arg.Name == argname + arg.Value.Kind == 3 } # Extract the variable name for a variable argument. -variable_arg(value, argname) := arg.Value.Raw { - some arg in value.Arguments - arg.Name == argname - arg.Value.Kind == 0 +variable_arg(value, argname) := arg.Value.Raw if { + some arg in value.Arguments + arg.Name == argname + arg.Value.Kind == 0 } # Ensure we're dealing with a selection set that includes the "salary" field. selected_salary(value) := value.SelectionSet[_].Name == "salary" - ``` Then, build a bundle. @@ -310,16 +308,16 @@ Let's extend the policy to handle this. ```live:hr_example:module:read_only,openable package graphqlapi.authz +import rego.v1 + # Allow HR members to get anyone's salary. -allowed_query(q) { - selected_salary(q) - input.user == hr[_] +allowed_query(q) if { + selected_salary(q) + input.user == hr[_] } # David is the only member of HR. -hr = [ - "david", -] +hr := ["david"] ``` Build a new bundle with the new policy included. @@ -354,85 +352,83 @@ To get a sense of one way the subordinate and HR data might be communicated in t ```live:jwt_example:module:hidden package graphqlapi.authz -import future.keywords.in -import future.keywords.every +import rego.v1 query_ast := graphql.parse(input.query, input.schema)[0] # If validation fails, the rules depending on this will be undefined. # Helper rules. # Allow users to see the salaries of their subordinates. (variable case) -allowed_query(q) { - selected_salary(q) - varname := variable_arg(q, "id") - input.variables[varname] in token.payload.subordinates # Do value lookup from the 'variables' object. +allowed_query(q) if { + selected_salary(q) + varname := variable_arg(q, "id") + input.variables[varname] in token.payload.subordinates # Do value lookup from the 'variables' object. } + # Allow users to see the salaries of their subordinates. (constant value case) -allowed_query(q) { - selected_salary(q) - username := constant_string_arg(q, "id") - username in token.payload.subordinates +allowed_query(q) if { + selected_salary(q) + username := constant_string_arg(q, "id") + username in token.payload.subordinates } # Allow users to get their own salaries. (variable case) -allowed_query(q) { - selected_salary(q) - varname := variable_arg(q, "id") - token.payload.user == input.variables[varname] # Do value lookup from the 'variables' object. +allowed_query(q) if { + selected_salary(q) + varname := variable_arg(q, "id") + token.payload.user == input.variables[varname] # Do value lookup from the 'variables' object. } # Allow users to get their own salaries. (constant value case) -allowed_query(q) { - selected_salary(q) - username := constant_string_arg(q, "id") - token.payload.user == username +allowed_query(q) if { + selected_salary(q) + username := constant_string_arg(q, "id") + token.payload.user == username } # Allow HR members to get anyone's salary. -allowed_query(q) { - selected_salary(q) - token.payload.hr == true +allowed_query(q) if { + selected_salary(q) + token.payload.hr == true } # Helper functions. # Build up a set with all queries of interest as values. -employeeByIDQueries[value] { - some value +employeeByIDQueries contains value if { + some value walk(query_ast, [_, value]) - value.Name == "employeeByID" - count(value.SelectionSet) > 0 # Ensure we latch onto an employeeByID query. + value.Name == "employeeByID" + count(value.SelectionSet) > 0 # Ensure we latch onto an employeeByID query. } # Extract the string value of a constant value argument. -constant_string_arg(value, argname) := arg.Value.Raw { - some arg in value.Arguments - arg.Name == argname - arg.Value.Kind == 3 +constant_string_arg(value, argname) := arg.Value.Raw if { + some arg in value.Arguments + arg.Name == argname + arg.Value.Kind == 3 } # Extract the variable name for a variable argument. -variable_arg(value, argname) := arg.Value.Raw { - some arg in value.Arguments - arg.Name == argname - arg.Value.Kind == 0 +variable_arg(value, argname) := arg.Value.Raw if { + some arg in value.Arguments + arg.Name == argname + arg.Value.Kind == 0 } # Ensure we're dealing with a selection set that includes the "salary" field. selected_salary(value) := value.SelectionSet[_].Name == "salary" - ``` ```live:jwt_example/new_rules:module:openable - default allow := false -allow { - employeeByIDQueries != {} - user_owns_token # Ensure we validate the JWT token. - every query in employeeByIDQueries { - allowed_query(query) - } +allow if { + employeeByIDQueries != {} + user_owns_token # Ensure we validate the JWT token. + every query in employeeByIDQueries { + allowed_query(query) + } } # Helper rules ... (Same as example.rego) @@ -443,11 +439,11 @@ allow { # JWT Token Support # Ensure that the token was issued to the user supplying it. -user_owns_token { input.user == token.payload.azp } +user_owns_token if input.user == token.payload.azp # Helper to get the token payload. -token = {"payload": payload} { - [_, payload, _] := io.jwt.decode(input.token) +token := {"payload": payload} if { + [_, payload, _] := io.jwt.decode(input.token) } ``` diff --git a/docs/content/http-api-authorization.md b/docs/content/http-api-authorization.md index d6e8a4416b..8449fd0eb7 100644 --- a/docs/content/http-api-authorization.md +++ b/docs/content/http-api-authorization.md @@ -39,23 +39,25 @@ cd bundles ```live:example:module:openable package httpapi.authz +import rego.v1 + # bob is alice's manager, and betty is charlie's. subordinates := {"alice": [], "charlie": [], "bob": ["alice"], "betty": ["charlie"]} default allow := false # Allow users to get their own salaries. -allow { - input.method == "GET" - input.path == ["finance", "salary", input.user] +allow if { + input.method == "GET" + input.path == ["finance", "salary", input.user] } # Allow managers to get their subordinates' salaries. -allow { - some username - input.method == "GET" - input.path = ["finance", "salary", username] - subordinates[input.user][_] == username +allow if { + some username + input.method == "GET" + input.path = ["finance", "salary", username] + subordinates[input.user][_] == username } ``` @@ -209,17 +211,17 @@ this. ```live:hr_example:module:read_only,openable package httpapi.authz +import rego.v1 + # Allow HR members to get anyone's salary. -allow { - input.method == "GET" - input.path = ["finance", "salary", _] - input.user == hr[_] +allow if { + input.method == "GET" + input.path = ["finance", "salary", _] + input.user == hr[_] } # David is the only member of HR. -hr := [ - "david", -] +hr := ["david"] ``` Build a new bundle with the new policy included. @@ -256,40 +258,42 @@ real world, let's try a similar exercise utilizing the JWT utilities of OPA. ```live:jwt_example:module:openable package httpapi.authz +import rego.v1 + default allow := false # Allow users to get their own salaries. -allow { - some username - input.method == "GET" - input.path = ["finance", "salary", username] - token.payload.user == username - user_owns_token +allow if { + some username + input.method == "GET" + input.path = ["finance", "salary", username] + token.payload.user == username + user_owns_token } # Allow managers to get their subordinate' salaries. -allow { - some username - input.method == "GET" - input.path = ["finance", "salary", username] - token.payload.subordinates[_] == username - user_owns_token +allow if { + some username + input.method == "GET" + input.path = ["finance", "salary", username] + token.payload.subordinates[_] == username + user_owns_token } # Allow HR members to get anyone's salary. -allow { - input.method == "GET" - input.path = ["finance", "salary", _] - token.payload.hr == true - user_owns_token +allow if { + input.method == "GET" + input.path = ["finance", "salary", _] + token.payload.hr == true + user_owns_token } # Ensure that the token was issued to the user supplying it. -user_owns_token { input.user == token.payload.azp } +user_owns_token if input.user == token.payload.azp # Helper to get the token payload. -token := {"payload": payload} { - [header, payload, signature] := io.jwt.decode(input.token) +token := {"payload": payload} if { + [header, payload, signature] := io.jwt.decode(input.token) } ``` diff --git a/docs/content/integration.md b/docs/content/integration.md index 159389ac50..3b1ee64312 100644 --- a/docs/content/integration.md +++ b/docs/content/integration.md @@ -68,14 +68,13 @@ decisions: `example/authz/allow` and `example/authz/is_admin`. ```live:authz:module:openable,read_only package example.authz -import future.keywords.if -import future.keywords.in +import rego.v1 default allow := false allow if { - input.method == "GET" - input.path == ["salary", input.subject.user] + input.method == "GET" + input.path == ["salary", input.subject.user] } allow if is_admin @@ -217,7 +216,7 @@ func main() { "example.rego": ` package authz - import future.keywords.if + import rego.v1 default allow := false @@ -312,8 +311,7 @@ store, etc. module := ` package example.authz -import future.keywords.if -import future.keywords.in +import rego.v1 default allow := false diff --git a/docs/content/kafka-authorization.md b/docs/content/kafka-authorization.md index 418d90b3f2..2fb3a746ef 100644 --- a/docs/content/kafka-authorization.md +++ b/docs/content/kafka-authorization.md @@ -244,15 +244,15 @@ Update the `policies/tutorial.rego` with the following content. #----------------------------------------------------------------------------- package kafka.authz -import future.keywords.in +import rego.v1 default allow := false -allow { +allow if { not deny } -deny { +deny if { is_read_operation topic_contains_pii not consumer_is_allowlisted_for_pii @@ -272,11 +272,11 @@ topic_metadata := {"credit-scores": {"tags": ["pii"]}} # Helpers for checking topic access. #----------------------------------- -topic_contains_pii { +topic_contains_pii if { "pii" in topic_metadata[topic_name].tags } -consumer_is_allowlisted_for_pii { +consumer_is_allowlisted_for_pii if { principal.name in consumer_allowlist.pii } @@ -286,23 +286,23 @@ consumer_is_allowlisted_for_pii { # place. #----------------------------------------------------------------------------- -is_write_operation { - input.action.operation == "WRITE" +is_write_operation if { + input.action.operation == "WRITE" } -is_read_operation { +is_read_operation if { input.action.operation == "READ" } -is_topic_resource { +is_topic_resource if { input.action.resourcePattern.resourceType == "TOPIC" } -topic_name := input.action.resourcePattern.name { +topic_name := input.action.resourcePattern.name if { is_topic_resource } -principal := {"fqn": parsed.CN, "name": cn_parts[0]} { +principal := {"fqn": parsed.CN, "name": cn_parts[0]} if { parsed := parse_user(input.requestContext.principal.name) cn_parts := split(parsed.CN, ".") } @@ -440,24 +440,24 @@ Processed a total of 0 messages First, add the following content to the policy file (`./policies/tutorial.rego`): ```live:example/deny:module:openable -deny { - is_write_operation - topic_has_large_fanout - not producer_is_allowlisted_for_large_fanout +deny if { + is_write_operation + topic_has_large_fanout + not producer_is_allowlisted_for_large_fanout } producer_allowlist := { - "large-fanout": { - "fanout_producer", - } + "large-fanout": { + "fanout_producer", + } } -topic_has_large_fanout { - topic_metadata[topic_name].tags[_] == "large-fanout" +topic_has_large_fanout if { + topic_metadata[topic_name].tags[_] == "large-fanout" } -producer_is_allowlisted_for_large_fanout { - producer_allowlist["large-fanout"][_] == principal.name +producer_is_allowlisted_for_large_fanout if { + producer_allowlist["large-fanout"][_] == principal.name } ``` diff --git a/docs/content/kubernetes-introduction.md b/docs/content/kubernetes-introduction.md index 829365d8fa..96e45d1afe 100644 --- a/docs/content/kubernetes-introduction.md +++ b/docs/content/kubernetes-introduction.md @@ -75,19 +75,21 @@ referring to illegal registries: ```live:container_image:module:openable package kubernetes.admission -deny[reason] { - some container - input_containers[container] - not startswith(container.image, "hooli.com/") - reason := "container image refers to illegal registry (must be hooli.com)" +import rego.v1 + +deny contains reason if { + some container + input_containers[container] + not startswith(container.image, "hooli.com/") + reason := "container image refers to illegal registry (must be hooli.com)" } -input_containers[container] { - container := input.request.object.spec.containers[_] +input_containers contains container if { + container := input.request.object.spec.containers[_] } -input_containers[container] { - container := input.request.object.spec.template.spec.containers[_] +input_containers contains container if { + container := input.request.object.spec.template.spec.containers[_] } ``` diff --git a/docs/content/kubernetes-primer.md b/docs/content/kubernetes-primer.md index 4dd58a7739..36e9e1249b 100644 --- a/docs/content/kubernetes-primer.md +++ b/docs/content/kubernetes-primer.md @@ -17,7 +17,9 @@ trusted registry. ```live:container_images:module:openable package kubernetes.admission # line 1 -deny[msg] { # line 2 +import rego.v1 + +deny contains msg if { # line 2 input.request.kind.kind == "Pod" # line 3 image := input.request.object.spec.containers[_].image # line 4 not startswith(image, "hooli.com/") # line 5 @@ -31,7 +33,7 @@ In line 1 the `package kubernetes.admission` declaration gives the (hierarchical ### Deny Rules -For admission control, you write `deny` statements. Order does not matter. (OPA is far more flexible than this, but we recommend writing just `deny` statements to start.) In line 2, the *head* of the rule `deny[msg]` says that the admission control request should be rejected and the user handed the error message `msg` if the conditions in the *body* (the statements between the `{}`) are true. +For admission control, you write `deny` statements. Order does not matter. (OPA is far more flexible than this, but we recommend writing just `deny` statements to start.) In line 2, the *head* of the rule `deny contains msg if` says that the admission control request should be rejected and the user handed the error message `msg` if the conditions in the *body* (the statements between the `{}`) are true. `deny` is the *set* of error messages that should be returned to the user. Each rule you write adds to that set of error messages. @@ -203,9 +205,11 @@ When you write policies, you should use the OPA unit-test framework *before* sen ```live:container_images/test:module:read_only,openable package kubernetes.test_admission # line 1 +import rego.v1 + import data.kubernetes.admission # line 2 -test_image_safety { # line 3 +test_image_safety if { # line 3 unsafe_image := { # line 4 "request": { "kind": {"kind": "Pod"}, @@ -281,7 +285,9 @@ To avoid conflicting ingresses, you write a policy like the one that follows. ```live:ingress_conflicts:module:read_only package kubernetes.admission -deny[msg] { +import rego.v1 + +deny contains msg if { some namespace, name input.request.kind.kind == "Ingress" # line 1 newhost := input.request.object.spec.rules[_].host # line 2 @@ -486,12 +492,14 @@ have been loaded into OPA and unions the results: ```live:admission_main:module:read_only package system +import rego.v1 + import data.kubernetes.admission main := { - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": response, + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "response": response, } default uid := "" @@ -499,14 +507,12 @@ default uid := "" uid := input.request.uid response := { - "allowed": false, - "uid": uid, - "status": { - "message": reason, - }, -} { - reason := concat(", ", admission.deny) - reason != "" + "allowed": false, + "uid": uid, + "status": {"message": reason}, +} if { + reason := concat(", ", admission.deny) + reason != "" } else := {"allowed": true, "uid": uid} diff --git a/docs/content/kubernetes-tutorial.md b/docs/content/kubernetes-tutorial.md index 2e6b198d98..cfb3017690 100644 --- a/docs/content/kubernetes-tutorial.md +++ b/docs/content/kubernetes-tutorial.md @@ -125,11 +125,13 @@ expressions will be allowed. ```live:ingress_allowlist:module:read_only package kubernetes.admission +import rego.v1 + import data.kubernetes.namespaces operations := {"CREATE", "UPDATE"} -deny[msg] { +deny contains msg if { input.request.kind.kind == "Ingress" operations[input.request.operation] host := input.request.object.spec.rules[_].host @@ -143,20 +145,20 @@ valid_ingress_hosts := {host | host := hosts[_] } -fqdn_matches_any(str, patterns) { +fqdn_matches_any(str, patterns) if { fqdn_matches(str, patterns[_]) } -fqdn_matches(str, pattern) { +fqdn_matches(str, pattern) if { pattern_parts := split(pattern, ".") pattern_parts[0] == "*" suffix := trim(pattern, "*.") endswith(str, suffix) } -fqdn_matches(str, pattern) { - not contains(pattern, "*") - str == pattern +fqdn_matches(str, pattern) if { + not contains(pattern, "*") + str == pattern } ``` @@ -170,17 +172,19 @@ namespaces from sharing the same hostname. ```live:ingress_conflicts:module:read_only package kubernetes.admission +import rego.v1 + import data.kubernetes.ingresses -deny[msg] { - some other_ns, other_ingress - input.request.kind.kind == "Ingress" - input.request.operation == "CREATE" - host := input.request.object.spec.rules[_].host - ingress := ingresses[other_ns][other_ingress] - other_ns != input.request.namespace - ingress.spec.rules[_].host == host - msg := sprintf("invalid ingress host %q (conflicts with %v/%v)", [host, other_ns, other_ingress]) +deny contains msg if { + some other_ns, other_ingress + input.request.kind.kind == "Ingress" + input.request.operation == "CREATE" + host := input.request.object.spec.rules[_].host + ingress := ingresses[other_ns][other_ingress] + other_ns != input.request.namespace + ingress.spec.rules[_].host == host + msg := sprintf("invalid ingress host %q (conflicts with %v/%v)", [host, other_ns, other_ingress]) } ``` @@ -194,12 +198,14 @@ Let's define a main policy that imports the [Restrict Hostnames](#policy-1-restr ```live:main:module:read_only package system +import rego.v1 + import data.kubernetes.admission main := { - "apiVersion": "admission.k8s.io/v1", - "kind": "AdmissionReview", - "response": response, + "apiVersion": "admission.k8s.io/v1", + "kind": "AdmissionReview", + "response": response, } default uid := "" @@ -207,14 +213,12 @@ default uid := "" uid := input.request.uid response := { - "allowed": false, - "uid": uid, - "status": { - "message": reason, - }, -} { - reason = concat(", ", admission.deny) - reason != "" + "allowed": false, + "uid": uid, + "status": {"message": reason}, +} if { + reason = concat(", ", admission.deny) + reason != "" } else := {"allowed": true, "uid": uid} diff --git a/docs/content/management-decision-logs.md b/docs/content/management-decision-logs.md index 04bd2063de..f1eff7f2db 100644 --- a/docs/content/management-decision-logs.md +++ b/docs/content/management-decision-logs.md @@ -264,11 +264,11 @@ This rule will drop all requests to the _allow_ rule in the _kafka_ package, tha ```live:drop_rule_example/kafka_allow_rule:module:read_only package system.log -import future.keywords.if +import rego.v1 drop if { - input.path == "kafka/allow" - input.result == true + input.path == "kafka/allow" + input.result == true } ``` @@ -278,12 +278,11 @@ Log only requests for _delete_ and _alter_ operations ```live:drop_rule_example/log_only_delete_alter_operations:module:read_only package system.log -import future.keywords.if -import future.keywords.in +import rego.v1 drop if { - input.path == "kafka/allow" - not input.input.action.operation in {"DELETE", "ALTER"} + input.path == "kafka/allow" + not input.input.action.operation in {"DELETE", "ALTER"} } ``` diff --git a/docs/content/policy-language.md b/docs/content/policy-language.md index a236ddd5f9..d06a0b4f30 100644 --- a/docs/content/policy-language.md +++ b/docs/content/policy-language.md @@ -7,7 +7,7 @@ toc: true ```live:eg:module:hidden package example -import future.keywords +import rego.v1 ``` OPA is purpose built for reasoning about information represented in structured @@ -577,12 +577,12 @@ s[[1, x]] Rules are often written in terms of multiple expressions that contain references to documents. In the following example, the rule defines a set of arrays where each array contains an application name and a hostname of a server where the application is deployed. ```live:eg/data/multi:module -apps_and_hostnames[[name, hostname]] { - some i, j, k - name := apps[i].name - server := apps[i].servers[_] - sites[j].servers[k].name == server - hostname := sites[j].servers[k].hostname +apps_and_hostnames contains [name, hostname] if { + some i, j, k + name := apps[i].name + server := apps[i].servers[_] + sites[j].servers[k].name == server + hostname := sites[j].servers[k].hostname } ``` @@ -605,14 +605,14 @@ Don’t worry about understanding everything in this example right now. There ar Using a different key on the same array or object provides the equivalent of self-join in SQL. For example, the following rule defines a document containing apps deployed on the same site as `"mysql"`: ```live:eg/data/self_join:module -same_site[apps[k].name] { - some i, j, k - apps[i].name == "mysql" - server := apps[i].servers[_] - server == sites[j].servers[_].name - other_server := sites[j].servers[_].name - server != other_server - other_server == apps[k].servers[_] +same_site contains apps[k].name if { + some i, j, k + apps[i].name == "mysql" + server := apps[i].servers[_] + server == sites[j].servers[_].name + other_server := sites[j].servers[_].name + server != other_server + other_server == apps[k].servers[_] } ``` @@ -756,13 +756,12 @@ The sample code in this section make use of the data defined in [Examples](#exam {{< info >}} Rule definitions can be more expressive when using the _future keywords_ `contains` and -`if`. They are optional, and you will find examples below of defining rules without them. +`if`. -To follow along as-is, please import the keywords: +To follow along as-is, please import the keywords, or preferably, import `rego.v1`: ```live:eg/data/info:module:read_only -import future.keywords.if -import future.keywords.contains +import rego.v1 ``` [See the docs on _future keywords_](#future-keywords) for more information. @@ -978,9 +977,9 @@ max_memory := 4 { restricted_users[user] } As a shorthand for defining nested rule structures, it's valid to use references as rule heads: ```live:eg/ref_heads:module -fruit.apple.seeds = 12 +fruit.apple.seeds := 12 -fruit.orange.color = "orange" +fruit.orange.color := "orange" ``` This module defines _two complete rules_, `data.example.fruit.apple.seeds` and `data.example.fruit.orange.color`: @@ -1032,25 +1031,25 @@ Module: ```live:general_ref_head:module package example -import future.keywords +import rego.v1 # A partial object rule that converts a list of users to a mapping by "role" and then "id". users_by_role[role][id] := user if { - some user in input.users - id := user.id - role := user.role + some user in input.users + id := user.id + role := user.role } # Partial rule with an explicit "admin" key override users_by_role.admin[id] := user if { - some user in input.admins - id := user.id + some user in input.admins + id := user.id } # Leaf entries can be partial sets users_by_country[country] contains user.id if { - some user in input.users - country := user.country + some user in input.users + country := user.country } ``` @@ -1066,10 +1065,12 @@ The first variable declared in a rule head's reference divides the reference in ```live:general_ref_head_conflict:module package example +import rego.v1 + # R1 -p[x].r := y { - x := "q" - y := 1 +p[x].r := y if { + x := "q" + y := 1 } # R2 @@ -1089,10 +1090,12 @@ Conflicts are detected at compile-time, where possible, between rules even if th ```live:general_ref_head_conflict2:module package example +import rego.v1 + # R1 -p[x].r := y { - x := "foo" - y := 1 +p[x].r := y if { + x := "foo" + y := 1 } # R2 @@ -1114,12 +1117,14 @@ Rules are not allowed to overlap with object values of other rules. ```live:general_ref_head_conflict3:module package example +import rego.v1 + # R1 p.q.r := {"s": 1} # R2 -p[x].r.t := 2 { - x := "q" +p[x].r.t := 2 if { + x := "q" } ``` @@ -1134,12 +1139,14 @@ We won't get a conflict if we update the policy to the following: ```live:general_ref_head_conflict4:module package example +import rego.v1 + # R1 p.q.r.s := 1 # R2 -p[x].r.t := 2 { - x := "q" +p[x].r.t := 2 if { + x := "q" } ``` @@ -1200,11 +1207,9 @@ be the literal `true`. Furthermore, `if` can be used to write shorter definition function declarations below are equivalent: ```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" ``` @@ -1433,8 +1438,6 @@ There must be no apps named "bitcoin-miner". 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 if { every app in apps { app.name != "bitcoin-miner" @@ -1611,7 +1614,7 @@ 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 # uses 'in' and 'contains' and 'if' +import rego.v1 # uses 'in' and 'contains' and 'if' import data.servers @@ -1625,7 +1628,7 @@ Similarly, modules can declare dependencies on query arguments by specifying an ```live:import_input:module:read_only package opa.examples -import future.keywords +import rego.v1 import input.user import input.method @@ -1657,7 +1660,7 @@ Imports can include an optional `as` keyword to handle namespacing issues: ```live:import_namespacing:module:read_only package opa.examples -import future.keywords +import rego.v1 import data.servers as my_servers @@ -1674,6 +1677,7 @@ In the first stage, users can opt-in to using the new keywords via a special imp * `import future.keywords` introduces _all_ future keywords, and * `import future.keywords.x` _only_ introduces the `x` keyword -- see below for all known future keywords. +* `import rego.v1` introduces all future keywords, and enforces the use of `if` and `contains` in rule heads where applicable. {{< danger >}} Using `import future.keywords` to import all future keywords means an **opt-out of a @@ -1691,6 +1695,7 @@ update their policies, so that the new keyword will not cause clashes with exist variable names. {{< info >}} +It is recomended to use `rego.v1` import instead of `future.keywords` imports, as this will ensure that your policy is compatible with the future release of OPA 1.0. If the `rego.v1` import is present in a module, then `future.keywords` and `future.keywords.*` import is implied, and not allowed. {{< /info >}} @@ -1815,14 +1820,12 @@ For using the `some` keyword with iteration, see {{< info >}} `every` is a future keyword and needs to be imported. -`import future.keywords.every` introduces the `every` keyword described here. +`import rego.v1` or, alternatively, `import future.keywords.every` introduces the `every` keyword described here. [See the docs on _future keywords_](#future-keywords) for more information. {{< /info >}} ```live:eg/data/every0:module:merge_down -import future.keywords.every - names_with_dev if { some site in sites site.name == "dev" @@ -1857,8 +1860,6 @@ Used with a key argument, the index, or property name (for objects), comes into scope of the body evaluation: ```live:eg/every1:module:merge_down -import future.keywords.every - array_domain if { every i, x in [1, 2, 3] { x-i == 1 } # array domain } @@ -1882,8 +1883,6 @@ Semantically, `every x in xs { p(x) }` is equivalent to, but shorter than, a "no construct using a helper rule: ```live:eg/every2:module:merge_down -import future.keywords.every - xs := [2, 2, 4, 8] larger_than_one(x) := x > 1 @@ -2129,7 +2128,7 @@ For example: ```live:eg/defaultfunc:module:read_only default clamp_positive(_) := 0 -clamp_positive(x) = x { +clamp_positive(x) := x if { x > 0 } ``` @@ -2212,7 +2211,7 @@ limit imposed on the number of `else` clauses on a rule. {{< info >}} To ensure backwards-compatibility, new keywords (like `in`) are introduced slowly. In the first stage, users can opt-in to using the new keywords via a special import: -`import future.keywords.in` introduces the `in` keyword described here. +`import rego.v1` or, alternatively, `import future.keywords.in` introduces the `in` keyword described here. [See the docs on _future keywords_](#future-keywords) for more information. {{< /info >}} @@ -2220,8 +2219,6 @@ In the first stage, users can opt-in to using the new keywords via a special imp The membership operator `in` lets you check if an element is part of a collection (array, set, or object). It always evaluates to `true` or `false`: ```live:eg/member1:module:merge_down -import future.keywords.in - p := [x, y, z] if { x := 3 in [1, 2, 3] # array y := 3 in {1, 2, 3} # set @@ -2237,8 +2234,6 @@ and an object or an array on the right-hand side, the first argument is taken to be the key (object) or index (array), respectively: ```live:eg/member1c:module:merge_down -import future.keywords.in - 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 @@ -2253,8 +2248,6 @@ arguments, parentheses are required to use the form with two left-hand side arguments -- compare: ```live:eg/member1d:module:merge_down -import future.keywords.in - p := x if { x := { 0, 2 in [2] } } @@ -2279,11 +2272,9 @@ Combined with `not`, the operator can be handy when asserting that an element is member of an array: ```live:eg/member1a:module:merge_down -import future.keywords.in - deny if not "admin" in input.user.roles -test_deny { +test_deny if { deny with input.user.roles as ["operator", "user"] } ``` @@ -2295,8 +2286,6 @@ test_deny { when called in non-collection arguments: ```live:eg/member1b:module:merge_down -import future.keywords.in - q := x if { x := 3 in "three" } @@ -2308,18 +2297,16 @@ q := x if { Using the `some` variant, it can be used to introduce new variables based on a collections' items: ```live:eg/member2:module:merge_down -import future.keywords.in - -p[x] { - some x in ["a", "r", "r", "a", "y"] +p contains x if { + some x in ["a", "r", "r", "a", "y"] } -q[x] { - some x in {"s", "e", "t"} +q contains x if { + some x in {"s", "e", "t"} } -r[x] { - some x in {"foo": "bar", "baz": "quz"} +r contains x if { + some x in {"foo": "bar", "baz": "quz"} } ``` @@ -2329,18 +2316,16 @@ r[x] { Furthermore, passing a second argument allows you to work with _object keys_ and _array indices_: ```live:eg/member3:module:merge_down -import future.keywords.in - -p[x] { - some x, "r" in ["a", "r", "r", "a", "y"] # key variable, value constant +p contains x if { + some x, "r" in ["a", "r", "r", "a", "y"] # key variable, value constant } -q[x] = y if { - some x, y in ["a", "r", "r", "a", "y"] # both variables +q[x] := y if { + some x, y in ["a", "r", "r", "a", "y"] # both variables } -r[y] = x if { - some x, y in {"foo": "bar", "baz": "quz"} +r[y] := x if { + some x, y in {"foo": "bar", "baz": "quz"} } ``` @@ -2350,8 +2335,6 @@ r[y] = x if { 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 if { some x, {"foo": y} in [{"foo": 100}, {"bar": 200}] } @@ -2680,7 +2663,7 @@ The package and individual rules in a module can be annotated with a rich set of # authors: # - John Doe # entrypoint: true -allow { +allow if { ... } ``` @@ -2740,13 +2723,13 @@ The `document` scope annotation can be applied to any rule in the set (i.e., ord # METADATA # title: Allow Ones -allow { +allow if { x == 1 } # METADATA # title: Allow Twos -allow { +allow if { x == 2 } ``` @@ -2760,13 +2743,13 @@ The `title` annotation is a string value giving a human-readable name to the ann ```live:rego/metadata/title:module:read_only # METADATA # title: Allow Ones -allow { +allow if { x == 1 } # METADATA # title: Allow Twos -allow { +allow if { x == 2 } ``` @@ -2783,7 +2766,7 @@ The `description` annotation is a string value describing the annotation target, # The 'allow' rule... # Is about allowing things. # Not denying them. -allow { +allow if { ... } ``` @@ -2813,7 +2796,7 @@ When a _related-resource_ entry is presented as a string, it needs to be a valid # ... # - ref: https://example.com/foo # description: A text describing this resource -allow { +allow if { ... } ``` @@ -2824,7 +2807,7 @@ allow { # - https://example.com/foo # ... # - https://example.com/bar -allow { +allow if { ... } ``` @@ -2858,7 +2841,7 @@ Optionally, the last word may represent an email, if enclosed with `<>`. # ... # - name: Jane Doe # email: jane@example.com -allow { +allow if { ... } ``` @@ -2869,7 +2852,7 @@ allow { # - John Doe # ... # - Jane Doe -allow { +allow if { ... } ``` @@ -2886,7 +2869,7 @@ The `organizations` annotation is a list of string values representing the organ # - Acme Corp. # ... # - Tyrell Corp. -allow { +allow if { ... } ``` @@ -2907,7 +2890,7 @@ If the `--schema` flag is not present, referenced schemas are ignored during typ # schemas: # - input: schema.input # - data.acl: schema["acl-schema"] -allow { +allow if { access := data.acl["alice"] access[_] == input.operation } @@ -2923,7 +2906,7 @@ in contrast to [by-reference schema annotations](#schema-reference-format), whic # METADATA # schemas: # - input.x: {type: number} -allow { +allow if { input.x == 42 } ``` @@ -2959,7 +2942,7 @@ The `custom` annotation is a mapping of user-defined data, mapping string keys t # my_map: # a: 1 # b: 2 -allow { +allow if { ... } ``` @@ -2987,19 +2970,21 @@ The following policy ```live:example/metadata/1:module package example +import rego.v1 + # METADATA # title: Deny invalid numbers # description: Numbers may not be higher than 5 # custom: # severity: MEDIUM -output := decision { - input.number > 5 +output := decision if { + input.number > 5 - annotation := rego.metadata.rule() - decision := { - "severity": annotation.custom.severity, - "message": annotation.description, - } + annotation := rego.metadata.rule() + decision := { + "severity": annotation.custom.severity, + "message": annotation.description, + } } ``` @@ -3166,11 +3151,13 @@ starts with a specific prefix. ``` package kubernetes.admission -deny[msg] { - input.request.kind.kinds == "Pod" - image := input.request.object.spec.containers[_].image - not startswith(image, "hooli.com/") - msg := sprintf("image '%v' comes from untrusted registry", [image]) +import rego.v1 + +deny contains msg if { + input.request.kind.kinds == "Pod" + image := input.request.object.spec.containers[_].image + not startswith(image, "hooli.com/") + msg := sprintf("image '%v' comes from untrusted registry", [image]) } ``` @@ -3258,7 +3245,7 @@ When passing a directory of schemas to `opa eval`, schema annotations become han # - : # ... # - : -allow { +allow if { ... } ``` @@ -3276,6 +3263,8 @@ Consider the following Rego code which checks if an operation is allowed by a us ``` package policy +import rego.v1 + import data.acl default allow := false @@ -3284,14 +3273,14 @@ default allow := false # schemas: # - input: schema.input # - data.acl: schema["acl-schema"] -allow { - access := data.acl["alice"] - access[_] == input.operation +allow if { + access := data.acl.alice + access[_] == input.operation } -allow { - access := data.acl["bob"] - access[_] == input.operation +allow if { + access := data.acl.bob + access[_] == input.operation } ``` @@ -3342,7 +3331,7 @@ annotation multiple times: # schemas: # - input: schema.input # - data.acl: schema["acl-schema"] -allow { +allow if { access := data.acl["alice"] access[_] == input.operation } @@ -3352,7 +3341,7 @@ allow { # schemas: # - input: schema.input # - data.acl: schema["acl-schema"] -allow { +allow if { access := data.acl["bob"] access[_] == input.operation } @@ -3367,12 +3356,12 @@ define the annotation once on a rule with scope `document`: # schemas: # - input: schema.input # - data.acl: schema["acl-schema"] -allow { +allow if { access := data.acl["alice"] access[_] == input.operation } -allow { +allow if { access := data.acl["bob"] access[_] == input.operation } @@ -3394,12 +3383,14 @@ within the package: # - data.acl: schema["acl-schema"] package example -allow { +import rego.v1 + +allow if { access := data.acl["alice"] access[_] == input.operation } -allow { +allow if { access := data.acl["bob"] access[_] == input.operation } @@ -3432,16 +3423,18 @@ Consider the following example: ``` package kubernetes.admission +import rego.v1 + # METADATA # scope: rule # schemas: # - input: schema.input # - input.request.object: schema.kubernetes.pod -deny[msg] { - input.request.kind.kind == "Pod" - image := input.request.object.spec.containers[_].image - not startswith(image, "hooli.com/") - msg := sprintf("image '%v' comes from untrusted registry", [image]) +deny contains msg if { + input.request.kind.kind == "Pod" + image := input.request.object.spec.containers[_].image + not startswith(image, "hooli.com/") + msg := sprintf("image '%v' comes from untrusted registry", [image]) } ``` @@ -3492,6 +3485,8 @@ It is sometimes useful to have different input schemas for different rules in th ``` package policy +import rego.v1 + import data.acl default allow := false @@ -3501,9 +3496,9 @@ default allow := false # schemas: # - input: schema["input"] # - data.acl: schema["acl-schema"] -allow { - access := data.acl[input.user] - access[_] == input.operation +allow if { + access := data.acl[input.user] + access[_] == input.operation } # METADATA for whocan rule @@ -3511,9 +3506,9 @@ allow { # schemas: # - input: schema["whocan-input-schema"] # - data.acl: schema["acl-schema"] -whocan[user] { - access := acl[user] - access[_] == input.operation +whocan contains user if { + access := acl[user] + access[_] == input.operation } ``` @@ -3550,12 +3545,14 @@ Specifically, `anyOf` acts as an Rego Or type where at least one (can be more th ``` package kubernetes.admission +import rego.v1 + # METADATA # scope: rule # schemas: # - input: schema["input-anyOf"] -deny { - input.request.servers.versions == "Pod" +deny if { + input.request.servers.versions == "Pod" } ``` @@ -3631,12 +3628,14 @@ Specifically, `allOf` keyword implies that all conditions under `allOf` within a ``` package kubernetes.admission +import rego.v1 + # METADATA # scope: rule # schemas: # - input: schema["input-allof"] -deny { - input.request.servers.versions == "Pod" +deny if { + input.request.servers.versions == "Pod" } ``` diff --git a/docs/content/policy-performance.md b/docs/content/policy-performance.md index 5b43c54621..3152d1ee3b 100644 --- a/docs/content/policy-performance.md +++ b/docs/content/policy-performance.md @@ -20,11 +20,13 @@ For example, the following rule has one local variable `user`, and that variable ```live:linear:module:read_only,openable package linear -allow { - some user - input.method == "GET" - input.path = ["accounts", user] - input.user == user +import rego.v1 + +allow if { + some user + input.method == "GET" + input.path = ["accounts", user] + input.user == user } ``` @@ -71,30 +73,32 @@ Here is an example policy from the [rule-indexing blog](https://blog.openpolicya ```live:indexed:module:openable package indexed +import rego.v1 + default allow := false -allow { - some user - input.method == "GET" - input.path = ["accounts", user] - input.user == user +allow if { + some user + input.method == "GET" + input.path = ["accounts", user] + input.user == user } -allow { - input.method == "GET" - input.path == ["accounts", "report"] - roles[input.user][_] == "admin" +allow if { + input.method == "GET" + input.path == ["accounts", "report"] + roles[input.user][_] == "admin" } -allow { - input.method == "POST" - input.path == ["accounts"] - roles[input.user][_] == "admin" +allow if { + input.method == "POST" + input.path == ["accounts"] + roles[input.user][_] == "admin" } roles := { - "bob": ["admin", "hr"], - "alice": ["procurement"], + "bob": ["admin", "hr"], + "alice": ["procurement"], } ``` @@ -149,58 +153,64 @@ The most common case for this are a set of `allow` rules: ```live:ee:module:read_only package earlyexit -allow { - input.user == "alice" +import rego.v1 + +allow if { + input.user == "alice" } -allow { - input.user == "bob" + +allow if { + input.user == "bob" } -allow { - input.group == "admins" + +allow if { + input.group == "admins" } ``` -since `allow { ... }` is a shorthand for `allow = true { ... }`. +since `allow if { ... }` is a shorthand for `allow := true if { ... }`. Intuitively, the value can be anything that does not contain a variable: ```live:eeexamples:module:read_only package earlyexit.examples +import rego.v1 + # p, q, r and s could be evaluated with early-exit semantics: -p { - # ... +p if { + # ... } -q := 123 { - # ... +q := 123 if { + # ... } -r := {"hello": "world"} { - # ... +r := {"hello": "world"} if { + # ... } -s(x) := 12 { - # ... +s(x) := 12 if { + # ... } # u, v, w, and y could _not_ -u[x] { # not a complete document rule, but a partial set - x := 911 +u contains x if { # not a complete document rule, but a partial set + x := 911 } -v := x { # x is a variable, not ground - x := true +v := x if { # x is a variable, not ground + x := true } -w := { "foo": x } { # a compound term containing a variable - x := "bar" +w := {"foo": x} if { # a compound term containing a variable + x := "bar" } -y(z) := r { # variable value, not ground - r := z + 1 +y(z) := r if { # variable value, not ground + r := z + 1 } ``` @@ -210,9 +220,11 @@ When "early exit" is possible for a (set of) rules, iterations inside that rule ```live:eeiteration:module:read_only package earlyexit.iteration -p { - some p - data.projects[p] == "project-a" +import rego.v1 + +p if { + some p + data.projects[p] == "project-a" } ``` @@ -227,14 +239,18 @@ early; an evaluation with `{"user": "bob", "group": "admins"}` *would not*: ```live:eeindex:module:read_only package earlyexit -allow { - input.user == "alice" +import rego.v1 + +allow if { + input.user == "alice" } -allow = false { - input.user == "bob" + +allow := false if { + input.user == "bob" } -allow { - input.group == "admins" + +allow if { + input.group == "admins" } ``` @@ -308,20 +324,23 @@ at once. These values are indexed by the assignments of `intf`. To implement the policy above we could write: ```rego -deny[msg] { - some i - count(exposed_ports_by_interface[i]) > 100 - msg := sprintf("interface '%v' exposes too many ports", [i]) +package example +import rego.v1 + +deny contains msg if { + some i + count(exposed_ports_by_interface[i]) > 100 + msg := sprintf("interface '%v' exposes too many ports", [i]) } exposed_ports_by_interface := {intf: ports | - some i - intf := input.exposed[i].interface - ports := [port | - some j - input.exposed[j].interface == intf - port := input.exposed[j].port - ] + some i + intf := input.exposed[i].interface + ports := [port | + some j + input.exposed[j].interface == intf + port := input.exposed[j].port + ] } ``` @@ -343,41 +362,46 @@ In order to be indexed, comprehensions must meet the following conditions: The following examples shows rules that are **not** indexed: ```rego -not_indexed_because_missing_assignment { - x := input[_] - [y | some y; x == input[y]] +package example +import rego.v1 + +not_indexed_because_missing_assignment if { + x := input[_] + [y | some y; x == input[y]] } -not_indexed_because_includes_with { - x := input[_] - ys := [y | some y; x := input[y]] with input as {} +not_indexed_because_includes_with if { + x := input[_] + ys := [y | some y; x := input[y]] with input as {} } -not_indexed_because_negated { - x := input[_] - not data.arr = [y | some y; x := input[y]] +not_indexed_because_negated if { + x := input[_] + not data.arr = [y | some y; x := input[y]] } -not_indexed_because_safety { - obj := input.foo.bar - x := obj[_] - ys := [y | some y; x == obj[y]] +not_indexed_because_safety if { + obj := input.foo.bar + x := obj[_] + ys := [y | some y; x == obj[y]] } -not_indexed_because_no_closure { - ys := [y | x := input[y]] +not_indexed_because_no_closure if { + ys := [y | x := input[y]] } -not_indexed_because_reference_operand_closure { - x := input[y].x - ys := [y | x == input[y].z[_]] +not_indexed_because_reference_operand_closure if { + x := input[y].x + ys := [y | x == input[y].z[_]] } -not_indexed_because_nested_closure { - x := 1 - y := 2 - _ = [i | x == input.foo[i] - _ = [j | y == input.bar[j]]] +not_indexed_because_nested_closure if { + x := 1 + y := 2 + _ = [i | + x == input.foo[i] + _ = [j | y == input.bar[j]] + ] } ``` @@ -414,12 +438,13 @@ let's take the following policy: ```rego package test +import rego.v1 -p { +p if { a := 1 b := 2 c := 3 - x = a + b * c + x = a + (b * c) } ``` @@ -429,20 +454,20 @@ If we profile the above policy we would get something like the following output: +----------+----------+----------+--------------+-------------+ | TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | +----------+----------+----------+--------------+-------------+ -| 20.291µs | 3 | 3 | 3 | test.rego:7 | -| 1µs | 1 | 1 | 1 | test.rego:6 | -| 2.333µs | 1 | 1 | 1 | test.rego:5 | -| 6.333µs | 1 | 1 | 1 | test.rego:4 | +| 20.291µs | 3 | 3 | 3 | test.rego:8 | +| 1µs | 1 | 1 | 1 | test.rego:7 | +| 2.333µs | 1 | 1 | 1 | test.rego:6 | +| 6.333µs | 1 | 1 | 1 | test.rego:5 | | 84.75µs | 1 | 1 | 1 | data | +----------+----------+----------+--------------+-------------+ ``` -The first entry indicates that line `test.rego:7` has a `EVAL/REDO` count of `3`. If we look at the expression on line `test.rego:7` +The first entry indicates that line `test.rego:8` has a `EVAL/REDO` count of `3`. If we look at the expression on line `test.rego:8` ie `x = a + b * c` it's not immediately clear why this line has a `EVAL/REDO` count of `3`. But we also notice that there -are `3` generated expressions (ie. `NUM GEN EXPR`) at line `test.rego:7`. This is because the compiler rewrites the above policy to +are `3` generated expressions (ie. `NUM GEN EXPR`) at line `test.rego:8`. This is because the compiler rewrites the above policy to something like below: -`p = true { +`p = true if { __local0__ = 1; __local1__ = 2; __local2__ = 3; @@ -451,7 +476,7 @@ something like below: x = __local4__ }` -And that line `test.rego:7` is rewritten to `mul(__local1__, __local2__, __local3__); plus(__local0__, __local3__, __local4__); x = __local4__` which +And that line `test.rego:8` is rewritten to `mul(__local1__, __local2__, __local3__); plus(__local0__, __local3__, __local4__); x = __local4__` which results in a `NUM GEN EXPR` count of `3`. Hence, the `NUM GEN EXPR` count can help to better understand the `EVAL/REDO` counts for a given expression and also provide more clarity into the profile results and how policy evaluation works. @@ -463,63 +488,64 @@ sample policy. ```live:profile:module:read_only,openable package rbac +import rego.v1 # Example input request -input := { - "subject": "bob", - "resource": "foo123", - "action": "write", +inp := { + "subject": "bob", + "resource": "foo123", + "action": "write", } # Example RBAC configuration. bindings := [ - { - "user": "alice", - "roles": ["dev", "test"], - }, - { - "user": "bob", - "roles": ["test"], - }, + { + "user": "alice", + "roles": ["dev", "test"], + }, + { + "user": "bob", + "roles": ["test"], + }, ] roles := [ - { - "name": "dev", - "permissions": [ - {"resource": "foo123", "action": "write"}, - {"resource": "foo123", "action": "read"}, - ], - }, - { - "name": "test", - "permissions": [{"resource": "foo123", "action": "read"}], - }, + { + "name": "dev", + "permissions": [ + {"resource": "foo123", "action": "write"}, + {"resource": "foo123", "action": "read"}, + ], + }, + { + "name": "test", + "permissions": [{"resource": "foo123", "action": "read"}], + }, ] # Example RBAC policy implementation. default allow := false -allow { - some role_name - user_has_role[role_name] - role_has_permission[role_name] +allow if { + some role_name + user_has_role[role_name] + role_has_permission[role_name] } -user_has_role[role_name] { - binding := bindings[_] - binding.user == input.subject - role_name := binding.roles[_] +user_has_role contains role_name if { + binding := bindings[_] + binding.user == inp.subject + role_name := binding.roles[_] } -role_has_permission[role_name] { - role := roles[_] - role_name := role.name - perm := role.permissions[_] - perm.resource == input.resource - perm.action == input.action +role_has_permission contains role_name if { + role := roles[_] + role_name := role.name + perm := role.permissions[_] + perm.resource == inp.resource + perm.action == inp.action } ``` @@ -533,30 +559,30 @@ opa eval --data rbac.rego --profile --format=pretty 'data.rbac.allow' ```ruby false - -+----------+----------+----------+--------------+-----------------+ -| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | -+----------+----------+----------+--------------+-----------------+ -| 47.148µs | 1 | 1 | 1 | data.rbac.allow | -| 28.965µs | 1 | 1 | 1 | rbac.rego:11 | -| 24.384µs | 1 | 1 | 1 | rbac.rego:41 | -| 23.064µs | 2 | 1 | 1 | rbac.rego:47 | -| 15.525µs | 1 | 1 | 1 | rbac.rego:38 | -| 14.137µs | 1 | 2 | 1 | rbac.rego:46 | -| 13.927µs | 1 | 0 | 1 | rbac.rego:42 | -| 13.568µs | 1 | 1 | 1 | rbac.rego:55 | -| 12.982µs | 1 | 0 | 1 | rbac.rego:56 | -| 12.763µs | 1 | 2 | 1 | rbac.rego:52 | -+----------+----------+----------+--------------+-----------------+ - -+------------------------------+----------+ -| METRIC | VALUE | -+------------------------------+----------+ -| timer_rego_module_compile_ns | 1871613 | -| timer_rego_query_compile_ns | 82290 | -| timer_rego_query_eval_ns | 257952 | -| timer_rego_query_parse_ns | 12337169 | -+------------------------------+----------+ ++------------------------------+---------+ +| METRIC | VALUE | ++------------------------------+---------+ +| timer_rego_load_files_ns | 769583 | +| timer_rego_module_compile_ns | 1652125 | +| timer_rego_module_parse_ns | 482417 | +| timer_rego_query_compile_ns | 23042 | +| timer_rego_query_eval_ns | 440542 | +| timer_rego_query_parse_ns | 36250 | ++------------------------------+---------+ ++-----------+----------+----------+--------------+-----------------+ +| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | ++-----------+----------+----------+--------------+-----------------+ +| 237.126µs | 1 | 1 | 1 | data.rbac.allow | +| 25.75µs | 1 | 1 | 1 | docs.rego:13 | +| 17.5µs | 1 | 1 | 1 | docs.rego:40 | +| 6.832µs | 2 | 1 | 1 | docs.rego:50 | +| 5.042µs | 1 | 1 | 1 | docs.rego:44 | +| 4.666µs | 1 | 0 | 1 | docs.rego:45 | +| 4.209µs | 1 | 1 | 1 | docs.rego:58 | +| 3.792µs | 1 | 2 | 1 | docs.rego:49 | +| 3.666µs | 1 | 2 | 1 | docs.rego:55 | +| 3.167µs | 1 | 1 | 1 | docs.rego:24 | ++-----------+----------+----------+--------------+-----------------+ ``` As seen from the above table, all results are displayed. The profile results are @@ -573,30 +599,30 @@ opa eval --data rbac.rego --profile --format=pretty --count=10 'data.rbac.allow' ```ruby false -+------------------------------+---------+----------+---------------+----------------+---------------+ -| METRIC | MIN | MAX | MEAN | 90% | 99% | -+------------------------------+---------+----------+---------------+----------------+---------------+ -| timer_rego_load_files_ns | 349969 | 2549399 | 1.4760619e+06 | 2.5312689e+06 | 2.549399e+06 | -| timer_rego_module_compile_ns | 1087507 | 24537496 | 1.120074e+07 | 2.41699473e+07 | 2.4537496e+07 | -| timer_rego_module_parse_ns | 275531 | 1915263 | 1.126406e+06 | 1.9016968e+06 | 1.915263e+06 | -| timer_rego_query_compile_ns | 61663 | 64395 | 63062.5 | 64374.1 | 64395 | -| timer_rego_query_eval_ns | 161812 | 1198092 | 637754 | 1.1846622e+06 | 1.198092e+06 | -| timer_rego_query_parse_ns | 6078 | 6078 | 6078 | 6078 | 6078 | -+------------------------------+---------+----------+---------------+----------------+---------------+ -+----------+-------------+-------------+-------------+-------------+----------+----------+--------------+------------------+ -| MIN | MAX | MEAN | 90% | 99% | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | -+----------+-------------+-------------+-------------+-------------+----------+----------+--------------+------------------+ -| 43.875µs | 26.135469ms | 11.494512ms | 25.746215ms | 26.135469ms | 1 | 1 | 1 | data.rbac.allow | -| 21.478µs | 211.461µs | 98.102µs | 205.72µs | 211.461µs | 1 | 1 | 1 | rbac.rego:13 | -| 19.652µs | 123.537µs | 73.161µs | 122.75µs | 123.537µs | 1 | 1 | 1 | rbac.rego:40 | -| 12.303µs | 117.277µs | 61.59µs | 116.733µs | 117.277µs | 2 | 1 | 1 | rbac.rego:50 | -| 12.224µs | 93.214µs | 51.289µs | 92.217µs | 93.214µs | 1 | 1 | 1 | rbac.rego:44 | -| 5.561µs | 84.121µs | 43.002µs | 83.469µs | 84.121µs | 1 | 1 | 1 | rbac.rego:51 | -| 5.56µs | 71.712µs | 36.545µs | 71.158µs | 71.712µs | 1 | 0 | 1 | rbac.rego:45 | -| 4.958µs | 66.04µs | 33.161µs | 65.636µs | 66.04µs | 1 | 2 | 1 | rbac.rego:49 | -| 4.326µs | 65.836µs | 30.461µs | 65.083µs | 65.836µs | 1 | 1 | 1 | rbac.rego:6 | -| 3.948µs | 43.399µs | 24.167µs | 43.055µs | 43.399µs | 1 | 2 | 1 | rbac.rego:55 | -+----------+-------------+-------------+-------------+-------------+----------+----------+--------------+------------------+ ++------------------------------+--------+---------+----------+------------------------+--------------+ +| METRIC | MIN | MAX | MEAN | 90% | 99% | ++------------------------------+--------+---------+----------+------------------------+--------------+ +| timer_rego_load_files_ns | 140167 | 1092875 | 387233.3 | 1.0803291e+06 | 1.092875e+06 | +| timer_rego_module_compile_ns | 447208 | 1178542 | 646295.9 | 1.1565419000000001e+06 | 1.178542e+06 | +| timer_rego_module_parse_ns | 121458 | 1041333 | 349183.2 | 1.022583e+06 | 1.041333e+06 | +| timer_rego_query_compile_ns | 17542 | 47875 | 25758.4 | 47450 | 47875 | +| timer_rego_query_eval_ns | 47666 | 136625 | 68200 | 132762.5 | 136625 | +| timer_rego_query_parse_ns | 14334 | 46917 | 26270.9 | 46842 | 46917 | ++------------------------------+--------+---------+----------+------------------------+--------------+ ++---------+----------+---------+----------+----------+----------+----------+--------------+-----------------+ +| MIN | MAX | MEAN | 90% | 99% | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | ++---------+----------+---------+----------+----------+----------+----------+--------------+-----------------+ +| 5.208µs | 27µs | 9.008µs | 25.525µs | 27µs | 1 | 1 | 1 | data.rbac.allow | +| 4.126µs | 17µs | 7.196µs | 16.479µs | 17µs | 1 | 1 | 1 | docs.rego:13 | +| 3.958µs | 12.833µs | 6.116µs | 12.583µs | 12.833µs | 1 | 1 | 1 | docs.rego:40 | +| 3.459µs | 10.708µs | 5.354µs | 10.499µs | 10.708µs | 2 | 1 | 1 | docs.rego:50 | +| 3.291µs | 9.209µs | 4.912µs | 9.096µs | 9.209µs | 1 | 1 | 1 | docs.rego:44 | +| 3.209µs | 8.75µs | 4.637µs | 8.62µs | 8.75µs | 1 | 0 | 1 | docs.rego:45 | +| 3.042µs | 8.333µs | 4.491µs | 8.233µs | 8.333µs | 1 | 1 | 1 | docs.rego:51 | +| 3µs | 7.25µs | 4.1µs | 7.112µs | 7.25µs | 1 | 1 | 1 | docs.rego:58 | +| 2.667µs | 5.75µs | 3.783µs | 5.72µs | 5.75µs | 1 | 2 | 1 | docs.rego:49 | +| 2.583µs | 5.708µs | 3.479µs | 5.595µs | 5.708µs | 1 | 1 | 1 | docs.rego:24 | ++---------+----------+---------+----------+----------+----------+----------+--------------+-----------------+ ``` ##### Example: Display top 5 profile results @@ -609,13 +635,13 @@ opa eval --data rbac.rego --profile-limit 5 --format=pretty 'data.rbac.allow' ```ruby +----------+----------+----------+--------------+-----------------+ -| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | +| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | +----------+----------+----------+--------------+-----------------+ -| 46.329µs | 1 | 1 | 1 | data.rbac.allow | -| 26.656µs | 1 | 1 | 1 | rbac.rego:11 | -| 24.206µs | 2 | 1 | 1 | rbac.rego:47 | -| 23.235µs | 1 | 1 | 1 | rbac.rego:41 | -| 18.242µs | 1 | 1 | 1 | rbac.rego:38 | +| 24.624µs | 1 | 1 | 1 | data.rbac.allow | +| 15.251µs | 1 | 1 | 1 | docs.rego:13 | +| 12.167µs | 1 | 1 | 1 | docs.rego:40 | +| 9.625µs | 2 | 1 | 1 | docs.rego:50 | +| 8.751µs | 1 | 1 | 1 | docs.rego:44 | +----------+----------+----------+--------------+-----------------+ ``` @@ -634,11 +660,11 @@ opa eval --data rbac.rego --profile-limit 5 --profile-sort num_eval --format=pr +----------+----------+----------+--------------+-----------------+ | TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | +----------+----------+----------+--------------+-----------------+ -| 26.675µs | 2 | 1 | 1 | rbac.rego:47 | -| 9.274µs | 2 | 1 | 1 | rbac.rego:53 | -| 43.356µs | 1 | 1 | 1 | data.rbac.allow | -| 22.467µs | 1 | 1 | 1 | rbac.rego:41 | -| 22.425µs | 1 | 1 | 1 | rbac.rego:11 | +| 10.541µs | 2 | 1 | 1 | docs.rego:50 | +| 4.041µs | 2 | 1 | 1 | docs.rego:56 | +| 27.876µs | 1 | 1 | 1 | data.rbac.allow | +| 19.916µs | 1 | 1 | 1 | docs.rego:40 | +| 19.416µs | 1 | 1 | 1 | docs.rego:13 | +----------+----------+----------+--------------+-----------------+ ``` @@ -657,15 +683,15 @@ opa eval --data rbac.rego --profile-limit 5 --profile-sort num_eval,num_redo --f **Sample Profile Output** ```ruby -+----------+----------+----------+--------------+-----------------+ -| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | -+----------+----------+----------+--------------+-----------------+ -| 22.892µs | 2 | 1 | 1 | rbac.rego:47 | -| 8.831µs | 2 | 1 | 1 | rbac.rego:53 | -| 13.767µs | 1 | 2 | 1 | rbac.rego:46 | -| 10.78µs | 1 | 2 | 1 | rbac.rego:52 | -| 42.338µs | 1 | 1 | 1 | data.rbac.allow | -+----------+----------+----------+--------------+-----------------+ ++---------+----------+----------+--------------+-----------------+ +| TIME | NUM EVAL | NUM REDO | NUM GEN EXPR | LOCATION | ++---------+----------+----------+--------------+-----------------+ +| 9.625µs | 2 | 1 | 1 | docs.rego:50 | +| 3.458µs | 2 | 1 | 1 | docs.rego:56 | +| 5.625µs | 1 | 2 | 1 | docs.rego:49 | +| 5.292µs | 1 | 2 | 1 | docs.rego:55 | +| 18.25µs | 1 | 1 | 1 | data.rbac.allow | ++---------+----------+----------+--------------+-----------------+ ``` As seen from the above table, result are first arranged based on *number of evaluations*, @@ -750,13 +776,14 @@ Adding a unit test file for the [policy source as shown above](#example-policy): ```rego package rbac +import rego.v1 -test_user_has_role_dev { - user_has_role["dev"] with input as {"subject": "alice"} +test_user_has_role_dev if { + user_has_role.dev with input as {"subject": "alice"} } -test_user_has_role_negative { - not user_has_role["super-admin"] with input as {"subject": "alice"} +test_user_has_role_negative if { + not user_has_role["super-admin"] with input as {"subject": "alice"} } ``` diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index 15e36e9ebf..e6c11e3ffb 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -39,7 +39,7 @@ val := arr[0] # lookup last value val := arr[count(arr)-1] -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` some 0, val in arr # lookup value at index 0 0, "foo" in arr # check if value at index 0 is "foo" some i, "foo" in arr # find all indices i that have value "foo" @@ -71,7 +71,7 @@ obj.foo.bar.baz # check if path foo.bar.baz, foo.bar, or foo does not exist or is false not obj.foo.bar.baz -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` o := {"foo": false} # check if value exists: the expression will be true false in o @@ -94,7 +94,7 @@ a_set[["a", "b", "c"]] # find all arrays of the form [x, "b", z] in the set a_set[[x, "b", z]] -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` "foo" in a_set not "foo" in a_set some ["a", "b", "c"] in a_set @@ -120,7 +120,7 @@ val := arr[_] # iterate over index/value pairs val := arr[i] -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` some val in arr # iterate over values some i, _ in arr # iterate over indices some i, val in arr # iterate over index/value pairs @@ -138,7 +138,7 @@ val := obj[_] # iterate over key/value pairs val := obj[key] -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` some val in obj # iterate over values some key, _ in obj # iterate over keys some key, val in obj # key/value pairs @@ -150,7 +150,7 @@ some key, val in obj # key/value pairs # iterate over values set[val] -# with `import future.keywords.in` +# with `import rego.v1` or `import future.keywords.in` some val in set ``` @@ -187,7 +187,7 @@ not any_not_match ``` ```live:iteration/forall:module:read_only -# with `import future.keywords.in` and `import future.keywords.if` +# with `import rego.v1`, or `import future.keywords.in` and `import future.keywords.if` any_match if { some x in set f(x) @@ -223,7 +223,7 @@ c := a | b p := true { ... } # OR -# with `import future.keywords.if` +# with `import rego.v1` or `import future.keywords.if` p if { ... } # OR @@ -233,7 +233,7 @@ p { ... } ### Conditionals ```live:rules/cond:module:read_only -# with `import future.keywords.if` +# with `import rego.v1` or `import future.keywords.if` default a := 1 a := 5 if { ... } a := 100 if { ... } @@ -246,7 +246,7 @@ a := 100 if { ... } a_set[x] { ... } a_set[y] { ... } -# alternatively, with `import future.keywords.contains` and `import future.keywords.if` +# alternatively, with `import rego.v1`, or `import future.keywords.contains` and `import future.keywords.if` a_set contains x if { ... } a_set contains y if { ... } @@ -258,7 +258,7 @@ a_map[w] := z if { ... } ### Ordered (Else) ```live:rules/ordered:module:read_only -# with `import future.keywords.if` +# with `import rego.v1` or `import future.keywords.if` default a := 1 a := 5 if { ... } else := 10 if { ... } @@ -267,7 +267,7 @@ else := 10 if { ... } ### Functions (Boolean) ```live:rules/funcs:module:read_only -# with `import future.keywords.if` +# with `import rego.v1` or `import future.keywords.if` f(x, y) if { ... } @@ -282,7 +282,7 @@ f(x, y) := true if { ### Functions (Conditionals) ```live:rules/condfuncs:module:read_only -# with `import future.keywords.if` +# with `import rego.v1` or `import future.keywords.if` f(x) := "A" if { x >= 90 } f(x) := "B" if { x >= 80; x < 90 } f(x) := "C" if { x >= 70; x < 80 } @@ -291,7 +291,7 @@ f(x) := "C" if { x >= 70; x < 80 } ### Reference Heads ```live:rules/ref_heads:module:read_only -# with `import future.keywords.contains` and `import future.keywords.if` +# with `import rego.v1`, or `import future.keywords.contains` and `import future.keywords.if` fruit.apple.seeds = 12 if input == "apple" # complete document (single value rule) fruit.pineapple.colors contains x if x := "yellow" # multi-value rule @@ -1190,7 +1190,7 @@ OPA doesn't presume what merge strategy is appropriate; instead, this lies in th # - Acme Corp. package example -import future.keywords.in +import rego.v1 # METADATA # scope: document @@ -1200,51 +1200,52 @@ import future.keywords.in # title: My Allow Rule # authors: # - Jane Doe -allow { - meta := merge(rego.metadata.chain()) - meta.title == "My Allow Rule" # 'title' pulled from 'rule' scope - meta.description == "A rule that merges metadata annotations in various ways." # 'description' pulled from 'document' scope - meta.authors == { # 'authors' joined from 'package' and 'rule' scopes - {"email": "jane@example.com", "name": "Jane Doe"}, - {"email": "john@example.com", "name": "John Doe"} - } - meta.organizations == {"Acme Corp."} # 'organizations' pulled from 'package' scope +allow if { + meta := merge(rego.metadata.chain()) + meta.title == "My Allow Rule" # 'title' pulled from 'rule' scope + meta.description == "A rule that merges metadata annotations in various ways." # 'description' pulled from 'document' scope + meta.authors == { + {"email": "jane@example.com", "name": "Jane Doe"}, # 'authors' joined from 'package' and 'rule' scopes + {"email": "john@example.com", "name": "John Doe"}, + } + meta.organizations == {"Acme Corp."} # 'organizations' pulled from 'package' scope } -allow { - meta := merge(rego.metadata.chain()) - meta.title == null # No 'title' present in 'rule' or 'document' scopes - meta.description == "A rule that merges metadata annotations in various ways." # 'description' pulled from 'document' scope - meta.authors == { # 'authors' pulled from 'package' scope - {"email": "john@example.com", "name": "John Doe"} - } - meta.organizations == {"Acme Corp."} # 'organizations' pulled from 'package' scope +allow if { + meta := merge(rego.metadata.chain()) + meta.title == null # No 'title' present in 'rule' or 'document' scopes + meta.description == "A rule that merges metadata annotations in various ways." # 'description' pulled from 'document' scope + meta.authors == { # 'authors' pulled from 'package' scope + {"email": "john@example.com", "name": "John Doe"} + } + meta.organizations == {"Acme Corp."} # 'organizations' pulled from 'package' scope } -merge(chain) := meta { - ruleAndDoc := ["rule", "document"] - meta := { - "title": override_annot(chain, "title", ruleAndDoc), # looks for 'title' in 'rule' scope, then 'document' scope - "description": override_annot(chain, "description", ruleAndDoc), # looks for 'description' in 'rule' scope, then 'document' scope - "related_resources": override_annot(chain, "related_resources", ruleAndDoc), # looks for 'related_resources' in 'rule' scope, then 'document' scope - "authors": merge_annot(chain, "authors"), # merges all 'authors' across all scopes - "organizations": merge_annot(chain, "organizations"), # merges all 'organizations' across all scopes - } +merge(chain) := meta if { + ruleAndDoc := ["rule", "document"] + meta := { + "title": override_annot(chain, "title", ruleAndDoc), # looks for 'title' in 'rule' scope, then 'document' scope + "description": override_annot(chain, "description", ruleAndDoc), # looks for 'description' in 'rule' scope, then 'document' scope + "related_resources": override_annot(chain, "related_resources", ruleAndDoc), # looks for 'related_resources' in 'rule' scope, then 'document' scope + "authors": merge_annot(chain, "authors"), # merges all 'authors' across all scopes + "organizations": merge_annot(chain, "organizations"), # merges all 'organizations' across all scopes + } } -override_annot(chain, name, scopes) := val { - val := [v | - link := chain[_] - link.annotations.scope in scopes - v := link.annotations[name] - ][0] +override_annot(chain, name, scopes) := val if { + val := [v | + link := chain[_] + link.annotations.scope in scopes + v := link.annotations[name] + ][0] } else := null -merge_annot(chain, name) := val { - val := {v | - v := chain[_].annotations[name][_] - } +merge_annot(chain, name) := val if { + val := {v | + v := chain[_].annotations[name][_] + } } else := null + ``` {{< builtin-table cat=opa title=OPA >}} diff --git a/docs/content/policy-testing.md b/docs/content/policy-testing.md index 83413743ae..24728dcb75 100644 --- a/docs/content/policy-testing.md +++ b/docs/content/policy-testing.md @@ -32,16 +32,16 @@ profile. ```live:example:module:read_only,openable package authz -import future.keywords +import rego.v1 allow if { - input.path == ["users"] - input.method == "POST" + input.path == ["users"] + input.method == "POST" } allow if { - input.path == ["users", input.user_id] - input.method == "GET" + input.path == ["users", input.user_id] + input.method == "GET" } ``` @@ -51,22 +51,22 @@ To test this policy, we will create a separate Rego file that contains test case ```live:example/test:module:read_only package authz -import future.keywords +import rego.v1 test_post_allowed if { - allow with input as {"path": ["users"], "method": "POST"} + allow with input as {"path": ["users"], "method": "POST"} } test_get_anonymous_denied if { - not allow with input as {"path": ["users"], "method": "GET"} + not allow with input as {"path": ["users"], "method": "GET"} } test_get_user_allowed if { - allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"} + allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"} } test_get_another_user_denied if { - not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"} + not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"} } ``` @@ -122,10 +122,10 @@ name is prefixed with `test_`. ```live:example_format:module:read_only package mypackage -import future.keywords +import rego.v1 test_some_descriptive_name if { - # test logic + # test logic } ``` @@ -154,7 +154,7 @@ by zero condition) the test result is marked as an `ERROR`. Tests prefixed with ```live:example_results:module:read_only package example -import future.keywords +import rego.v1 # This test will pass. test_ok if true @@ -167,7 +167,7 @@ test_error if 1 / 0 # This test will be skipped. todo_test_missing_implementation if { - allow with data.roles as ["not", "implemented"] + allow with data.roles as ["not", "implemented"] } ``` @@ -255,12 +255,12 @@ Below is a simple policy that depends on the data document. ```live:with_keyword:module:read_only,openable package authz -import future.keywords +import rego.v1 -allow { - some x in data.policies - x.name == "test_policy" - matches_role(input.role) +allow if { + some x in data.policies + x.name == "test_policy" + matches_role(input.role) } matches_role(my_role) if input.user in data.roles[my_role] @@ -272,15 +272,15 @@ Below is the Rego file to test the above policy. ```live:with_keyword/tests:module:read_only package authz -import future.keywords +import rego.v1 policies := [{"name": "test_policy"}] roles := {"admin": ["alice"]} test_allow_with_data if { - allow with input as {"user": "alice", "role": "admin"} - with data.policies as policies - with data.roles as roles + allow with input as {"user": "alice", "role": "admin"} + with data.policies as policies + with data.roles as roles } ``` @@ -299,7 +299,7 @@ Below is an example to replace a **rule without arguments**. ```live:with_keyword_rules:module:read_only package authz -import future.keywords +import rego.v1 allow1 if allow2 @@ -310,10 +310,10 @@ allow2 if 2 == 1 ```live:with_keyword_rules/tests:module:read_only package authz -import future.keywords +import rego.v1 test_replace_rule if { - allow1 with allow2 as true + allow1 with allow2 as true } ``` @@ -330,12 +330,12 @@ 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 rego.v1 import data.jwks.cert allow if { - [true, _, _] = io.jwt.decode_verify(input.headers["x-token"], {"cert": cert, "iss": "corp.issuer.com"}) + [true, _, _] = io.jwt.decode_verify(input.headers["x-token"], {"cert": cert, "iss": "corp.issuer.com"}) } ``` @@ -343,16 +343,15 @@ allow if { ```live:with_keyword_builtins/tests:module:read_only package authz -import future.keywords +import rego.v1 mock_decode_verify("my-jwt", _) := [true, {}, {}] mock_decode_verify(x, _) := [false, {}, {}] if x != "my-jwt" test_allow if { - allow - with input.headers["x-token"] as "my-jwt" - with data.jwks.cert as "mock-cert" - with io.jwt.decode_verify as mock_decode_verify + allow with input.headers["x-token"] as "my-jwt" + with data.jwks.cert as "mock-cert" + with io.jwt.decode_verify as mock_decode_verify } ``` @@ -367,10 +366,10 @@ 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 if { - allow - with input.headers["x-token"] as "my-jwt" - with data.jwks.cert as "mock-cert" - with io.jwt.decode_verify as [true, {}, {}] + allow + with input.headers["x-token"] as "my-jwt" + with data.jwks.cert as "mock-cert" + with io.jwt.decode_verify as [true, {}, {}] } ``` @@ -384,14 +383,14 @@ function by a built-in function. ```live:with_keyword_funcs:module:read_only package authz -import future.keywords +import rego.v1 replace_rule if { - replace(input.label) + replace(input.label) } replace(label) if { - label == "test_label" + label == "test_label" } ``` @@ -399,10 +398,10 @@ replace(label) if { ```live:with_keyword_funcs/tests:module:read_only package authz -import future.keywords +import rego.v1 test_replace_rule if { - replace_rule with input.label as "does-not-matter" with replace as true + replace_rule with input.label as "does-not-matter" with replace as true } ``` diff --git a/docs/content/rest-api.md b/docs/content/rest-api.md index ab4318e3f2..449dc62805 100644 --- a/docs/content/rest-api.md +++ b/docs/content/rest-api.md @@ -666,12 +666,14 @@ Content-Type: text/plain ```live:put_example:module:read_only package opa.examples -import data.servers +import rego.v1 + import data.networks import data.ports +import data.servers -public_servers[server] { - some k, m +public_servers contains server if { + some k, m server := servers[_] server.ports[_] == ports[k].id ports[k].networks[_] == networks[m].id @@ -886,9 +888,11 @@ The examples below assume the following policy: ```live:input_example:module:read_only package opa.examples +import rego.v1 + import input.example.flag -allow_request { flag == true } +allow_request if flag == true ``` #### Example Request @@ -989,9 +993,11 @@ The examples below assume the following policy: ```live:webhook_example:module:read_only package opa.examples +import rego.v1 + import input.example.flag -allow_request { flag == true } +allow_request if flag == true ``` #### Example Request @@ -1200,8 +1206,10 @@ Content-Type: text/plain ```live:system_example:module:read_only package system -main = msg { - msg := sprintf("hello, %v", [input.user]) +import rego.v1 + +main := msg if { + msg := sprintf("hello, %v", [input.user]) } ``` @@ -1349,8 +1357,10 @@ The example below assumes that OPA has been given the following policy: ```live:compile_example:module:read_only package example -allow { - input.subject.clearance_level >= data.reports[_].clearance_level +import rego.v1 + +allow if { + input.subject.clearance_level >= data.reports[_].clearance_level } ``` @@ -1444,12 +1454,14 @@ For example, if you extend to policy above to include a "break glass" condition, ```live:compile_unconditional_example:module:read_only package example -allow { - input.subject.clearance_level >= data.reports[_].clearance_level +import rego.v1 + +allow if { + input.subject.clearance_level >= data.reports[_].clearance_level } -allow { - data.break_glass = true +allow if { + data.break_glass = true } ``` @@ -1523,13 +1535,15 @@ exception: ```live:compile_unconditional_false_example:module:read_only package example -allow { - input.subject.clearance_level >= data.reports[_].clearance_level - exceptions[input.subject.name] +import rego.v1 + +allow if { + input.subject.clearance_level >= data.reports[_].clearance_level + exceptions[input.subject.name] } -exceptions["bob"] -exceptions["alice"] +exceptions contains "bob" +exceptions contains "alice" ``` In this case, if we execute query on behalf of a user that does not @@ -1657,15 +1671,17 @@ able to process the `live` rule. OPA is ready once all plugins have entered the ```live:health_policy_example:module:read_only package system.health +import rego.v1 + # opa is live if it can process this rule -default live = true +default live := true # by default, opa is not ready -default ready = false +default ready := false # opa is ready once all plugins have reported OK at least once -ready { - input.plugins_ready +ready if { + input.plugins_ready } ``` @@ -1675,15 +1691,17 @@ specific a plugin leaves the OK state, try this: ```live:health_policy_example_2:module:read_only package system.health -default live = true +import rego.v1 + +default live := true -default ready = false +default ready := false # opa is ready once all plugins have reported OK at least once AND # the bundle plugin is currently in an OK state -ready { - input.plugins_ready - input.plugin_state.bundle == "OK" +ready if { + input.plugins_ready + input.plugin_state.bundle == "OK" } ``` diff --git a/docs/content/security.md b/docs/content/security.md index b5996864b9..3f9485a17d 100644 --- a/docs/content/security.md +++ b/docs/content/security.md @@ -159,10 +159,12 @@ must be provided on startup. The authorization policy must be structured as foll # system.authz as follows: package system.authz -default allow := false # Reject requests by default. +import rego.v1 -allow { - # Logic to authorize request goes here. +default allow := false # Reject requests by default. + +allow if { + # Logic to authorize request goes here. } ``` @@ -265,9 +267,11 @@ identity: ```live:system_authz_secret:module:read_only package system.authz -default allow := false # Reject requests by default. +import rego.v1 + +default allow := false # Reject requests by default. -allow { # Allow request if... +allow if { # Allow request if... "secret" == input.identity # Identity is the secret root key. } ``` @@ -315,18 +319,20 @@ follows: ```live:system_authz_object_resp:module:read_only package system.authz +import rego.v1 + default allow := { - "allowed": false, - "reason": "unauthorized resource access" + "allowed": false, + "reason": "unauthorized resource access", } -allow := { "allowed": true } { # Allow request if... - "secret" == input.identity # identity is the secret root key. +allow := {"allowed": true} if { # Allow request if... + "secret" == input.identity # identity is the secret root key. } -allow := { "allowed": false, "reason": reason } { - not input.identity - reason := "no identity provided" +allow := {"allowed": false, "reason": reason} if { + not input.identity + reason := "no identity provided" } ``` @@ -338,6 +344,8 @@ validate the identity: ```live:system_authz_bearer:module:read_only package system.authz +import rego.v1 + # Tokens may defined in policy or pushed into OPA as data. tokens := { "my-secret-token-foo": { @@ -351,14 +359,14 @@ tokens := { } } -default allow := false # Reject requests by default. +default allow := false # Reject requests by default. -allow { # Allow request if... +allow if { # Allow request if... input.identity == "secret" # Identity is the secret root key. } -allow { # Allow request if... - tokens[input.identity] # Identity exists in "tokens". +allow if { # Allow request if... + tokens[input.identity] # Identity exists in "tokens". } ``` @@ -368,6 +376,8 @@ documents: ```live:system_authz_bearer_complete:module:read_only package system.authz +import rego.v1 + # Rights may be defined in policy or pushed into OPA as data. rights := { "admin": { @@ -396,19 +406,19 @@ tokens := { default allow := false # Reject requests by default. -allow { # Allow request if... +allow if { # Allow request if... some right - identity_rights[right] # Rights for identity exist, and... - right.path == "*" # Right.path is '*'. + identity_rights[right] # Rights for identity exist, and... + right.path == "*" # Right.path is '*'. } -allow { # Allow request if... +allow if { # Allow request if... some right - identity_rights[right] # Rights for identity exist, and... - right.path == input.path # Right.path matches input.path. + identity_rights[right] # Rights for identity exist, and... + right.path == input.path # Right.path matches input.path. } -identity_rights[right] { # Right is in the identity_rights set if... +identity_rights contains right if { # Right is in the identity_rights set if... token := tokens[input.identity] # Token exists for identity, and... role := token.roles[_] # Token has a role, and... right := rights[role] # Role has rights defined. @@ -506,28 +516,27 @@ information such as which paths are allowed. ```live:system_authz_x509:module:read_only package system.authz -import future.keywords.if -import future.keywords.in +import rego.v1 id_uri := input.client_certificates[0].URIs[0] id_string := sprintf("%s://%s%s", [id_uri.Scheme, id_uri.Host, id_uri.Path]) # client_acl represents an access control list and may defined in policy or pushed into OPA as data changes. client_acl := { - "spiffe://example.com/client-1": [["v1", "data"]], - "spiffe://example.com/client-2": [], + "spiffe://example.com/client-1": [["v1", "data"]], + "spiffe://example.com/client-2": [], } default allow := {"allowed": false, "reason": "Access denied: unknown caller"} -allow := { "allowed": true } if { - input.path in client_acl[id_string] +allow := {"allowed": true} if { + input.path in client_acl[id_string] } else := { - "allowed": false, - "reason": sprintf("%s is not allowed to call /%s", [ - id_string, - concat("/", input.path), - ]) + "allowed": false, + "reason": sprintf("%s is not allowed to call /%s", [ + id_string, + concat("/", input.path), + ]), } ``` @@ -627,13 +636,15 @@ clients access to the default policy decision, i.e., `POST /`: ```live:hardened_example:module:read_only package system.authz +import rego.v1 + # Deny access by default. default allow := false # Allow anonymous access to the default policy decision. -allow { - input.method == "POST" - input.path == [""] +allow if { + input.method == "POST" + input.path == [""] } ``` diff --git a/docs/content/ssh-and-sudo-authorization.md b/docs/content/ssh-and-sudo-authorization.md index 2a0a4989d8..cabbb572e8 100644 --- a/docs/content/ssh-and-sudo-authorization.md +++ b/docs/content/ssh-and-sudo-authorization.md @@ -160,6 +160,8 @@ and non-admins to only SSH into hosts that they contributed code to. ```live:sshd_authz:module:read_only package sshd.authz +import rego.v1 + import input.pull_responses import input.sysinfo @@ -169,8 +171,8 @@ import data.hosts default allow := false # Allow access to any user that has the "admin" role. -allow { - data.roles["admin"][_] == input.sysinfo.pam_username +allow if { + data.roles.admin[_] == input.sysinfo.pam_username } # Allow access to any user who contributed to the code running on the host. @@ -181,13 +183,13 @@ allow { # # It then compares all the contributors for that host against the username # that is asking for authorization. -allow { - hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] == sysinfo.pam_username +allow if { + hosts[pull_responses.files["/etc/host_identity.json"].host_id].contributors[_] == sysinfo.pam_username } # If the user is not authorized, then include an error message in the response. -errors["Request denied by administrative policy"] { - not allow +errors contains "Request denied by administrative policy" if { + not allow } ``` @@ -200,17 +202,19 @@ Create the `sudo` authorization policy. It should allow only admins to use `sudo ```live:sudo_authz:module:read_only package sudo.authz +import rego.v1 + # By default, users are not authorized. default allow := false # Allow access to any user that has the "admin" role. -allow { - data.roles["admin"][_] == input.sysinfo.pam_username +allow if { + data.roles.admin[_] == input.sysinfo.pam_username } # If the user is not authorized, then include an error message in the response. -errors["Request denied by administrative policy"] { - not allow +errors contains "Request denied by administrative policy" if { + not allow } ``` @@ -371,14 +375,16 @@ Then we need to make sure that the authorization takes this input into account. # A package can be defined across multiple files. package sudo.authz +import rego.v1 + import data.elevate -import input.sysinfo import input.display_responses +import input.sysinfo # Allow this user if the elevation ticket they provided matches our mock API # of an internal elevation system. -allow { - elevate.tickets[sysinfo.pam_username] == display_responses.ticket +allow if { + elevate.tickets[sysinfo.pam_username] == display_responses.ticket } ``` diff --git a/docs/content/terraform.md b/docs/content/terraform.md index af096f7ba4..faa14c9916 100644 --- a/docs/content/terraform.md +++ b/docs/content/terraform.md @@ -470,6 +470,8 @@ practice you would vary the threshold depending on the user.) ```live:terraform:module:openable package terraform.analysis +import rego.v1 + import input as tfplan ######################## @@ -481,8 +483,8 @@ blast_radius := 30 # weights assigned for each operation on each resource-type weights := { - "aws_autoscaling_group": {"delete": 100, "create": 10, "modify": 1}, - "aws_instance": {"delete": 10, "create": 1, "modify": 1} + "aws_autoscaling_group": {"delete": 100, "create": 10, "modify": 1}, + "aws_instance": {"delete": 10, "create": 1, "modify": 1}, } # Consider exactly these resource types in calculations @@ -494,28 +496,29 @@ resource_types := {"aws_autoscaling_group", "aws_instance", "aws_iam", "aws_laun # Authorization holds if score for the plan is acceptable and no changes are made to IAM default authz := false -authz { - score < blast_radius - not touches_iam + +authz if { + score < blast_radius + not touches_iam } # Compute the score for a Terraform plan as the weighted sum of deletions, creations, modifications -score := s { - all := [ x | - some resource_type - crud := weights[resource_type]; - del := crud["delete"] * num_deletes[resource_type]; - new := crud["create"] * num_creates[resource_type]; - mod := crud["modify"] * num_modifies[resource_type]; - x := del + new + mod - ] - s := sum(all) +score := s if { + all := [x | + some resource_type + crud := weights[resource_type] + del := crud["delete"] * num_deletes[resource_type] + new := crud["create"] * num_creates[resource_type] + mod := crud["modify"] * num_modifies[resource_type] + x := (del + new) + mod + ] + s := sum(all) } # Whether there is any change to IAM -touches_iam { - all := resources["aws_iam"] - count(all) > 0 +touches_iam if { + all := resources.aws_iam + count(all) > 0 } #################### @@ -523,41 +526,40 @@ touches_iam { #################### # list of all resources of a given type -resources[resource_type] := all { - some resource_type - resource_types[resource_type] - all := [name | - name:= tfplan.resource_changes[_] - name.type == resource_type - ] +resources[resource_type] := all if { + some resource_type + resource_types[resource_type] + all := [name | + name := tfplan.resource_changes[_] + name.type == resource_type + ] } # number of creations of resources of a given type -num_creates[resource_type] := num { - some resource_type - resource_types[resource_type] - all := resources[resource_type] - creates := [res | res:= all[_]; res.change.actions[_] == "create"] - num := count(creates) +num_creates[resource_type] := num if { + some resource_type + resource_types[resource_type] + all := resources[resource_type] + creates := [res | res := all[_]; res.change.actions[_] == "create"] + num := count(creates) } - # number of deletions of resources of a given type -num_deletes[resource_type] := num { - some resource_type - resource_types[resource_type] - all := resources[resource_type] - deletions := [res | res:= all[_]; res.change.actions[_] == "delete"] - num := count(deletions) +num_deletes[resource_type] := num if { + some resource_type + resource_types[resource_type] + all := resources[resource_type] + deletions := [res | res := all[_]; res.change.actions[_] == "delete"] + num := count(deletions) } # number of modifications to resources of a given type -num_modifies[resource_type] := num { - some resource_type - resource_types[resource_type] - all := resources[resource_type] - modifies := [res | res:= all[_]; res.change.actions[_] == "update"] - num := count(modifies) +num_modifies[resource_type] := num if { + some resource_type + resource_types[resource_type] + all := resources[resource_type] + modifies := [res | res := all[_]; res.change.actions[_] == "update"] + num := count(modifies) } ``` @@ -792,73 +794,75 @@ The policy uses the walk keyword to explore the json structure, and uses conditi ```rego package terraform.module -deny[msg] { - desc := resources[r].values.description - contains(desc, "HTTP") - msg := sprintf("No security groups should be using HTTP. Resource in violation: %v", [r.address]) +import rego.v1 + +deny contains msg if { + desc := resources[r].values.description + contains(desc, "HTTP") + msg := sprintf("No security groups should be using HTTP. Resource in violation: %v", [r.address]) } -resources := { r | - some path, value +resources := {r | + some path, value - # Walk over the JSON tree and check if the node we are - # currently on is a module (either root or child) resources - # value. - walk(input.planned_values, [path, value]) + # Walk over the JSON tree and check if the node we are + # currently on is a module (either root or child) resources + # value. + walk(input.planned_values, [path, value]) - # Look for resources in the current value based on path - rs := module_resources(path, value) + # Look for resources in the current value based on path + rs := module_resources(path, value) - # Aggregate them into `resources` - r := rs[_] + # Aggregate them into `resources` + r := rs[_] } # Variant to match root_module resources -module_resources(path, value) := rs { - # Expect something like: - # - # { - # "root_module": { - # "resources": [...], - # ... - # } - # ... - # } - # - # Where the path is [..., "root_module", "resources"] - - reverse_index(path, 1) == "resources" - reverse_index(path, 2) == "root_module" - rs := value +module_resources(path, value) := rs if { + # Expect something like: + # + # { + # "root_module": { + # "resources": [...], + # ... + # } + # ... + # } + # + # Where the path is [..., "root_module", "resources"] + + reverse_index(path, 1) == "resources" + reverse_index(path, 2) == "root_module" + rs := value } # Variant to match child_modules resources -module_resources(path, value) := rs { - # Expect something like: - # - # { - # ... - # "child_modules": [ - # { - # "resources": [...], - # ... - # }, - # ... - # ] - # ... - # } - # - # Where the path is [..., "child_modules", 0, "resources"] - # Note that there will always be an index int between `child_modules` - # and `resources`. We know that walk will only visit each one once, - # so we shouldn't need to keep track of what the index is. - - reverse_index(path, 1) == "resources" - reverse_index(path, 3) == "child_modules" - rs := value +module_resources(path, value) := rs if { + # Expect something like: + # + # { + # ... + # "child_modules": [ + # { + # "resources": [...], + # ... + # }, + # ... + # ] + # ... + # } + # + # Where the path is [..., "child_modules", 0, "resources"] + # Note that there will always be an index int between `child_modules` + # and `resources`. We know that walk will only visit each one once, + # so we shouldn't need to keep track of what the index is. + + reverse_index(path, 1) == "resources" + reverse_index(path, 3) == "child_modules" + rs := value } -reverse_index(path, idx) := value { +reverse_index(path, idx) := value if { value := path[count(path) - idx] } ```