Links

Policies

Policies, written as Open Policy Agent (OPA) Policies in Rego, define if someone should get access.
There are two types of Policies:
  1. 1.
    Access Policies define if someone should get access.
  2. 2.
    Workflow Policies define if a Workflow Step should be skipped.

Polices Spec

Access Policies

policies = [
# Access Policies
{
bundle = "github://organization/repo/path/to/policies[.tar.gz]"
# or,
# query = <<-EOT
# package main
#
# ...
# EOT
},
...
]

Workflow Policies

workflow = {
steps = [
{
...
# Workflow Policies
skip_if = [
{
# bundle = "..."
# query = "..."
}
]
}
]
}

Configuring Policies For Your Grant Kit

Access Policies and Workflow Policies are configured in identical ways.
  • For Access Policies, put your policies in the policies attribute.
  • For Workflow Policies, use the skip_if attribute.
These attributes represent a list of OPA Policies as either a bundle or a query.
Example Policy Configurations
1
// Grant Kit with an Access Policy containing a single bundle.
2
policies = [
3
{ bundle = "..." }
4
]
5
6
// Grant Kit with an Access Policy containing two bundles.
7
policies = [
8
{ bundle = "..." },
9
{ bundle = "..." },
10
]
11
12
// Grant Kit with an Access Policy containing two bundles and a single policy.
13
policies = [
14
{ bundle = "..." },
15
{ bundle = "..." },
16
{ query = "..." },
17
]
18
19
// Grant Kit with a single step configured with a Workflow Policy that
20
// contains a single bundle.
21
steps = [
22
{
23
skip_if = [
24
{ bundle = "..." }
25
]
26
}
27
]
28
29
// Grant Kit with two steps, one of which is configured with
30
// a Workflow Policy that contains a single bundle and the
31
// other with a Workflow Policy that contain a bundle and a query.
32
steps = [
33
{
34
skip_if = [
35
{ bundle = "..." }
36
]
37
},
38
{
39
skip_if = [
40
{ bundle = "..." },
41
{ query = "..." }
42
]
43
}
44
]

Bundles

A Bundle is a native OPA Bundle that represents a group of OPA Policies. Bundles are typically either a directory or a gzipped tarball.
Bundles are configured using an RFC 3986 URI string for the bundle attribute. This string must point to the location of your bundle in your code repository.

Bundle Spec

github://{organization}/{repository}/path/to/my/bundle[.tar.gz]
Example Bundle Configurations
// Organization: abbeylabs
// Repository: starter-kit-quickstart
// Path to policy bundle: /policies/soc2.tar.gz
//
// This example assumes the soc2.tar.gz was built ahead of time
// using `opa build` and is committed directly to the
// `starter-kit-quickstart` repository.
github://abbeylabs/starter-kit-quickstart/policies/soc2.tar.gz
// Organization: jeffchao (personal, not a GitHub org)
// Repository: my-starter-kit
// Path to policy bundle: /policies/access-policies/hipaa
//
// This example assumes `opa build` was not used and instead prefers
// Abbey to build the bundle instead. Abbey will inspect the `hipaa`
// directory and recursively add all OPA Policies defined with
// the `.rego` extension to the bundle.
github://jeffchao/my-starter-kit/policies/access-policies/hipaa
Currently Abbey supports the github:// scheme. Future schemes such as file://, s3://, and https:// coming soon.

Organizing Bundles

Bundles are great for distributing and reusing policies.
Before building bundles, you should first organize your policies into a directory structure that represents the functionality of the bundle, for example:
❯ tree -a
.
└── policies
├── soc2
│   ├── .manifest
│   └── soc2-type2
│   └── example.rego
└── privacy
├── .manifest
└── privacy-gdpr
└── example.rego
5 directories, 4 files
The above setup has a policies directory that contains 2 bundles, each with 1 package in them. The bundles contain a .manifest file to define the packages it knows about and is used to avoid package conflicts.
Example .manifest file
{
"roots": [
"soc2-type2",
"privacy-gdpr"
]
}
.manifest files are required if you're using OPA bundles. They help avoid package conflicts. As we add more packages, we add their paths to the .manifest file.

Building Bundles

