Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 26 additions & 38 deletions docs/tutorials/manage-data-masking-with-terraform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ This tutorial is part of the **Bytebase Terraform Provider** series:
- **Configure** data classification levels and categories
- **Create** global masking policies that apply workspace-wide
- **Set up** database-specific column masking
- **Grant** masking exceptions for specific users
- **Grant** masking exemptions for specific users

## Prerequisites

Expand Down Expand Up @@ -146,11 +146,11 @@ resource "bytebase_setting" "classification" {
title = "Classification Example"

levels {
id = "1"
level = 1
title = "Level 1"
}
levels {
id = "2"
level = 2
title = "Level 2"
}

Expand All @@ -162,13 +162,13 @@ resource "bytebase_setting" "classification" {
classifications {
id = "1-1"
title = "User basic"
level = "1"
level = 1
}

classifications {
id = "1-2"
title = "User contact info"
level = "2"
level = 2
}

classifications {
Expand All @@ -179,7 +179,7 @@ resource "bytebase_setting" "classification" {
classifications {
id = "2-1"
title = "Employment info"
level = "2"
level = 2
}
}
}
Expand Down Expand Up @@ -227,29 +227,29 @@ resource "bytebase_policy" "global_masking_policy" {
bytebase_setting.environments
]

parent = "workspaces/-"
# parent defaults to the current workspace when omitted.
type = "MASKING_RULE"
enforce = true
inherit_from_parent = false

global_masking_policy {

rules {
condition = "column_name == \"birth_date\""
condition = "resource.column_name == \"birth_date\""
id = "birth-date-mask"
semantic_type = "date-year-mask"
title = "Mask Birth Date Year"
}

rules {
condition = "column_name == \"last_name\""
condition = "resource.column_name == \"last_name\""
id = "last-name-first-letter-only"
semantic_type = "name-first-letter-only"
title = "Last Name Only Show First Letter"
}

rules {
condition = "classification_level in [\"2\"]"
condition = "resource.classification_level == 2"
id = "classification-level-2"
semantic_type = "full-mask" # Maps Level 2 classification to full-mask semantic type
title = "Full Mask for Classification Level 2"
Expand Down Expand Up @@ -339,50 +339,42 @@ Verify in Bytebase:

![dev1-salary](/content/docs/tutorials/manage-data-masking-with-terraform/bb-dev1-salary.webp)

## Step 6: Grant Masking Exceptions
## Step 6: Grant Masking Exemptions

| | |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Terraform resource | [bytebase_policy](https://registry.terraform.io/providers/bytebase/bytebase/latest/docs/resources/policy) |
| Sample file | [8-5-masking-exception.tf](https://github.com/bytebase/terraform-provider-bytebase/blob/main/tutorials/8-5-masking-exception.tf) |
| Sample file | [8-5-masking-exemption.tf](https://github.com/bytebase/terraform-provider-bytebase/blob/main/tutorials/8-5-masking-exemption.tf) |

Create `8-5-masking-exception.tf` to grant bypass permissions:
Create `8-5-masking-exemption.tf` to grant bypass permissions:

- Workspace Admin (`admin@example.com`) has Masking Exemptions for `birth_date` in table `employee` for Query
- Workspace Admin (`admin@example.com`) has Masking Exemptions for `last_name` in table `employee` for Export
- Workspace Admin (`admin@example.com`) and QA (`qa1@example.com`) can see unmasked `birth_date` and `last_name` in the `employee` table (expires 2027-07-30).
- Developer 1 (`dev1@example.com`) can see unmasked `first_name`, `last_name`, and `gender` in the `employee` table (via `raw_expression`, no expiry).
Comment thread
adela-bytebase marked this conversation as resolved.

```hcl 8-5-masking-exception.tf
resource "bytebase_policy" "masking_exception_policy" {
```hcl 8-5-masking-exemption.tf
resource "bytebase_policy" "masking_exemption_policy" {
depends_on = [
bytebase_project.project-two,
bytebase_instance.prod
]

parent = bytebase_project.project-two.name
Comment thread
adela-bytebase marked this conversation as resolved.
type = "MASKING_EXCEPTION"
type = "MASKING_EXEMPTION"
enforce = true
inherit_from_parent = false

masking_exception_policy {
exceptions {
masking_exemption_policy {
exemptions {
reason = "Business requirement"
database = "instances/prod-sample-instance/databases/hr_prod"
table = "employee"
columns = ["birth_date", "last_name"]
members = ["user:admin@example.com"]
actions = ["QUERY", "EXPORT"]
expire_timestamp = "2027-07-30T16:11:49Z"
}
exceptions {
reason = "Export data for analysis"
members = ["user:qa1@example.com"]
actions = ["EXPORT"]
members = ["user:admin@example.com", "user:qa1@example.com"]
expire_timestamp = "2027-07-30T16:11:49Z"
}
exceptions {
exemptions {
reason = "Grant query access"
members = ["user:dev1@example.com"]
actions = ["QUERY"]
members = ["user:dev1@example.com"]
raw_expression = "resource.instance_id == \"prod-sample-instance\" && resource.database_name == \"hr_prod\" && resource.table_name == \"employee\" && resource.column_name in [\"first_name\", \"last_name\", \"gender\"]"
}
}
Expand All @@ -407,23 +399,19 @@ If you specify `raw_expression`, it defines the exemption condition directly as

## Step 7: Apply Final Configuration and Test

Apply the masking exceptions and test everything:
Apply the masking exemptions and test everything:

```bash
terraform plan
terraform apply
```

Verify the masking exceptions are working:
Verify the masking exemptions are working:

1. Log in as Workspace Admin (`admin@example.com`), then go to **SQL Editor** to access `hr_prod`, double-click `employee` table on the left. You may notice the `birth_date` is not masked any longer.
1. Log in as Workspace Admin (`admin@example.com`), then go to **SQL Editor** to access `hr_prod` and double-click the `employee` table. Both `birth_date` and `last_name` are now unmasked for both query and export.

![admin-employee-query](/content/docs/tutorials/manage-data-masking-with-terraform/bb-admin-employee-query.webp)

1. Click **Export**, and then open the file. You should notice the `birth_date` is still masked while `last_name` is no longer masked.

![admin-employee-export](/content/docs/tutorials/manage-data-masking-with-terraform/bb-admin-employee-export.webp)

1. You may go to **Manage > Masking Exemptions** to view current exemptions. They will expire automatically after the expiration time.

![masking-exceptions](/content/docs/tutorials/manage-data-masking-with-terraform/bb-masking-exceptions.webp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,29 +72,33 @@ Create `7-1-workspace-iam.tf` with workspace-level permissions:
resource "bytebase_iam_policy" "workspace_iam" {
depends_on = [
bytebase_user.workspace_admin,
bytebase_user.tf_service_account,
bytebase_service_account.tf_service_account,
bytebase_workload_identity.github_ci,
bytebase_user.workspace_dba1,
bytebase_user.workspace_dba2,
bytebase_group.qa
]

parent = "workspaces/-"
# parent defaults to the current workspace when omitted.

iam_policy {

binding {
role = "roles/workspaceAdmin"
members = [
format("user:%s", bytebase_user.workspace_admin.email),
Comment thread
adela-bytebase marked this conversation as resolved.
format("user:%s", bytebase_user.tf_service_account.email),
# Keep the Terraform-running service account as Workspace Admin so
# subsequent `terraform apply` runs retain full permissions.
format("serviceAccount:%s", bytebase_service_account.tf_service_account.email),
]
}

binding {
role = "roles/workspaceDBA"
members = [
format("user:%s", bytebase_user.workspace_dba1.email),
format("user:%s", bytebase_user.workspace_dba2.email)
format("user:%s", bytebase_user.workspace_dba2.email),
format("workloadIdentity:%s", bytebase_workload_identity.github_ci.email),
]
}

Expand All @@ -117,6 +121,10 @@ resource "bytebase_iam_policy" "workspace_iam" {
}
```

<Note>
Service accounts use the `serviceAccount:` member prefix, workload identities use `workloadIdentity:`, users use `user:`, and groups use `group:`.
</Note>

<Note>
`allUsers` is a special member representing everyone in the workspace. Without it, users may be
unable to access the workspace.
Expand Down
83 changes: 21 additions & 62 deletions docs/tutorials/manage-environments-with-terraform.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ Navigate to **Environments** in Bytebase. You'll see two default environments: `
required_providers {
bytebase = {
source = "registry.terraform.io/bytebase/bytebase"
version = "3.8.6" # Check for latest version
version = "3.17.1" # Check for latest version
}
}
}
Expand Down Expand Up @@ -198,40 +198,25 @@ Let's add rollout and data protection policies, for more details, see: [Environm
| Terraform resource | [bytebase_policy](https://registry.terraform.io/providers/bytebase/bytebase/latest/docs/resources/policy) |
| Sample file | [1-2-env-policy-rollout.tf](https://github.com/bytebase/terraform-provider-bytebase/blob/main/tutorials/1-2-env-policy-rollout.tf) |

When no rollout policy is found for an environment, Bytebase applies a default rollout policy with the following checkers:

- **Required Issue Approval**: Changes must be approved before deployment
- **Plan Check Enforcement**: SQL plan checks must pass (errors only)

You can explicitly configure these policies using Terraform. Create `1-2-env-policy-rollout.tf`:
The rollout policy controls whether changes deploy automatically and which roles can manually roll out. Create `1-2-env-policy-rollout.tf`:

```hcl 1-2-env-policy-rollout.tf
# Test environment - automatic deployment with default checkers
# Test environment - automatic deployment
resource "bytebase_policy" "rollout_policy_test" {
depends_on = [bytebase_setting.environments]
parent = bytebase_setting.environments.environment_setting[0].environment[0].name
type = "ROLLOUT_POLICY"

rollout_policy {
automatic = true # Deploy changes automatically
automatic = true # Deploy changes automatically when all checks pass
roles = [
"roles/workspaceAdmin",
"roles/projectOwner",
"roles/LAST_APPROVER",
"roles/CREATOR"
"roles/projectOwner"
]

# Default checkers (explicitly configured)
checkers {
required_issue_approval = true
required_status_checks {
plan_check_enforcement = "ERROR_ONLY" # Block on errors only
}
}
}
}

# Production - manual deployment with stricter checks
# Production - manual deployment
resource "bytebase_policy" "rollout_policy_prod" {
depends_on = [bytebase_setting.environments]
parent = bytebase_setting.environments.environment_setting[0].environment[1].name
Expand All @@ -241,31 +226,16 @@ resource "bytebase_policy" "rollout_policy_prod" {
automatic = false # Require manual deployment
roles = [
"roles/workspaceAdmin",
"roles/projectOwner",
"roles/LAST_APPROVER",
"roles/CREATOR"
"roles/projectOwner"
]

# Enforce all plan checks (errors and warnings)
checkers {
required_issue_approval = true
required_status_checks {
plan_check_enforcement = "STRICT" # Block on both errors and warnings
}
}
}
}
```

**Key Configuration Options:**

- `automatic`: When `true`, changes deploy automatically after approval. When `false`, requires manual click to deploy.
- `roles`: List of roles allowed to manually deploy changes. Required even with automatic rollout, as manual approval is needed when checks fail.
- `checkers.required_issue_approval`: When `true`, requires issue approval before rollout.
- `checkers.required_status_checks.plan_check_enforcement`: Controls SQL plan check enforcement:
- `PLAN_CHECK_ENFORCEMENT_UNSPECIFIED`: Allow rollout regardless of plan check results (no enforcement)
- `ERROR_ONLY`: Block rollout only when plan check finds errors (default)
- `STRICT`: Block rollout when plan check finds errors or warnings (stricter for production)
- `automatic`: When `true`, changes deploy automatically after all checks pass. When `false`, requires manual click to deploy.
- `roles`: List of roles allowed to manually roll out changes. Required even with automatic rollout, as manual approval is needed when checks fail.

### Data Protection Policy

Expand All @@ -277,38 +247,27 @@ resource "bytebase_policy" "rollout_policy_prod" {
Create `1-3-env-policy-data.tf`:

```hcl 1-3-env-policy-data.tf
# Prevent copying data in SQL Editor from production
resource "bytebase_policy" "disable_copy_data_policy_prod" {
depends_on = [bytebase_setting.environments]
parent = bytebase_setting.environments.environment_setting[0].environment[1].name
type = "DISABLE_COPY_DATA"

disable_copy_data_policy {
enable = true # Block data copy
}
}

# Restrict SQL execution in SQL Editor from production
resource "bytebase_policy" "data_source_query_policy_prod" {
# Restrict SQL Editor data access on production
resource "bytebase_policy" "query_data_policy_prod" {
depends_on = [bytebase_setting.environments]
parent = bytebase_setting.environments.environment_setting[0].environment[1].name
type = "DATA_SOURCE_QUERY"
type = "DATA_QUERY"

data_source_query_policy {
restriction = "RESTRICTION_UNSPECIFIED"
disallow_ddl = true # No schema changes via SQL Editor
disallow_dml = true # No data changes via SQL Editor
query_data_policy {
maximum_result_rows = 1000 # Cap rows returned per query
disable_copy_data = true # Block copy-to-clipboard
disable_export = true # Block export
allow_admin_data_source = false # Force read-only data source when configured
}
}
```

- Here data protection policy is only applied to the `Prod` environment. Which means in `Test` environment, by default, users may execute DDL and DML statements or copy data directly in SQL Editor.
- The policy is only applied to the `Prod` environment. In `Test`, the defaults apply (no row cap, copy/export allowed, admin data source usable).

- `restriction` controls [access to the data source](/sql-editor/settings/data-source-restriction):
- `allow_admin_data_source` controls [access to the data source](/sql-editor/settings/data-source-restriction):

- `RESTRICTION_UNSPECIFIED`: Admin data source is allowed.
- `DISALLOW`: Admin data source is completely disallowed.
- `FALLBACK`: Prefer the read-only data source; use admin only if read-only is not configured.
- `true`: Admin data source is allowed.
- `false`: When a read-only data source is configured, users are forced onto it; otherwise falls back to admin.

## Step 6 - Apply Configuration

Expand Down
Loading
Loading