diff --git a/api/setting.go b/api/setting.go index fab775f..abb08fb 100644 --- a/api/setting.go +++ b/api/setting.go @@ -10,6 +10,8 @@ const ( SettingWorkspaceProfile SettingName = "bb.workspace.profile" // SettingWorkspaceExternalApproval is the setting name for workspace external approval config. SettingWorkspaceExternalApproval SettingName = "bb.workspace.approval.external" + // SettingDataClassification is the setting name for data classification. + SettingDataClassification SettingName = "bb.workspace.data-classification" ) // RiskLevel is the approval risk level. diff --git a/docs/data-sources/setting.md b/docs/data-sources/setting.md index bcf5780..8c7447a 100644 --- a/docs/data-sources/setting.md +++ b/docs/data-sources/setting.md @@ -21,6 +21,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)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) ### Read-Only @@ -29,6 +30,39 @@ The setting data source. - `external_approval_nodes` (Block List) Configure external nodes in the approval flow. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--external_approval_nodes)) - `id` (String) The ID of this resource. + +### Nested Schema for `classification` + +Optional: + +- `classification_from_config` (Boolean) If true, we will only store the classification in the config. Otherwise we will get the classification from table/column comment, and write back to the schema metadata. +- `classifications` (Block List) (see [below for nested schema](#nestedblock--classification--classifications)) +- `id` (String) The classification unique uuid. +- `levels` (Block List) (see [below for nested schema](#nestedblock--classification--levels)) +- `title` (String) The classification title. Optional. + + +### Nested Schema for `classification.classifications` + +Optional: + +- `description` (String) The classification description. +- `id` (String) The classification unique id, must in {number}-{number} format. +- `level` (String) The classification level id. +- `title` (String) The classification title. + + + +### Nested Schema for `classification.levels` + +Optional: + +- `description` (String) The classification level description. +- `id` (String) The classification level unique uuid. +- `title` (String) The classification level title. + + + ### Nested Schema for `workspace_profile` diff --git a/docs/resources/setting.md b/docs/resources/setting.md index 2e1b80e..421ad28 100644 --- a/docs/resources/setting.md +++ b/docs/resources/setting.md @@ -22,6 +22,7 @@ The setting resource. ### Optional - `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)) - `external_approval_nodes` (Block List) Configure external nodes in the approval flow. Require ENTERPRISE subscription. (see [below for nested schema](#nestedblock--external_approval_nodes)) - `workspace_profile` (Block List, Max: 1) (see [below for nested schema](#nestedblock--workspace_profile)) @@ -84,6 +85,39 @@ Optional: + +### Nested Schema for `classification` + +Optional: + +- `classification_from_config` (Boolean) If true, we will only store the classification in the config. Otherwise we will get the classification from table/column comment, and write back to the schema metadata. +- `classifications` (Block List) (see [below for nested schema](#nestedblock--classification--classifications)) +- `id` (String) The classification unique uuid. +- `levels` (Block List) (see [below for nested schema](#nestedblock--classification--levels)) +- `title` (String) The classification title. Optional. + + +### Nested Schema for `classification.classifications` + +Optional: + +- `description` (String) The classification description. +- `id` (String) The classification unique id, must in {number}-{number} format. +- `level` (String) The classification level id. +- `title` (String) The classification title. + + + +### Nested Schema for `classification.levels` + +Optional: + +- `description` (String) The classification level description. +- `id` (String) The classification level unique uuid. +- `title` (String) The classification level title. + + + ### Nested Schema for `external_approval_nodes` diff --git a/examples/database/main.tf b/examples/database/main.tf index e139030..a01c738 100644 --- a/examples/database/main.tf +++ b/examples/database/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 8929126..fc4fa79 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 7d0bc8f..0efb1b7 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 cac84e7..ae342b3 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 3b9b7a4..c1ef376 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 5421c88..b898b8a 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 4c9244f..e2974ca 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } @@ -29,6 +29,10 @@ data "bytebase_setting" "workspace_profile" { name = "bb.workspace.profile" } +data "bytebase_setting" "classification" { + name = "bb.workspace.data-classification" +} + output "approval_flow" { value = data.bytebase_setting.approval_flow } @@ -40,3 +44,7 @@ output "external_approval" { output "workspace_profile" { value = data.bytebase_setting.workspace_profile } + +output "classification" { + value = data.bytebase_setting.classification +} diff --git a/examples/setup/data_masking.tf b/examples/setup/data_masking.tf index 74be919..ded651d 100644 --- a/examples/setup/data_masking.tf +++ b/examples/setup/data_masking.tf @@ -1,6 +1,49 @@ +resource "bytebase_setting" "classification" { + name = "bb.workspace.data-classification" + + classification { + id = "unique-id" + title = "Classification Example" + + levels { + id = "1" + title = "Level 1" + } + levels { + id = "2" + title = "Level 2" + } + + classifications { + id = "1" + title = "Basic" + } + classifications { + id = "1-1" + title = "User basic info" + level = "2" + } + classifications { + id = "1-2" + title = "User contact info" + level = "2" + } + classifications { + id = "2" + title = "Relationship" + } + classifications { + id = "2-1" + title = "Social info" + level = "2" + } + } +} + resource "bytebase_database_catalog" "employee_catalog" { depends_on = [ - bytebase_instance.test + bytebase_instance.test, + bytebase_setting.classification ] database = "instances/test-sample-instance/databases/employee" @@ -9,13 +52,13 @@ resource "bytebase_database_catalog" "employee_catalog" { tables { name = "salary" columns { - name = "amount" - semantic_type = "default" - classification = "1-1-1" + name = "amount" + semantic_type = "default" } columns { - name = "emp_no" - semantic_type = "default-partial" + name = "emp_no" + semantic_type = "default-partial" + classification = "1-1" labels = { tenant = "example" region = "asia" diff --git a/examples/setup/main.tf b/examples/setup/main.tf index d6c3f96..57a4c9f 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # 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 a571ee7..f262419 100644 --- a/examples/users/main.tf +++ b/examples/users/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/vcs/main.tf b/examples/vcs/main.tf index 5d9c9f7..9afc88b 100644 --- a/examples/vcs/main.tf +++ b/examples/vcs/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.6" + version = "1.0.7" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go index 239a169..c5240c1 100644 --- a/provider/data_source_setting.go +++ b/provider/data_source_setting.go @@ -29,11 +29,109 @@ func dataSourceSetting() *schema.Resource { string(api.SettingWorkspaceApproval), string(api.SettingWorkspaceExternalApproval), string(api.SettingWorkspaceProfile), + string(api.SettingDataClassification), }, false), }, "approval_flow": getWorkspaceApprovalSetting(true), "external_approval_nodes": getExternalApprovalSetting(true), "workspace_profile": getWorkspaceProfileSetting(true), + "classification": getClassificationSetting(true), + }, + } +} + +func getClassificationSetting(computed bool) *schema.Schema { + return &schema.Schema{ + Computed: computed, + Optional: true, + Default: nil, + Type: schema.TypeList, + MaxItems: 1, + MinItems: 1, + Description: "Classification for data masking. Require ENTERPRISE subscription.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification unique uuid.", + }, + "title": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification title. Optional.", + }, + "classification_from_config": { + Type: schema.TypeBool, + Computed: computed, + Optional: true, + Description: "If true, we will only store the classification in the config. Otherwise we will get the classification from table/column comment, and write back to the schema metadata.", + }, + "levels": { + Computed: computed, + Optional: true, + Type: schema.TypeList, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification level unique uuid.", + }, + "title": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification level title.", + }, + "description": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification level description.", + }, + }, + }, + }, + "classifications": { + Computed: computed, + Optional: true, + Type: schema.TypeList, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification unique id, must in {number}-{number} format.", + }, + "title": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification title.", + }, + "description": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification description.", + }, + "level": { + Type: schema.TypeString, + Computed: computed, + Optional: true, + Description: "The classification level id.", + }, + }, + }, + }, + }, }, } } @@ -271,6 +369,12 @@ func setSettingMessage(ctx context.Context, d *schema.ResourceData, client api.C return diag.Errorf("cannot set workspace_profile: %s", err.Error()) } } + if value := setting.Value.GetDataClassificationSettingValue(); value != nil { + settingVal := flattenClassificationSetting(value) + if err := d.Set("classification", settingVal); err != nil { + return diag.Errorf("cannot set classification: %s", err.Error()) + } + } return nil } @@ -422,3 +526,37 @@ func flattenWorkspaceProfileSetting(setting *v1pb.WorkspaceProfileSetting) []int return []interface{}{raw} } + +func flattenClassificationSetting(setting *v1pb.DataClassificationSetting) []interface{} { + raw := map[string]interface{}{} + + if len(setting.GetConfigs()) > 0 { + config := setting.GetConfigs()[0] + raw["id"] = config.Id + raw["title"] = config.Title + raw["classification_from_config"] = config.ClassificationFromConfig + + rawLevels := []interface{}{} + for _, level := range config.Levels { + rawLevel := map[string]interface{}{} + rawLevel["id"] = level.Id + rawLevel["title"] = level.Title + rawLevel["description"] = level.Description + rawLevels = append(rawLevels, rawLevel) + } + raw["levels"] = rawLevels + + rawClassifications := []interface{}{} + for _, classification := range config.GetClassification() { + rawClassification := map[string]interface{}{} + rawClassification["id"] = classification.Id + rawClassification["title"] = classification.Title + rawClassification["description"] = classification.Description + rawClassification["level"] = classification.LevelId + rawClassifications = append(rawClassifications, rawClassification) + } + raw["classifications"] = rawClassifications + } + + return []interface{}{raw} +} diff --git a/provider/resource_setting.go b/provider/resource_setting.go index 1dfc4d5..4702a8b 100644 --- a/provider/resource_setting.go +++ b/provider/resource_setting.go @@ -35,12 +35,13 @@ func resourceSetting() *schema.Resource { string(api.SettingWorkspaceApproval), string(api.SettingWorkspaceExternalApproval), string(api.SettingWorkspaceProfile), + string(api.SettingDataClassification), }, false), }, "approval_flow": getWorkspaceApprovalSetting(false), "external_approval_nodes": getExternalApprovalSetting(false), "workspace_profile": getWorkspaceProfileSetting(false), - }, + "classification": getClassificationSetting(false)}, } } @@ -88,6 +89,16 @@ func resourceSettingUpsert(ctx context.Context, d *schema.ResourceData, m interf }, } updateMasks = updatePathes + case api.SettingDataClassification: + classificationSetting, err := convertToV1ClassificationSetting(d) + if err != nil { + return diag.FromErr(err) + } + setting.Value = &v1pb.Value{ + Value: &v1pb.Value_DataClassificationSettingValue{ + DataClassificationSettingValue: classificationSetting, + }, + } default: return diag.FromErr(errors.Errorf("Unsupport setting: %v", name)) } @@ -146,6 +157,70 @@ func convertToV1WorkspaceProfileSetting(d *schema.ResourceData) (*v1pb.Workspace return workspacePrfile, updateMasks, nil } +func convertToV1ClassificationSetting(d *schema.ResourceData) (*v1pb.DataClassificationSetting, error) { + rawList, ok := d.Get("classification").([]interface{}) + if !ok || len(rawList) != 1 { + return nil, errors.Errorf("invalid classification") + } + + raw := rawList[0].(map[string]interface{}) + + dataClassificationConfig := &v1pb.DataClassificationSetting_DataClassificationConfig{ + Id: raw["id"].(string), + Title: raw["title"].(string), + ClassificationFromConfig: raw["classification_from_config"].(bool), + Levels: []*v1pb.DataClassificationSetting_DataClassificationConfig_Level{}, + Classification: map[string]*v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification{}, + } + if dataClassificationConfig.Id == "" { + return nil, errors.Errorf("id is required for classification config") + } + + rawLevels := raw["levels"].([]interface{}) + for _, level := range rawLevels { + rawLevel := level.(map[string]interface{}) + classificationLevel := &v1pb.DataClassificationSetting_DataClassificationConfig_Level{ + Id: rawLevel["id"].(string), + Title: rawLevel["title"].(string), + Description: rawLevel["description"].(string), + } + if classificationLevel.Id == "" { + return nil, errors.Errorf("classification level id is required") + } + if classificationLevel.Title == "" { + return nil, errors.Errorf("classification level title is required") + } + dataClassificationConfig.Levels = append(dataClassificationConfig.Levels, classificationLevel) + } + + rawClassificationss := raw["classifications"].([]interface{}) + for _, classification := range rawClassificationss { + rawClassification := classification.(map[string]interface{}) + classificationData := &v1pb.DataClassificationSetting_DataClassificationConfig_DataClassification{ + Id: rawClassification["id"].(string), + Title: rawClassification["title"].(string), + Description: rawClassification["description"].(string), + } + if classificationData.Id == "" { + return nil, errors.Errorf("classification id is required") + } + if classificationData.Title == "" { + return nil, errors.Errorf("classification title is required") + } + levelID, ok := rawClassification["level"].(string) + if ok { + classificationData.LevelId = &levelID + } + dataClassificationConfig.Classification[classificationData.Id] = classificationData + } + + return &v1pb.DataClassificationSetting{ + Configs: []*v1pb.DataClassificationSetting_DataClassificationConfig{ + dataClassificationConfig, + }, + }, nil +} + func convertToV1ExternalNodesSetting(d *schema.ResourceData) (*v1pb.ExternalApprovalSetting, error) { rawList, ok := d.Get("external_approval_nodes").([]interface{}) if !ok || len(rawList) != 1 {