Once you have your directory set up, you can build your bundles using the OPA CLI, for example:
# Build the `soc2-type2` bundle from the `soc2` directory and
# Output the bundle to the `policies/soc2` directory.
opa build -b policies/soc2/soc2-type2 -o policies/soc2/soc2-type2.tar.gz
# Build the `privacy-gdpr` bundle from the `privacy` directory and
# Output the bundle to the `policies/privacy` directory.
opa build -b policies/privacy/privacy-gdpr -o policies/privacy/privacy-gdpr.tar.gz

Queries

A Query represents a single OPA Policy. Queries are typically used for simple one-off rules that don't require the hierarchical rule organization that Bundles provide and aren't intended to be distributed.
Queries are configured by writing native Rego code directly as a string, typically a multiline Heredoc string for better visibility.
Queries must be defined using the main namespace for Abbey to evaluate it.
Example Query Configurations
// Policies defined using `query` must use `package main`.
// This example shows the use of a multiline Heredoc string to define a simple
// Policy that always denies access.
query = <<-EOT
package main
deny[msg] {
true
msg := "always deny access because we're always returning true"
}
EOT
// This example shows the same policy above, but using a single line string.
// The result is the same but the configuration is harder to read.
query = "package main\n\ndeny[msg] {\n\ttrue\n\tmsg := \"always deny access because we're always returning true\"\n}"

Writing Policies For Your Grant Kit

Policies are implemented using the Abbey OPA Constraint Framework. This Framework is simplified subset of the OPA Constraint Framework that makes working with OPA and Rego easier.
Writing Policies consists of three steps:

Define Your Logic Using Rego

Access Policies and Workflow Policies are implemented using identical schemas.

Writing Access Policies

