diff --git a/VERSION b/VERSION index 0be1fc7..aaaff91 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.8.0 \ No newline at end of file +3.8.1 \ No newline at end of file diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index 0ca5b20..53f79e5 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -17,13 +17,16 @@ The policy data source. ### Required +- `parent` (String) The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name} - `type` (String) The policy type. ### Optional +- `data_source_query_policy` (Block List, Max: 1) Restrict querying admin data sources (see [below for nested schema](#nestedblock--data_source_query_policy)) +- `disable_copy_data_policy` (Block List, Max: 1) Restrict data copying in SQL Editor (Admins/DBAs allowed) (see [below for nested schema](#nestedblock--disable_copy_data_policy)) - `global_masking_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--global_masking_policy)) - `masking_exception_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_exception_policy)) -- `parent` (String) The policy parent name for the policy, support projects/{resource id}, environments/{resource id}, instances/{resource id}, or instances/{resource id}/databases/{database name} +- `rollout_policy` (Block List, Max: 1) Control issue rollout. Learn more: https://docs.bytebase.com/administration/environment-policy/rollout-policy (see [below for nested schema](#nestedblock--rollout_policy)) ### Read-Only @@ -32,6 +35,24 @@ The policy data source. - `inherit_from_parent` (Boolean) Decide if the policy should inherit from the parent. - `name` (String) The policy full name + +### Nested Schema for `data_source_query_policy` + +Optional: + +- `disallow_ddl` (Boolean) Disallow running DDL statements in the SQL editor. +- `disallow_dml` (Boolean) Disallow running DML statements in the SQL editor. +- `restriction` (String) RESTRICTION_UNSPECIFIED means no restriction; FALLBACK will allows to query admin data sources when there is no read-only data source; DISALLOW will always disallow to query admin data sources. + + + +### Nested Schema for `disable_copy_data_policy` + +Required: + +- `enable` (Boolean) Restrict data copying + + ### Nested Schema for `global_masking_policy` @@ -74,3 +95,13 @@ Optional: - `table` (String) + + +### Nested Schema for `rollout_policy` + +Optional: + +- `automatic` (Boolean) If all check pass, the change will be rolled out and executed automatically. +- `roles` (Set of String) If any roles are specified, Bytebase requires users with those roles to manually roll out the change. + + diff --git a/docs/data-sources/setting.md b/docs/data-sources/setting.md index 3bc8fdb..fdf13ba 100644 --- a/docs/data-sources/setting.md +++ b/docs/data-sources/setting.md @@ -22,7 +22,7 @@ The setting data source. ### Optional - `classification` (Block List, Max: 1) Classification for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--classification)) -- `semantic_types` (Block Set) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) +- `semantic_types` (Block List) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) ### Read-Only @@ -38,7 +38,7 @@ Required: - `classifications` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) - `id` (String) The classification unique uuid. -- `levels` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) +- `levels` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) - `title` (String) The classification title. Optional. Optional: diff --git a/docs/resources/environment.md b/docs/resources/environment.md index 1350776..5e76198 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -17,13 +17,13 @@ The environment resource. ### Required -- `order` (Number) The environment sorting order. - `resource_id` (String) The environment unique id. - `title` (String) The environment display name. ### Optional - `color` (String) The environment color. +- `order` (Number) The environment sorting order. - `protected` (Boolean) The environment is protected or not. ### Read-Only diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 7e57836..d57c986 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -22,16 +22,37 @@ The policy resource. ### Optional +- `data_source_query_policy` (Block List, Max: 1) Restrict querying admin data sources (see [below for nested schema](#nestedblock--data_source_query_policy)) +- `disable_copy_data_policy` (Block List, Max: 1) Restrict data copying in SQL Editor (Admins/DBAs allowed) (see [below for nested schema](#nestedblock--disable_copy_data_policy)) - `enforce` (Boolean) Decide if the policy is enforced. - `global_masking_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--global_masking_policy)) - `inherit_from_parent` (Boolean) Decide if the policy should inherit from the parent. - `masking_exception_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--masking_exception_policy)) +- `rollout_policy` (Block List, Max: 1) Control issue rollout. Learn more: https://docs.bytebase.com/administration/environment-policy/rollout-policy (see [below for nested schema](#nestedblock--rollout_policy)) ### Read-Only - `id` (String) The ID of this resource. - `name` (String) The policy full name + +### Nested Schema for `data_source_query_policy` + +Optional: + +- `disallow_ddl` (Boolean) Disallow running DDL statements in the SQL editor. +- `disallow_dml` (Boolean) Disallow running DML statements in the SQL editor. +- `restriction` (String) RESTRICTION_UNSPECIFIED means no restriction; FALLBACK will allows to query admin data sources when there is no read-only data source; DISALLOW will always disallow to query admin data sources. + + + +### Nested Schema for `disable_copy_data_policy` + +Required: + +- `enable` (Boolean) Restrict data copying + + ### Nested Schema for `global_masking_policy` @@ -74,3 +95,13 @@ Optional: - `table` (String) + + +### Nested Schema for `rollout_policy` + +Optional: + +- `automatic` (Boolean) If all check pass, the change will be rolled out and executed automatically. +- `roles` (Set of String) If any roles are specified, Bytebase requires users with those roles to manually roll out the change. + + diff --git a/docs/resources/setting.md b/docs/resources/setting.md index 4c9acc4..355791b 100644 --- a/docs/resources/setting.md +++ b/docs/resources/setting.md @@ -24,7 +24,7 @@ The setting resource. - `approval_flow` (Block List) Configure risk level and approval flow for different tasks. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--approval_flow)) - `classification` (Block List, Max: 1) Classification for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--classification)) - `environment_setting` (Block List) The environment (see [below for nested schema](#nestedblock--environment_setting)) -- `semantic_types` (Block Set) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) +- `semantic_types` (Block List) Semantic types for data masking. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--semantic_types)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) ### Read-Only @@ -88,7 +88,7 @@ Required: - `classifications` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--classifications)) - `id` (String) The classification unique uuid. -- `levels` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) +- `levels` (Block List, Min: 1) (see [below for nested schema](#nestedblock--classification--levels)) - `title` (String) The classification title. Optional. Optional: diff --git a/examples/database/main.tf b/examples/database/main.tf index e7ab2c4..2b0e12f 100644 --- a/examples/database/main.tf +++ b/examples/database/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/database_group/main.tf b/examples/database_group/main.tf index c1719e5..1c0a35f 100644 --- a/examples/database_group/main.tf +++ b/examples/database_group/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/environments/main.tf b/examples/environments/main.tf index dea81bb..1d9dfb7 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/groups/main.tf b/examples/groups/main.tf index 741d0b2..e65cbe9 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/iamPolicy/main.tf b/examples/iamPolicy/main.tf index d4ec9e2..5b6dac1 100644 --- a/examples/iamPolicy/main.tf +++ b/examples/iamPolicy/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/instances/main.tf b/examples/instances/main.tf index 18417f7..735e5e5 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/policies/main.tf b/examples/policies/main.tf index e9adc04..1ae87d0 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/projects/main.tf b/examples/projects/main.tf index 24b539e..681c99a 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/risk/main.tf b/examples/risk/main.tf index 645207d..b892277 100644 --- a/examples/risk/main.tf +++ b/examples/risk/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/roles/main.tf b/examples/roles/main.tf index 34fd4f7..06c42ba 100644 --- a/examples/roles/main.tf +++ b/examples/roles/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/settings/main.tf b/examples/settings/main.tf index 20f9f80..a089537 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/environment.tf b/examples/setup/environment.tf index 4242f12..28c141f 100644 --- a/examples/setup/environment.tf +++ b/examples/setup/environment.tf @@ -37,3 +37,41 @@ resource "bytebase_environment" "prod" { order = 1 // change order to 1 protected = true } + +resource "bytebase_policy" "rollout_policy" { + depends_on = [bytebase_environment.test] + parent = bytebase_environment.test.name + type = "ROLLOUT_POLICY" + + rollout_policy { + automatic = true + roles = [ + "roles/workspaceAdmin", + "roles/projectOwner", + "roles/LAST_APPROVER", + "roles/CREATOR" + ] + } +} + +resource "bytebase_policy" "disable_copy_data_policy" { + depends_on = [bytebase_environment.test] + parent = bytebase_environment.test.name + type = "DISABLE_COPY_DATA" + + disable_copy_data_policy { + enable = true + } +} + +resource "bytebase_policy" "data_source_query_policy" { + depends_on = [bytebase_environment.test] + parent = bytebase_environment.test.name + type = "DATA_SOURCE_QUERY" + + data_source_query_policy { + restriction = "FALLBACK" + disallow_ddl = false + disallow_dml = false + } +} diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 88c7059..5f4cfba 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/sql_review.tf b/examples/setup/sql_review.tf index da1cecb..587f558 100644 --- a/examples/setup/sql_review.tf +++ b/examples/setup/sql_review.tf @@ -1,6 +1,7 @@ resource "bytebase_review_config" "sample" { depends_on = [ - bytebase_setting.environments + bytebase_setting.environments, + bytebase_project.sample_project ] resource_id = "review-config-sample" @@ -8,7 +9,8 @@ resource "bytebase_review_config" "sample" { enabled = true resources = toset([ bytebase_setting.environments.environment_setting[0].environment[0].name, - bytebase_setting.environments.environment_setting[0].environment[1].name + bytebase_setting.environments.environment_setting[0].environment[1].name, + bytebase_project.sample_project.name ]) rules { type = "column.no-null" diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf index 05ad7c8..4b38687 100644 --- a/examples/sql_review/main.tf +++ b/examples/sql_review/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.7.2" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_iam_policy.go b/provider/data_source_iam_policy.go index 6c04105..f29e56d 100644 --- a/provider/data_source_iam_policy.go +++ b/provider/data_source_iam_policy.go @@ -67,6 +67,9 @@ func getIAMBindingSchema(computed bool) *schema.Schema { Computed: computed, Optional: !computed, Description: "The role full name in roles/{id} format.", + ValidateDiagFunc: internal.ResourceNameValidation( + fmt.Sprintf("^%s", internal.RoleNamePrefix), + ), }, "members": { Type: schema.TypeSet, @@ -75,6 +78,11 @@ func getIAMBindingSchema(computed bool) *schema.Schema { Description: `A set of memebers. The value can be "allUsers", "user:{email}" or "group:{email}".`, Elem: &schema.Schema{ Type: schema.TypeString, + ValidateDiagFunc: internal.ResourceNameValidation( + "allUsers", + "^user:", + "^group:", + ), }, }, "condition": { @@ -242,6 +250,12 @@ func bindingHash(rawBinding interface{}) int { _, _ = buf.WriteString(conditionHash(rawCondition)) } + if members, ok := binding["members"].(*schema.Set); ok && members.Len() > 0 { + for _, member := range members.List() { + _, _ = buf.WriteString(fmt.Sprintf("[member] %s", member)) + } + } + return internal.ToHashcodeInt(buf.String()) } diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index 80786d3..254635c 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -24,8 +24,7 @@ func dataSourcePolicy() *schema.Resource { Schema: map[string]*schema.Schema{ "parent": { Type: schema.TypeString, - Optional: true, - Default: "", + Required: true, ValidateDiagFunc: internal.ResourceNameValidation( // workspace policy fmt.Sprintf("^%s$", internal.WorkspaceName), @@ -46,6 +45,9 @@ func dataSourcePolicy() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{ v1pb.PolicyType_MASKING_EXCEPTION.String(), v1pb.PolicyType_MASKING_RULE.String(), + v1pb.PolicyType_DISABLE_COPY_DATA.String(), + v1pb.PolicyType_DATA_SOURCE_QUERY.String(), + v1pb.PolicyType_ROLLOUT_POLICY.String(), }, false), Description: "The policy type.", }, @@ -66,6 +68,9 @@ func dataSourcePolicy() *schema.Resource { }, "masking_exception_policy": getMaskingExceptionPolicySchema(true), "global_masking_policy": getGlobalMaskingPolicySchema(true), + "disable_copy_data_policy": getDisableCopyDataPolicySchema(true), + "data_source_query_policy": getDataSourceQueryPolicySchema(true), + "rollout_policy": getRolloutPolicySchema(true), }, } } @@ -184,6 +189,104 @@ func getGlobalMaskingPolicySchema(computed bool) *schema.Schema { } } +func getDisableCopyDataPolicySchema(computed bool) *schema.Schema { + return &schema.Schema{ + Computed: computed, + Optional: true, + Default: nil, + Type: schema.TypeList, + MinItems: 0, + MaxItems: 1, + Description: "Restrict data copying in SQL Editor (Admins/DBAs allowed)", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enable": { + Type: schema.TypeBool, + Required: true, + Description: "Restrict data copying", + }, + }, + }, + } +} + +func getDataSourceQueryPolicySchema(computed bool) *schema.Schema { + return &schema.Schema{ + Computed: computed, + Optional: true, + Default: nil, + Type: schema.TypeList, + MinItems: 0, + MaxItems: 1, + Description: "Restrict querying admin data sources", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "restriction": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + v1pb.DataSourceQueryPolicy_FALLBACK.String(), + v1pb.DataSourceQueryPolicy_DISALLOW.String(), + v1pb.DataSourceQueryPolicy_RESTRICTION_UNSPECIFIED.String(), + }, false), + Description: "RESTRICTION_UNSPECIFIED means no restriction; FALLBACK will allows to query admin data sources when there is no read-only data source; DISALLOW will always disallow to query admin data sources.", + }, + "disallow_ddl": { + Type: schema.TypeBool, + Optional: true, + Description: "Disallow running DDL statements in the SQL editor.", + }, + "disallow_dml": { + Type: schema.TypeBool, + Optional: true, + Description: "Disallow running DML statements in the SQL editor.", + }, + }, + }, + } +} + +const ( + issueLastApproverRole = "roles/LAST_APPROVER" + issueCreatorRole = "roles/CREATOR" +) + +func getRolloutPolicySchema(computed bool) *schema.Schema { + return &schema.Schema{ + Computed: computed, + Optional: true, + Default: nil, + Type: schema.TypeList, + MinItems: 0, + MaxItems: 1, + Description: "Control issue rollout. Learn more: https://docs.bytebase.com/administration/environment-policy/rollout-policy", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "automatic": { + Type: schema.TypeBool, + Optional: true, + Description: "If all check pass, the change will be rolled out and executed automatically.", + }, + "roles": { + Optional: true, + Type: schema.TypeSet, + MinItems: 0, + Description: "If any roles are specified, Bytebase requires users with those roles to manually roll out the change.", + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: fmt.Sprintf(`Role full name in roles/{id} format. You can also use the "%s" for the last approver of the issue, or "%s" for the creator of the issue.`, issueLastApproverRole, issueCreatorRole), + ValidateDiagFunc: internal.ResourceNameValidation( + fmt.Sprintf("^%s$", issueLastApproverRole), + fmt.Sprintf("^%s$", issueCreatorRole), + fmt.Sprintf("^%s", internal.RoleNamePrefix), + ), + }, + }, + }, + }, + } +} + func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) @@ -202,16 +305,13 @@ func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interfa } func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnostics { - parent, _, err := internal.GetPolicyParentAndType(policy.Name) + _, policyType, err := internal.GetPolicyParentAndType(policy.Name) if err != nil { return diag.Errorf("cannot parse name for policy: %s", err.Error()) } if err := d.Set("name", policy.Name); err != nil { return diag.Errorf("cannot set name for policy: %s", err.Error()) } - if err := d.Set("parent", parent); err != nil { - return diag.Errorf("cannot set parent for policy: %s", err.Error()) - } if err := d.Set("inherit_from_parent", policy.InheritFromParent); err != nil { return diag.Errorf("cannot set inherit_from_parent for policy: %s", err.Error()) } @@ -219,29 +319,80 @@ func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnost return diag.Errorf("cannot set enforce for policy: %s", err.Error()) } - if p := policy.GetMaskingExceptionPolicy(); p != nil { - exceptionPolicy, err := flattenMaskingExceptionPolicy(p) - if err != nil { - return diag.FromErr(err) + switch policyType { + case v1pb.PolicyType_MASKING_EXCEPTION: + if p := policy.GetMaskingExceptionPolicy(); p != nil { + exceptionPolicy, err := flattenMaskingExceptionPolicy(p) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("masking_exception_policy", exceptionPolicy); err != nil { + return diag.Errorf("cannot set masking_exception_policy: %s", err.Error()) + } } - if err := d.Set("masking_exception_policy", exceptionPolicy); err != nil { - return diag.Errorf("cannot set masking_exception_policy: %s", err.Error()) + case v1pb.PolicyType_MASKING_RULE: + if p := policy.GetMaskingRulePolicy(); p != nil { + maskingPolicy, err := flattenGlobalMaskingPolicy(p) + if err != nil { + return diag.FromErr(err) + } + if err := d.Set("global_masking_policy", maskingPolicy); err != nil { + return diag.Errorf("cannot set global_masking_policy: %s", err.Error()) + } } - } - - if p := policy.GetMaskingRulePolicy(); p != nil { - maskingPolicy, err := flattenGlobalMaskingPolicy(p) - if err != nil { - return diag.FromErr(err) + case v1pb.PolicyType_DISABLE_COPY_DATA: + if p := policy.GetDisableCopyDataPolicy(); p != nil { + disableCopyDataPolicy := flattenDisableCopyDataPolicy(p) + if err := d.Set("disable_copy_data_policy", disableCopyDataPolicy); err != nil { + return diag.Errorf("cannot set disable_copy_data_policy: %s", err.Error()) + } } - if err := d.Set("global_masking_policy", maskingPolicy); err != nil { - return diag.Errorf("cannot set global_masking_policy: %s", err.Error()) + case v1pb.PolicyType_DATA_SOURCE_QUERY: + if p := policy.GetDataSourceQueryPolicy(); p != nil { + dataSourceQueryPolicy := flattenDataSourceQueryPolicy(p) + if err := d.Set("data_source_query_policy", dataSourceQueryPolicy); err != nil { + return diag.Errorf("cannot set data_source_query_policy: %s", err.Error()) + } + } + case v1pb.PolicyType_ROLLOUT_POLICY: + if p := policy.GetRolloutPolicy(); p != nil { + rolloutPolicy := flattenRolloutPolicy(p) + if err := d.Set("rollout_policy", rolloutPolicy); err != nil { + return diag.Errorf("cannot set rollout_policy: %s", err.Error()) + } } } return nil } +func flattenRolloutPolicy(p *v1pb.RolloutPolicy) []interface{} { + roles := []string{} + roles = append(roles, p.Roles...) + roles = append(roles, p.IssueRoles...) + policy := map[string]interface{}{ + "automatic": p.Automatic, + "roles": roles, + } + return []interface{}{policy} +} + +func flattenDataSourceQueryPolicy(p *v1pb.DataSourceQueryPolicy) []interface{} { + policy := map[string]interface{}{ + "restriction": p.AdminDataSourceRestriction.String(), + "disallow_ddl": p.DisallowDdl, + "disallow_dml": p.DisallowDml, + } + return []interface{}{policy} +} + +func flattenDisableCopyDataPolicy(p *v1pb.DisableCopyDataPolicy) []interface{} { + policy := map[string]interface{}{ + "enable": p.Active, + } + return []interface{}{policy} +} + func flattenGlobalMaskingPolicy(p *v1pb.MaskingRulePolicy) ([]interface{}, error) { ruleList := []interface{}{} diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index e20a0b3..3433026 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -47,7 +47,7 @@ func getSemanticTypesSetting(computed bool) *schema.Schema { Computed: computed, Optional: true, Default: nil, - Type: schema.TypeSet, + Type: schema.TypeList, Description: "Semantic types for data masking. Require ENTERPRISE subscription.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -184,7 +184,6 @@ func getSemanticTypesSetting(computed bool) *schema.Schema { }, }, }, - Set: itemIDHash, } } @@ -217,7 +216,7 @@ func getClassificationSetting(computed bool) *schema.Schema { }, "levels": { Required: true, - Type: schema.TypeSet, + Type: schema.TypeList, MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -239,7 +238,6 @@ func getClassificationSetting(computed bool) *schema.Schema { }, }, }, - Set: itemIDHash, }, "classifications": { Required: true, @@ -271,7 +269,6 @@ func getClassificationSetting(computed bool) *schema.Schema { }, }, }, - Set: itemIDHash, }, }, }, @@ -507,7 +504,7 @@ func setSettingMessage(ctx context.Context, d *schema.ResourceData, client api.C } if value := setting.GetValue().GetSemanticTypeSettingValue(); value != nil { settingVal := flattenSemanticTypesSetting(value) - if err := d.Set("semantic_types", schema.NewSet(itemIDHash, settingVal)); err != nil { + if err := d.Set("semantic_types", settingVal); err != nil { return diag.Errorf("cannot set semantic_types: %s", err.Error()) } } @@ -678,7 +675,7 @@ func flattenClassificationSetting(setting *v1pb.DataClassificationSetting) []int rawLevel["description"] = level.Description rawLevels = append(rawLevels, rawLevel) } - raw["levels"] = schema.NewSet(itemIDHash, rawLevels) + raw["levels"] = rawLevels rawClassifications := []interface{}{} for _, classification := range config.GetClassification() { @@ -689,7 +686,7 @@ func flattenClassificationSetting(setting *v1pb.DataClassificationSetting) []int rawClassification["level"] = classification.LevelId rawClassifications = append(rawClassifications, rawClassification) } - raw["classifications"] = schema.NewSet(itemIDHash, rawClassifications) + raw["classifications"] = rawClassifications } return []interface{}{raw} @@ -769,8 +766,3 @@ func flattenSemanticTypesSetting(setting *v1pb.SemanticTypeSetting) []interface{ return raw } - -func itemIDHash(rawItem interface{}) int { - item := rawItem.(map[string]interface{}) - return internal.ToHashcodeInt(item["id"].(string)) -} diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 21ed0a5..559cd1f 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -52,6 +52,9 @@ func resourcePolicy() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{ v1pb.PolicyType_MASKING_EXCEPTION.String(), v1pb.PolicyType_MASKING_RULE.String(), + v1pb.PolicyType_DISABLE_COPY_DATA.String(), + v1pb.PolicyType_DATA_SOURCE_QUERY.String(), + v1pb.PolicyType_ROLLOUT_POLICY.String(), }, false), Description: "The policy type.", }, @@ -74,23 +77,22 @@ func resourcePolicy() *schema.Resource { }, "masking_exception_policy": getMaskingExceptionPolicySchema(false), "global_masking_policy": getGlobalMaskingPolicySchema(false), + "disable_copy_data_policy": getDisableCopyDataPolicySchema(false), + "data_source_query_policy": getDataSourceQueryPolicySchema(false), + "rollout_policy": getRolloutPolicySchema(false), }, } } func resourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) + policyName := d.Id() - policyName := fmt.Sprintf("%s/%s%s", d.Get("parent").(string), internal.PolicyNamePrefix, d.Get("type").(string)) - if strings.HasPrefix(policyName, internal.WorkspaceName) { - policyName = strings.TrimPrefix(policyName, fmt.Sprintf("%s/", internal.WorkspaceName)) - } policy, err := c.GetPolicy(ctx, policyName) if err != nil { return diag.FromErr(err) } - d.SetId(policy.Name) return setPolicyMessage(d, policy) } @@ -147,6 +149,39 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa patch.Policy = &v1pb.Policy_MaskingRulePolicy{ MaskingRulePolicy: maskingRulePolicy, } + case v1pb.PolicyType_DISABLE_COPY_DATA: + if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) && !strings.HasPrefix(policyName, internal.ProjectNamePrefix) { + return diag.Errorf("policy %v only support environment or project resource", policyName) + } + disableCopyDataPolicy, err := convertToDisableCopyDataPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_DisableCopyDataPolicy{ + DisableCopyDataPolicy: disableCopyDataPolicy, + } + case v1pb.PolicyType_DATA_SOURCE_QUERY: + if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) && !strings.HasPrefix(policyName, internal.ProjectNamePrefix) { + return diag.Errorf("policy %v only support environment or project resource", policyName) + } + dataSourceQueryPolicy, err := convertToDataSourceQueryPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_DataSourceQueryPolicy{ + DataSourceQueryPolicy: dataSourceQueryPolicy, + } + case v1pb.PolicyType_ROLLOUT_POLICY: + if !strings.HasPrefix(policyName, internal.EnvironmentNamePrefix) { + return diag.Errorf("policy %v only support environment resource", policyName) + } + rolloutPolicy, err := convertToRolloutPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_RolloutPolicy{ + RolloutPolicy: rolloutPolicy, + } default: return diag.Errorf("unsupport policy type: %v", policyName) } @@ -175,11 +210,16 @@ func resourcePolicyCreate(ctx context.Context, d *schema.ResourceData, m interfa func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) policyName := d.Id() + _, policyType, err := internal.GetPolicyParentAndType(policyName) if err != nil { return diag.FromErr(err) } + if d.HasChange("type") || d.HasChange("parent") { + return diag.Errorf("cannot change policy type or parent") + } + patch := &v1pb.Policy{ Name: policyName, InheritFromParent: d.Get("inherit_from_parent").(bool), @@ -215,6 +255,26 @@ func resourcePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interfa MaskingRulePolicy: maskingRulePolicy, } } + if d.HasChange("disable_copy_data_policy") { + updateMasks = append(updateMasks, "payload") + disableCopyDataPolicy, err := convertToDisableCopyDataPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_DisableCopyDataPolicy{ + DisableCopyDataPolicy: disableCopyDataPolicy, + } + } + if d.HasChange("data_source_query_policy") { + updateMasks = append(updateMasks, "payload") + dataSourceQueryPolicy, err := convertToDataSourceQueryPolicy(d) + if err != nil { + return diag.FromErr(err) + } + patch.Policy = &v1pb.Policy_DataSourceQueryPolicy{ + DataSourceQueryPolicy: dataSourceQueryPolicy, + } + } var diags diag.Diagnostics if len(updateMasks) > 0 { @@ -326,3 +386,59 @@ func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExcep } return policy, nil } + +func convertToRolloutPolicy(d *schema.ResourceData) (*v1pb.RolloutPolicy, error) { + rawList, ok := d.Get("rollout_policy").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid rollout_policy") + } + + raw := rawList[0].(map[string]interface{}) + policy := &v1pb.RolloutPolicy{ + Automatic: raw["automatic"].(bool), + } + + roles, ok := raw["roles"].(*schema.Set) + if !ok { + return policy, nil + } + + for _, rawRole := range roles.List() { + role := rawRole.(string) + if role == issueLastApproverRole || role == issueCreatorRole { + policy.IssueRoles = append(policy.IssueRoles, role) + } else { + policy.Roles = append(policy.Roles, role) + } + } + + return policy, nil +} + +func convertToDisableCopyDataPolicy(d *schema.ResourceData) (*v1pb.DisableCopyDataPolicy, error) { + rawList, ok := d.Get("disable_copy_data_policy").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid disable_copy_data_policy") + } + + raw := rawList[0].(map[string]interface{}) + return &v1pb.DisableCopyDataPolicy{ + Active: raw["enable"].(bool), + }, nil +} + +func convertToDataSourceQueryPolicy(d *schema.ResourceData) (*v1pb.DataSourceQueryPolicy, error) { + rawList, ok := d.Get("data_source_query_policy").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid data_source_query_policy") + } + + raw := rawList[0].(map[string]interface{}) + return &v1pb.DataSourceQueryPolicy{ + AdminDataSourceRestriction: v1pb.DataSourceQueryPolicy_Restriction( + v1pb.DataSourceQueryPolicy_Restriction_value[raw["restriction"].(string)], + ), + DisallowDdl: raw["disallow_ddl"].(bool), + DisallowDml: raw["disallow_dml"].(bool), + }, nil +} diff --git a/provider/resource_review_config.go b/provider/resource_review_config.go index 5179f36..bdc6d69 100644 --- a/provider/resource_review_config.go +++ b/provider/resource_review_config.go @@ -7,6 +7,7 @@ import ( "strings" v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -115,10 +116,12 @@ func resourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m int func resourceReviewConfigDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { c := m.(api.Client) fullName := d.Id() + resources := getReviewConfigRelatedResources(d) // Warning or errors can be collected in a slice type var diags diag.Diagnostics + removeReviewConfigTag(ctx, c, resources) if err := c.DeleteReviewConfig(ctx, fullName); err != nil { return diag.FromErr(err) } @@ -135,8 +138,18 @@ func resourceReviewConfigUpsert(ctx context.Context, d *schema.ResourceData, m i reviewID := d.Get("resource_id").(string) reviewName := fmt.Sprintf("%s%s", internal.ReviewConfigNamePrefix, reviewID) - if existedName != "" && existedName != reviewName { - return diag.Errorf("cannot change the resource id") + oldAttachedResources := []string{} + if existedName != "" { + if existedName != reviewName { + return diag.Errorf("cannot change the resource id") + } + + existedReview, err := c.GetReviewConfig(ctx, existedName) + if err != nil { + tflog.Debug(ctx, fmt.Sprintf("get review config %s failed with error: %v", existedName, err)) + } else if existedReview != nil { + oldAttachedResources = existedReview.Resources + } } rules, err := convertToV1RuleList(d) @@ -159,12 +172,34 @@ func resourceReviewConfigUpsert(ctx context.Context, d *schema.ResourceData, m i return diag.FromErr(err) } + removeReviewConfigTag(ctx, c, oldAttachedResources) patchTagPolicy(ctx, c, d, review.Name) d.SetId(review.Name) return resourceReviewConfigRead(ctx, d, m) } +func getReviewConfigRelatedResources(d *schema.ResourceData) []string { + resources := []string{} + rawSet, ok := d.Get("resources").(*schema.Set) + if !ok || rawSet.Len() == 0 { + return resources + } + for _, raw := range rawSet.List() { + resources = append(resources, raw.(string)) + } + return resources +} + +func removeReviewConfigTag(ctx context.Context, client api.Client, resources []string) { + for _, resource := range resources { + policyName := fmt.Sprintf("%s/%s%s", resource, internal.PolicyNamePrefix, v1pb.PolicyType_TAG.String()) + if err := client.DeletePolicy(ctx, policyName); err != nil { + tflog.Error(ctx, fmt.Sprintf("failed to delete %v policy with error: %v", policyName, err.Error())) + } + } +} + func patchTagPolicy(ctx context.Context, client api.Client, d *schema.ResourceData, reviewName string) diag.Diagnostics { rawSet, ok := d.Get("resources").(*schema.Set) if !ok || rawSet.Len() == 0 { diff --git a/provider/resource_setting.go b/provider/resource_setting.go index b10d2c9..e5b470d 100644 --- a/provider/resource_setting.go +++ b/provider/resource_setting.go @@ -192,11 +192,11 @@ func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassif return nil, errors.Errorf("id is required for classification config") } - rawLevels := raw["levels"].(*schema.Set) + rawLevels := raw["levels"].([]interface{}) if !ok { return nil, errors.Errorf("levels is required for classification config") } - for _, level := range rawLevels.List() { + for _, level := range rawLevels { rawLevel := level.(map[string]interface{}) classificationLevel := &v1pb.DataClassificationSetting_DataClassificationConfig_Level{ Id: rawLevel["id"].(string), @@ -212,11 +212,11 @@ func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassif dataClassificationConfig.Levels = append(dataClassificationConfig.Levels, classificationLevel) } - rawClassificationss := raw["classifications"].(*schema.Set) + rawClassificationss := raw["classifications"].([]interface{}) if !ok { return nil, errors.Errorf("classifications is required for classification config") } - for _, classification := range rawClassificationss.List() { + for _, classification := range rawClassificationss { rawClassification := classification.(map[string]interface{}) classificationData := &v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification{ Id: rawClassification["id"].(string), @@ -347,13 +347,13 @@ func convertToV1EnvironmentSetting(d *schema.ResourceData) (*v1pb.EnvironmentSet } func convertToV1SemanticTypeSetting(d *schema.ResourceData) (*v1pb.SemanticTypeSetting, error) { - set, ok := d.Get("semantic_types").(*schema.Set) + set, ok := d.Get("semantic_types").([]interface{}) if !ok { return nil, errors.Errorf("invalid semantic_types") } setting := &v1pb.SemanticTypeSetting{} - for _, s := range set.List() { + for _, s := range set { rawSemanticType := s.(map[string]interface{}) semanticType := &v1pb.SemanticTypeSetting_SemanticType{ Id: rawSemanticType["id"].(string), diff --git a/tutorials/0-provider.tf b/tutorials/0-provider.tf index 488cd8f..f17221d 100644 --- a/tutorials/0-provider.tf +++ b/tutorials/0-provider.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.8.0" + version = "3.8.1" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -11,5 +11,5 @@ terraform { provider "bytebase" { service_account = "tf@service.bytebase.com" service_key = "bbs_xxxx" - url = "https://xxx.xxx.xxx" -} \ No newline at end of file + url = "https://xxx.xxx.xxx" +}