diff --git a/VERSION b/VERSION index 33f465d..4764627 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.9.1 \ No newline at end of file +3.9.2 \ No newline at end of file diff --git a/docs/data-sources/policy.md b/docs/data-sources/policy.md index 113cba2..b2064bc 100644 --- a/docs/data-sources/policy.md +++ b/docs/data-sources/policy.md @@ -87,14 +87,15 @@ Optional: Required: -- `action` (String) -- `member` (String) The member in user:{email} or group:{email} format. +- `actions` (Set of String) +- `members` (Set of String) Optional: -- `column` (String) +- `columns` (Set of String) - `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format - `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format +- `raw_expression` (String) The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression. - `reason` (String) The reason for the masking exemption - `schema` (String) - `table` (String) diff --git a/docs/data-sources/policy_list.md b/docs/data-sources/policy_list.md index f66e19e..2d39b8e 100644 --- a/docs/data-sources/policy_list.md +++ b/docs/data-sources/policy_list.md @@ -29,13 +29,34 @@ The policy data source list. Read-Only: +- `data_source_query_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--data_source_query_policy)) +- `disable_copy_data_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--disable_copy_data_policy)) - `enforce` (Boolean) - `global_masking_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--global_masking_policy)) - `inherit_from_parent` (Boolean) - `masking_exception_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--masking_exception_policy)) - `name` (String) +- `rollout_policy` (List of Object) (see [below for nested schema](#nestedobjatt--policies--rollout_policy)) - `type` (String) + +### Nested Schema for `policies.data_source_query_policy` + +Read-Only: + +- `disallow_ddl` (Boolean) +- `disallow_dml` (Boolean) +- `restriction` (String) + + + +### Nested Schema for `policies.disable_copy_data_policy` + +Read-Only: + +- `enable` (Boolean) + + ### Nested Schema for `policies.global_masking_policy` @@ -67,13 +88,24 @@ Read-Only: Read-Only: -- `action` (String) -- `column` (String) +- `actions` (Set of String) +- `columns` (Set of String) - `database` (String) - `expire_timestamp` (String) -- `member` (String) +- `members` (Set of String) +- `raw_expression` (String) - `reason` (String) - `schema` (String) - `table` (String) + + +### Nested Schema for `policies.rollout_policy` + +Read-Only: + +- `automatic` (Boolean) +- `roles` (Set of String) + + diff --git a/docs/resources/policy.md b/docs/resources/policy.md index 4d5d1a7..cc20d1a 100644 --- a/docs/resources/policy.md +++ b/docs/resources/policy.md @@ -87,14 +87,15 @@ Optional: Required: -- `action` (String) -- `member` (String) The member in user:{email} or group:{email} format. +- `actions` (Set of String) +- `members` (Set of String) Optional: -- `column` (String) +- `columns` (Set of String) - `database` (String) The database full name in instances/{instance resource id}/databases/{database name} format - `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format +- `raw_expression` (String) The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression. - `reason` (String) The reason for the masking exemption - `schema` (String) - `table` (String) diff --git a/examples/database/main.tf b/examples/database/main.tf index 4e70469..e9a738a 100644 --- a/examples/database/main.tf +++ b/examples/database/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 1d20a56..de8d008 100644 --- a/examples/database_group/main.tf +++ b/examples/database_group/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 dac313c..15bf112 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 059fcac..32981d4 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -30,7 +30,7 @@ data "bytebase_project" "sample_project" { data "bytebase_group_list" "groups_in_project" { project = data.bytebase_project.sample_project.name - query = "Bytebase" + query = "Bytebase" } output "groups_in_project" { diff --git a/examples/iamPolicy/main.tf b/examples/iamPolicy/main.tf index 9fe20ec..67b6c50 100644 --- a/examples/iamPolicy/main.tf +++ b/examples/iamPolicy/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 ce241f8..aaaa9c4 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 a0b8dd8..39fbfdd 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 d534bfa..0c735f7 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 0c69e90..054dbd9 100644 --- a/examples/risk/main.tf +++ b/examples/risk/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 2b7f9cc..6eaf391 100644 --- a/examples/roles/main.tf +++ b/examples/roles/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # 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 b6d7e09..544b3ec 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/data_masking.tf b/examples/setup/data_masking.tf index 416ed4b..ce94e7a 100644 --- a/examples/setup/data_masking.tf +++ b/examples/setup/data_masking.tf @@ -100,7 +100,9 @@ resource "bytebase_setting" "semantic_types" { resource "bytebase_policy" "masking_exception_policy" { depends_on = [ bytebase_project.sample_project, - bytebase_instance.test + bytebase_instance.test, + bytebase_user.project_developer, + bytebase_user.workspace_dba ] parent = bytebase_project.sample_project.name @@ -112,17 +114,33 @@ resource "bytebase_policy" "masking_exception_policy" { exceptions { database = "instances/test-sample-instance/databases/employee" table = "salary" - column = "amount" - member = "user:ed@bytebase.com" - action = "EXPORT" - reason = "Grant access to ed for export" + columns = ["amount", "emp_no"] + members = [ + format("user:%s", bytebase_user.project_developer.email), + format("user:%s", bytebase_user.workspace_dba.email), + ] + actions = ["QUERY", "EXPORT"] + reason = "Grant access" } + exceptions { database = "instances/test-sample-instance/databases/employee" - table = "salary" - column = "amount" - member = "user:ed@bytebase.com" - action = "QUERY" + table = "employee" + columns = ["emp_no"] + members = [ + format("user:%s", bytebase_user.workspace_dba.email), + ] + actions = ["EXPORT"] + reason = "Grant export access" + } + + exceptions { + members = [ + format("user:%s", bytebase_user.project_developer.email), + ] + actions = ["QUERY"] + reason = "Grant query access" + raw_expression = "resource.instance_id == \"test-sample-instance\" && resource.database_name == \"employee\" && resource.table_name == \"employee\" && resource.column_name in [\"first_name\", \"last_name\", \"gender\"]" } } } diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 709ed27..a8b4784 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf index 72e3484..5352690 100644 --- a/examples/sql_review/main.tf +++ b/examples/sql_review/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/users/main.tf b/examples/users/main.tf index 6d4e02a..9954ec1 100644 --- a/examples/users/main.tf +++ b/examples/users/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go index 0c90ca9..77f1bef 100644 --- a/provider/data_source_policy.go +++ b/provider/data_source_policy.go @@ -113,25 +113,40 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { Optional: true, ValidateFunc: validation.StringIsNotEmpty, }, - "column": { - Type: schema.TypeString, - Computed: computed, - Optional: true, - ValidateFunc: validation.StringIsNotEmpty, + "columns": { + Type: schema.TypeSet, + Computed: computed, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, }, - "member": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringIsNotEmpty, - Description: "The member in user:{email} or group:{email} format.", + "members": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "The member in user:{email} or group:{email} format.", + ValidateDiagFunc: internal.ResourceNameValidation( + "^user:", + "^group:", + ), + }, }, - "action": { - Type: schema.TypeString, + "actions": { + Type: schema.TypeSet, Required: true, - ValidateFunc: validation.StringInSlice([]string{ - v1pb.MaskingExceptionPolicy_MaskingException_QUERY.String(), - v1pb.MaskingExceptionPolicy_MaskingException_EXPORT.String(), - }, false), + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "The action to allow for members. Support QUERY or EXPORT", + ValidateFunc: validation.StringInSlice([]string{ + v1pb.MaskingExceptionPolicy_MaskingException_QUERY.String(), + v1pb.MaskingExceptionPolicy_MaskingException_EXPORT.String(), + }, false), + }, }, "reason": { Type: schema.TypeString, @@ -144,6 +159,12 @@ func getMaskingExceptionPolicySchema(computed bool) *schema.Schema { Optional: true, Description: "The expiration timestamp in YYYY-MM-DDThh:mm:ss.000Z format", }, + "raw_expression": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: `The raw CEL expression. We will use it as the masking exception and ignore the "database"/"schema"/"table"/"columns"/"expire_timestamp" fields if you provide the raw expression.`, + }, }, }, Set: exceptionHash, @@ -318,10 +339,6 @@ func dataSourcePolicyRead(ctx context.Context, d *schema.ResourceData, m interfa } func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnostics { - _, 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()) } @@ -332,51 +349,57 @@ func setPolicyMessage(d *schema.ResourceData, policy *v1pb.Policy) diag.Diagnost return diag.Errorf("cannot set enforce for policy: %s", err.Error()) } + key, payload, diags := flattenPolicyPayload(policy) + if diags != nil { + return diags + } + if err := d.Set(key, payload); err != nil { + return diag.Errorf("cannot set %s for policy: %s", key, err.Error()) + } + + return nil +} + +func flattenPolicyPayload(policy *v1pb.Policy) (string, interface{}, diag.Diagnostics) { + _, policyType, err := internal.GetPolicyParentAndType(policy.Name) + if err != nil { + return "", nil, diag.Errorf("cannot parse name for policy: %s", err.Error()) + } 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()) + return "", nil, diag.FromErr(err) } + return "masking_exception_policy", exceptionPolicy, nil } 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()) + return "", nil, diag.FromErr(err) } + return "global_masking_policy", maskingPolicy, nil } 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()) - } + return "disable_copy_data_policy", disableCopyDataPolicy, nil } 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()) - } + return "data_source_query_policy", dataSourceQueryPolicy, nil } 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 "rollout_policy", rolloutPolicy, nil } } - return nil + return "", nil, diag.Errorf("unsupported policy: %s", policy.Name) } func flattenRolloutPolicy(p *v1pb.RolloutPolicy) []interface{} { @@ -428,21 +451,49 @@ func flattenGlobalMaskingPolicy(p *v1pb.MaskingRulePolicy) ([]interface{}, error return []interface{}{policy}, nil } +type combineException struct { + expression string + reason string + members []interface{} + actions []interface{} +} + func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{}, error) { exceptionList := []interface{}{} - for _, exception := range p.MaskingExceptions { - raw := map[string]interface{}{} - raw["member"] = exception.Member - raw["action"] = exception.Action.String() + exceptionMap := map[string]*combineException{} + for _, exception := range p.MaskingExceptions { if exception.Condition == nil || exception.Condition.Expression == "" { - return nil, errors.Errorf("invalid exception policy condition") + // Skip invalid data. + continue } - raw["reason"] = exception.Condition.Description - expressions := strings.Split(exception.Condition.Expression, " && ") + key := fmt.Sprintf("[expression:%s] [reason:%s]", exception.Condition.Expression, exception.Condition.Description) + if _, ok := exceptionMap[key]; !ok { + exceptionMap[key] = &combineException{ + expression: exception.Condition.Expression, + reason: exception.Condition.Description, + members: []interface{}{}, + actions: []interface{}{}, + } + } + exceptionMap[key].members = append(exceptionMap[key].members, exception.Member) + exceptionMap[key].actions = append(exceptionMap[key].actions, exception.Action.String()) + } + + for _, combine := range exceptionMap { + raw := map[string]interface{}{ + "members": schema.NewSet(schema.HashString, combine.members), + "actions": schema.NewSet(schema.HashString, combine.actions), + "reason": combine.reason, + "raw_expression": combine.expression, + } + + expressions := strings.Split(combine.expression, " && ") instanceID := "" databaseName := "" + columns := []interface{}{} + for _, expression := range expressions { if strings.HasPrefix(expression, "resource.instance_id == ") { instanceID = strings.TrimSuffix( @@ -469,10 +520,10 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ ) } if strings.HasPrefix(expression, "resource.column_name == ") { - raw["column"] = strings.TrimSuffix( + columns = append(columns, strings.TrimSuffix( strings.TrimPrefix(expression, `resource.column_name == "`), `"`, - ) + )) } if strings.HasPrefix(expression, "request.time < ") { raw["expire_timestamp"] = strings.TrimSuffix( @@ -480,12 +531,31 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ `")`, ) } + if strings.HasPrefix(expression, "resource.column_name in [") { + // rawColumnListString should be: "col1", "col2" + rawColumnListString := strings.TrimSuffix( + strings.TrimPrefix(expression, `resource.column_name in [`), + `]`, + ) + rawColumnList := strings.SplitSeq(rawColumnListString, ",") + for rawColumn := range rawColumnList { + column := strings.TrimSuffix( + strings.TrimPrefix(strings.TrimSpace(rawColumn), `"`), + `"`, + ) + columns = append(columns, column) + } + } } if instanceID != "" && databaseName != "" { raw["database"] = fmt.Sprintf("%s%s/%s%s", internal.InstanceNamePrefix, instanceID, internal.DatabaseIDPrefix, databaseName) } + if len(columns) > 0 { + raw["columns"] = schema.NewSet(schema.HashString, columns) + } exceptionList = append(exceptionList, raw) } + policy := map[string]interface{}{ "exceptions": exceptionList, } @@ -493,9 +563,11 @@ func flattenMaskingExceptionPolicy(p *v1pb.MaskingExceptionPolicy) ([]interface{ } func exceptionHash(rawSchema interface{}) int { - exception, err := convertToV1Exception(rawSchema) + exceptions, err := convertToV1Exceptions(rawSchema) if err != nil { return 0 } - return internal.ToHash(exception) + return internal.ToHash(&v1pb.MaskingExceptionPolicy{ + MaskingExceptions: exceptions, + }) } diff --git a/provider/data_source_policy_list.go b/provider/data_source_policy_list.go index a7832ac..a1ce768 100644 --- a/provider/data_source_policy_list.go +++ b/provider/data_source_policy_list.go @@ -65,6 +65,9 @@ func dataSourcePolicyList() *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), }, }, }, @@ -96,20 +99,11 @@ func dataSourcePolicyListRead(ctx context.Context, d *schema.ResourceData, m int raw["inherit_from_parent"] = policy.InheritFromParent raw["enforce"] = policy.Enforce - if p := policy.GetMaskingExceptionPolicy(); p != nil { - exceptionPolicy, err := flattenMaskingExceptionPolicy(p) - if err != nil { - return diag.FromErr(err) - } - raw["masking_exception_policy"] = exceptionPolicy - } - if p := policy.GetMaskingRulePolicy(); p != nil { - maskingPolicy, err := flattenGlobalMaskingPolicy(p) - if err != nil { - return diag.FromErr(err) - } - raw["global_masking_policy"] = maskingPolicy + key, payload, diags := flattenPolicyPayload(policy) + if diags != nil { + return diags } + raw[key] = payload policies = append(policies, raw) } diff --git a/provider/data_source_policy_test.go b/provider/data_source_policy_test.go index ab9b391..4072a0f 100644 --- a/provider/data_source_policy_test.go +++ b/provider/data_source_policy_test.go @@ -39,7 +39,7 @@ func TestAccPolicyDataSource(t *testing.T) { resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.#", "1"), resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.#", "1"), resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.table", "salary"), - resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.column", "amount"), + resource.TestCheckResourceAttr("data.bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.columns.0", "amount"), ), }, }, diff --git a/provider/resource_policy.go b/provider/resource_policy.go index 4d4466c..e908391 100644 --- a/provider/resource_policy.go +++ b/provider/resource_policy.go @@ -347,57 +347,85 @@ func convertToMaskingRulePolicy(d *schema.ResourceData) (*v1pb.MaskingRulePolicy return policy, nil } -func convertToV1Exception(rawSchema interface{}) (*v1pb.MaskingExceptionPolicy_MaskingException, error) { +func convertToV1Exceptions(rawSchema interface{}) ([]*v1pb.MaskingExceptionPolicy_MaskingException, error) { rawException := rawSchema.(map[string]interface{}) expressions := []string{} - databaseFullName := rawException["database"].(string) - if databaseFullName != "" { - instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) - if err != nil { - return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) - } - expressions = append( - expressions, - fmt.Sprintf(`resource.instance_id == "%s"`, instanceID), - fmt.Sprintf(`resource.database_name == "%s"`, databaseName), - ) - - if schema, ok := rawException["schema"].(string); ok && schema != "" { - expressions = append(expressions, fmt.Sprintf(`resource.schema_name == "%s"`, schema)) - } - if table, ok := rawException["table"].(string); ok && table != "" { - expressions = append(expressions, fmt.Sprintf(`resource.table_name == "%s"`, table)) + rawExpression := rawException["raw_expression"].(string) + + if rawExpression != "" { + expressions = append(expressions, rawExpression) + } else { + databaseFullName := rawException["database"].(string) + if databaseFullName != "" { + instanceID, databaseName, err := internal.GetInstanceDatabaseID(databaseFullName) + if err != nil { + return nil, errors.Wrapf(err, "invalid database full name: %v", databaseFullName) + } + expressions = append( + expressions, + fmt.Sprintf(`resource.instance_id == "%s"`, instanceID), + fmt.Sprintf(`resource.database_name == "%s"`, databaseName), + ) + + if schema, ok := rawException["schema"].(string); ok && schema != "" { + expressions = append(expressions, fmt.Sprintf(`resource.schema_name == "%s"`, schema)) + } + if table, ok := rawException["table"].(string); ok && table != "" { + expressions = append(expressions, fmt.Sprintf(`resource.table_name == "%s"`, table)) + } + + if rawColumns, ok := rawException["columns"].(*schema.Set); ok && rawColumns.Len() > 0 { + columnNames := []string{} + for _, column := range rawColumns.List() { + columnNames = append(columnNames, fmt.Sprintf(`"%s"`, column.(string))) + } + expressions = append(expressions, fmt.Sprintf(`resource.column_name in [%s]`, strings.Join(columnNames, ", "))) + } } - if column, ok := rawException["column"].(string); ok && column != "" { - expressions = append(expressions, fmt.Sprintf(`resource.column_name == "%s"`, column)) + + if expire, ok := rawException["expire_timestamp"].(string); ok && expire != "" { + formattedTime, err := time.Parse(time.RFC3339, expire) + if err != nil { + return nil, errors.Wrapf(err, "invalid time: %v", expire) + } + expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) } } - if expire, ok := rawException["expire_timestamp"].(string); ok && expire != "" { - formattedTime, err := time.Parse(time.RFC3339, expire) - if err != nil { - return nil, errors.Wrapf(err, "invalid time: %v", expire) - } - expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339))) + exceptions := []*v1pb.MaskingExceptionPolicy_MaskingException{} + reason := rawException["reason"].(string) + + rawMembers, ok := rawException["members"].(*schema.Set) + if !ok || rawMembers.Len() == 0 { + return nil, errors.Errorf("invalid members in masking_exception_policy.exceptions") } - member := rawException["member"].(string) - if member == "allUsers" { - return nil, errors.Errorf("not support allUsers in masking_exception_policy") + + rawActions, ok := rawException["actions"].(*schema.Set) + if !ok || rawActions.Len() == 0 { + return nil, errors.Errorf("invalid actions in masking_exception_policy.exceptions") } - if err := internal.ValidateMemberBinding(member); err != nil { - return nil, err + + for _, rawMember := range rawMembers.List() { + member := rawMember.(string) + if err := internal.ValidateMemberBinding(member); err != nil { + return nil, err + } + for _, action := range rawActions.List() { + exceptions = append(exceptions, &v1pb.MaskingExceptionPolicy_MaskingException{ + Member: member, + Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( + v1pb.MaskingExceptionPolicy_MaskingException_Action_value[action.(string)], + ), + Condition: &expr.Expr{ + Description: reason, + Expression: strings.Join(expressions, " && "), + }, + }) + } } - return &v1pb.MaskingExceptionPolicy_MaskingException{ - Member: member, - Action: v1pb.MaskingExceptionPolicy_MaskingException_Action( - v1pb.MaskingExceptionPolicy_MaskingException_Action_value[rawException["action"].(string)], - ), - Condition: &expr.Expr{ - Description: rawException["reason"].(string), - Expression: strings.Join(expressions, " && "), - }, - }, nil + + return exceptions, nil } func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExceptionPolicy, error) { @@ -415,11 +443,11 @@ func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExcep policy := &v1pb.MaskingExceptionPolicy{} for _, raw := range exceptionList.List() { - exception, err := convertToV1Exception(raw) + exceptions, err := convertToV1Exceptions(raw) if err != nil { return nil, err } - policy.MaskingExceptions = append(policy.MaskingExceptions, exception) + policy.MaskingExceptions = append(policy.MaskingExceptions, exceptions...) } return policy, nil } diff --git a/provider/resource_policy_test.go b/provider/resource_policy_test.go index cd1eadd..66dbc25 100644 --- a/provider/resource_policy_test.go +++ b/provider/resource_policy_test.go @@ -35,7 +35,7 @@ func TestAccPolicy(t *testing.T) { resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.#", "1"), resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.#", "1"), resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.table", "salary"), - resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.column", "amount"), + resource.TestCheckResourceAttr("bytebase_policy.masking_exception_policy", "masking_exception_policy.0.exceptions.0.columns.0", "amount"), ), }, }, @@ -59,9 +59,9 @@ func getMaskingExceptionPolicy(database, table, column string) string { exceptions { database = "%s" table = "%s" - column = "%s" - member = "user:ed@bytebase.com" - action = "QUERY" + columns = ["%s"] + members = ["user:ed@bytebase.com"] + actions = ["QUERY"] } } `, database, table, column) diff --git a/tutorials/0-provider.tf b/tutorials/0-provider.tf index d6c867a..0ce6d75 100644 --- a/tutorials/0-provider.tf +++ b/tutorials/0-provider.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "3.9.1" + version = "3.9.2" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/tutorials/8-5-masking-exception.tf b/tutorials/8-5-masking-exception.tf index ff3275e..9c675e0 100644 --- a/tutorials/8-5-masking-exception.tf +++ b/tutorials/8-5-masking-exception.tf @@ -11,21 +11,12 @@ resource "bytebase_policy" "masking_exception_policy" { masking_exception_policy { exceptions { - reason = "Business requirement" - database = "instances/prod-sample-instance/databases/hr_prod" - table = "employee" - column = "birth_date" - member = "user:admin@example.com" - action = "QUERY" - expire_timestamp = "2027-07-30T16:11:49Z" - } - exceptions { - reason = "Export data for analysis" - database = "instances/prod-sample-instance/databases/hr_prod" - table = "employee" - column = "last_name" - member = "user:admin@example.com" - action = "EXPORT" + 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" } }