Access Policies are written using the following schema:
1
allow[msg] {
2
// Policy logic goes here.
3
msg := "return an explanation for why the policy passes"
4
}
Example Access Policies
1
// Access Policy that always grants access because the rule will always
2
// return `true`.
3
allow[msg] {
4
true
5
msg := "granting access for non-sensitive resource"
6
}
7
8
// Access Policy that always grants access, but expires after 24 hours.
9
// Assumes you added `import data.abbey.functions` in your policy.
10
allow[msg] {
11
true; functions.expire_after("24h")
12
msg := "always approves access but will eventually expire"
13
}
14
15
// Access Policy that always grants access, but expires at a specific date.
16
// Assumes you added `import data.abbey.functions` in your policy.
17
allow[msg] {
18
true; functions.expire_at("2023-06-16T07:15:58+00:00"
19
msg := "always approves but will expire at a specific time"
20
}
21
22
// Access Policy that grants access to sensitive infrastructure if the
23
// requester is currently on call.
24
// `input` comes from infrastructure changes, generally `tfplan.json` via
25
// your CI's `terraform plan` output.
26
// `data` is automatically enriched for you by Abbey.
27
allow[msg] {
28
input.resource_changes[_].change.after.database_name == "pii_customers"
29
data.system.abbey.pagerduty.isoncall == true
30
msg := "for the pii_customers database, grant access only to on-calls. everyone else is denied by default"
31
}

Automatic Revocation

Once access is granted, Abbey will continuously monitor your policies and revoke access in realtime if they evaluate to false. This may happen if you set an expiration through functions.expire_at or functions.expire_after, a user's attribute has changed (e.g., they went off-call), or infrastructure has changed (e.g., a database was now marked as sensitive). You get this functionality out-of-the-box without having to configure anything extra.

Writing Workflow Policies

Workflow Policies are written using the following schema:
1
skip[msg] {
2
// Policy logic goes here.
3
msg := "return an explanation for why this step was skipped"
4
}
Example Workflow Policies
1
// Workflow Policy that always skips its step because the `skip` rule
2
// always evaluates to `true`.
3
skip[msg] {
4
true
5
msg := "always skip this step"
6
}

Use Policy Inputs and Enriched Data In Your Logic

In order to write meaningful policies, you need to be able to compare properties across different systems.
For example, you might want to create a policy that denies access to a sensitive database table by default, unless they're an engineer and active on an on-call rotation. "Sensitive database table", "engineer", and "active on an on-call rotation" are properties from systems such as Terraform Plan, GitHub Teams, and PagerDuty Schedules.
Abbey provides two categories of external data for you when writing your policies:

Policy Inputs

Policy Inputs represent the Terraform Plan output that contains the preview of changes that Terraform plans to make to your infrastructure. This allows you to write policies based on what your infrastructure looks like.
Usage
  • To use Policy Inputs, use the input object in your Rego code.
For example, given the following Terraform Plan output as your input:
{
"resource_changes": [
{
"change": {
"after": {
"role_name": "PII_READONLY"
}
}
}
]
}
You can define an Access Policy that automatically denies access to anyone attempting to get access to the PII_READONLY role:
deny[msg] {
input.resource_changes.change.after.role_name == "PII_READONLY"
msg := "auto deny anyone attempting to get access to the PII_READONLY role"
}
The input schema is native to Terraform and has many other attributes available for you to use.
Full Example of input
input.json
{
"format_version": "1.1",
"terraform_version": "1.4.2",
"variables": {
"account": {
"value": "***"
},
"password": "***",
"username": {
"value": "***"
}
},
"planned_values": {
"root_module": {
"resources": [
{
"address": "abbey_grant_kit.role__pii_readonly",
"mode": "managed",
"type": "abbey_grant_kit",
"name": "role__pii_readonly",
"provider_name": "registry.terraform.io/abbeylabs/abbey",
"schema_version": 0,
"values": {
"description": "Grants access to the PII READONLY Snowflake Role Grant.\n",
"name": "Abbey Alpha Demo: PII READONLY role grant",
"output": {
"append": "resource \"snowflake_role_grants\" \"pii_readonly__{{ .data.system.abbey.secondary_identities.snowflake.username }}\" {\n role_name = \"PII_READONLY\"\n users = [\"{{ .data.system.abbey.secondary_identities.snowflake.username }}\"]\n}\n",
"location": "github://organization/repo/access.tf",
"overwrite": null
},
"policies": [
{
"bundle": null,
"query": "package main\n\nwarn[msg] {\n input.resource_changes[_].change.after.database_name == \"DATABASE\"\n msg := \"be careful granting access to sensitive data\"\n}\n"
}
],
"workflow": {
"steps": [
{
"reviewers": {
"all_of": null,
"one_of": [
]
},
"skip_if": null
}
]
}
},
"sensitive_values": {
"output": {},
"policies": [],
"workflow": {
"steps": [
{
"reviewers": {
"one_of": [
false
]
}
}
]
}
}
},
{
"address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
"mode": "managed",
"type": "snowflake_role_grants",
"name": "pii_readonly__REPLACE_ME",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"schema_version": 0,
"values": {
"enable_multiple_grants": false,
"role_name": "PII_READONLY",
"roles": null,
"users": [
"REPLACE_ME"
]
},
"sensitive_values": {
"users": [
false
]
}
},
{
"address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
"mode": "managed",
"type": "snowflake_table_grant",
"name": "pii_readonly__can_read__pii__table",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"schema_version": 0,
"values": {
"database_name": "DATABASE",
"enable_multiple_grants": false,
"on_future": false,
"privilege": "SELECT",
"roles": [
"PII_READONLY"
],
"schema_name": "SCHEMA",
"shares": null,
"table_name": "TABLE",
"with_grant_option": false
},
"sensitive_values": {
"roles": [
false
]
}
}
]
}
},
"resource_changes": [
{
"address": "abbey_grant_kit.role__pii_readonly",
"mode": "managed",
"type": "abbey_grant_kit",
"name": "role__pii_readonly",
"provider_name": "registry.terraform.io/abbeylabs/abbey",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"description": "Grants access to the PII READONLY Snowflake Role Grant.\n",
"name": "Name",
"output": {
"append": "resource \"snowflake_role_grants\" \"pii_readonly__{{ .data.system.abbey.secondary_identities.snowflake.username }}\" {\n role_name = \"PII_READONLY\"\n users = [\"{{ .data.system.abbey.secondary_identities.snowflake.username }}\"]\n}\n",
"location": "github://organization/repo/access.tf",
"overwrite": null
},
"policies": [
{
"bundle": null,
"query": "package main\n\nwarn[msg] {\n input.resource_changes[_].change.after.database_name == \"DATABASE\"\n msg := \"be careful granting access to sensitive data\"\n}\n"
}
],
"workflow": {
"steps": [
{
"reviewers": {
"all_of": null,
"one_of": [
]
},
"skip_if": null
}
]
}
},
"after_unknown": {
"id": true,
"output": {},
"policies": [],
"workflow": {
"steps": [
{
"reviewers": {
"one_of": [
false
]
}
}
]
}
},
"before_sensitive": false,
"after_sensitive": {
"output": {},
"policies": ,
"workflow": {
"steps": [
{
"reviewers": {
"one_of": [
false
]
}
}
]
}
}
}
},
{
"address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
"mode": "managed",
"type": "snowflake_role_grants",
"name": "pii_readonly__REPLACE_ME",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"enable_multiple_grants": false,
"role_name": "PII_READONLY",
"roles": null,
"users": [
"***"
]
},
"after_unknown": {
"id": true,
"users": [
false
]
},
"before_sensitive": false,
"after_sensitive": {
"users": [
false
]
}
}
},
{
"address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
"mode": "managed",
"type": "snowflake_table_grant",
"name": "pii_readonly__can_read__pii__table",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"database_name": "DATABASE",
"enable_multiple_grants": false,
"on_future": false,
"privilege": "SELECT",
"roles": [
"PII_READONLY"
],
"schema_name": "SCHEMA",
"shares": null,
"table_name": "TABLE",
"with_grant_option": false
},
"after_unknown": {
"id": true,
"roles": [
false
]
},
"before_sensitive": false,
"after_sensitive": {
"roles": [
false
]
}
}
}
],
"prior_state": {
"format_version": "1.0",
"terraform_version": "1.4.2",
"values": {
"root_module": {
"resources": [
{
"address": "data.snowflake_database.pii_database",
"mode": "data",
"type": "snowflake_database",
"name": "pii_database",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"schema_version": 0,
"values": {
"comment": "",
"created_on": "DATE",
"id": "DATABASE",
"is_current": false,
"is_default": false,
"name": "DATABASE",
"options": "",
"origin": "",
"owner": "ACCOUNTADMIN",
"retention_time": 1
},
"sensitive_values": {}
},
{
"address": "data.snowflake_role.pii_readonly_role",
"mode": "data",
"type": "snowflake_role",
"name": "pii_readonly_role",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"schema_version": 0,
"values": {
"comment": "This role allows identities to read tables which contain PII",
"id": "PII_READONLY",
"name": "PII_READONLY"
},
"sensitive_values": {}
},
{
"address": "data.snowflake_users.my_snowflake_user",
"mode": "data",
"type": "snowflake_users",
"name": "my_snowflake_user",
"provider_name": "registry.terraform.io/snowflake-labs/snowflake",
"schema_version": 0,
"values": {
"id": "VMB31206.AWS_US_WEST_2",
"pattern": "REPLACE_ME",
"users": [
{
"comment": "",
"default_namespace": "",
"default_role": "ROLE",
"default_secondary_roles": [
""
],
"default_warehouse": "",
"disabled": false,
"display_name": "REPLACE_ME",
"email": "[email protected]",
"first_name": "Firstname",
"has_rsa_public_key": false,
"last_name": "Lastname",
"login_name": "REPLACE_ME",
"name": "REPLACE_ME"
}
]
},
"sensitive_values": {
"users": [
{
"default_secondary_roles": [
false
]
}
]
}
}
]
}
}
},
"configuration": {
"provider_config": {
"abbey": {
"name": "abbey",
"full_name": "registry.terraform.io/abbeylabs/abbey",
"version_constraint": "0.1.2"
},
"snowflake": {
"name": "snowflake",
"full_name": "registry.terraform.io/snowflake-labs/snowflake",
"version_constraint": "0.56.5",
"expressions": {
"account": {
"references": [
"var.account"
]
},
"password": {
"references": [
"var.password"
]
},
"username": {
"references": [
"var.username"
]
}
}
}
},
"root_module": {
"resources": [
{
"address": "abbey_grant_kit.role__pii_readonly",
"mode": "managed",
"type": "abbey_grant_kit",
"name": "role__pii_readonly",
"provider_config_key": "abbey",
"expressions": {
"description": {
"constant_value": "Grants access to the PII READONLY Snowflake Role Grant.\n"
},
"name": {
"constant_value": "Name"
},
"output": {
"references": [
"data.snowflake_role.pii_readonly_role.name",
"data.snowflake_role.pii_readonly_role"
]
},
"policies": {
"references": [
"data.snowflake_database.pii_database.name",
"data.snowflake_database.pii_database"
]
},
"workflow": {
"constant_value": {
"steps": [
{
"reviewers": {
"one_of": [
]
}
}
]
}
}
},
"schema_version": 0
},
{
"address": "snowflake_role_grants.pii_readonly__REPLACE_ME",
"mode": "managed",
"type": "snowflake_role_grants",
"name": "pii_readonly__REPLACE_ME",
"provider_config_key": "snowflake",
"expressions": {
"role_name": {
"constant_value": "PII_READONLY"
},
"users": {
"constant_value": [
"REPLACE_ME"
]
}
},
"schema_version": 0
},
{
"address": "snowflake_table_grant.pii_readonly__can_read__pii__table",
"mode": "managed",
"type": "snowflake_table_grant",
"name": "pii_readonly__can_read__pii__table",
"provider_config_key": "snowflake",
"expressions": {
"database_name": {
"references": [
"data.snowflake_database.pii_database.name",
"data.snowflake_database.pii_database"
]
},
"privilege": {
"constant_value": "SELECT"
},
"roles": {
"references": [
"data.snowflake_role.pii_readonly_role.name",
"data.snowflake_role.pii_readonly_role"
]
},
"schema_name": {
"constant_value": "SCHEMA"
},
"table_name": {
"constant_value": "TABLE"
},
"with_grant_option": {
"constant_value": false
}
},
"schema_version": 0
},
{
"address": "data.snowflake_database.pii_database",
"mode": "data",
"type": "snowflake_database",
"name": "pii_database",
"provider_config_key": "snowflake",
"expressions": {
"name": {
"constant_value": "DATABASE"
}
},
"schema_version": 0
},
{
"address": "data.snowflake_role.pii_readonly_role",
"mode": "data",
"type": "snowflake_role",
"name": "pii_readonly_role",
"provider_config_key": "snowflake",
"expressions": {
"name": {
"constant_value": "PII_READONLY"
}
},
"schema_version": 0
},
{
"address": "data.snowflake_users.my_snowflake_user",
"mode": "data",
"type": "snowflake_users",
"name": "my_snowflake_user",
"provider_config_key": "snowflake",
"expressions": {
"pattern": {
"constant_value": "REPLACE_ME"
}
},
"schema_version": 0
}
],
"variables": {
"account": {
"description": "Snowflake account",
"sensitive": true
},
"password": {
"description": "Snowflake password",
"sensitive": true
},
"username": {
"description": "Snowflake username",
"sensitive": true
}
}
}
},
"relevant_attributes": [
{
"resource": "data.snowflake_role.pii_readonly_role",
"attribute": [
"name"
]
},
{
"resource": "data.snowflake_database.pii_database",
"attribute": [
"name"
]
}
]
}
input is accessible for both Access Policies and Workflow Policies.

Enriched Data

Enriched Data represents properties from external systems that generally contain information about who someone is. This allows you to write policies based on roles, relationships, or attributes of a person. Abbey automatically enriches data for you.
Usage
  • To use Enriched Data, use the data object in your code with the system.abbey namespace.
For example, given the following data:
{
"system": {
"abbey": {
"identities": {
"snowflake": {
},
"pagerduty": {
"isoncall": true
}
}
}
}
}
You can define an Access Policy that automatically denies access to someone if they're not on-call:
deny[msg] {
data.system.abbey.identities.pagerduty.isoncall == false
}
The data schema is a component of the Abbey OPA Constraint Framework. As you connect more systems to the Abbey Platform, Abbey will automatically pick them up and enrich the data for you to use in your policies.
Schema of data
data.json
{
"system": {
"abbey": {
"identities": {
"abbey":{
"email":...
}
"snowflake": {
...
},
"pagerduty": {
...