diff --git a/VERSION b/VERSION index e7ad390..450f88e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.19 \ No newline at end of file +1.0.20 \ No newline at end of file diff --git a/api/client.go b/api/client.go index 69d0b74..08cdd3a 100644 --- a/api/client.go +++ b/api/client.go @@ -163,4 +163,14 @@ type Client interface { GetWorkspaceIAMPolicy(ctx context.Context) (*v1pb.IamPolicy, error) // SetWorkspaceIAMPolicy sets the workspace IAM policy. SetWorkspaceIAMPolicy(ctx context.Context, setIamPolicyRequest *v1pb.SetIamPolicyRequest) (*v1pb.IamPolicy, error) + + // Review config + // ListReviewConfig will return review configs. + ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsResponse, error) + // GetReviewConfig gets the review config by full name. + GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error) + // UpsertReviewConfig updates or creates the review config. + UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error) + // DeleteReviewConfig deletes the review config. + DeleteReviewConfig(ctx context.Context, reviewName string) error } diff --git a/client/instance.go b/client/instance.go index 8e453b2..91e4904 100644 --- a/client/instance.go +++ b/client/instance.go @@ -10,7 +10,7 @@ import ( "google.golang.org/protobuf/encoding/protojson" ) -// ListInstance will return instances in environment. +// ListInstance will return instances. func (c *client) ListInstance(ctx context.Context, showDeleted bool) (*v1pb.ListInstancesResponse, error) { req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/instances?showDeleted=%v", c.url, c.version, showDeleted), nil) if err != nil { diff --git a/client/review_config.go b/client/review_config.go new file mode 100644 index 0000000..4fcbe85 --- /dev/null +++ b/client/review_config.go @@ -0,0 +1,64 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" +) + +// ListReviewConfig will return review configs. +func (c *client) ListReviewConfig(ctx context.Context) (*v1pb.ListReviewConfigsResponse, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%s/reviewConfigs", c.url, c.version), nil) + if err != nil { + return nil, err + } + + body, err := c.doRequest(req) + if err != nil { + return nil, err + } + + var res v1pb.ListReviewConfigsResponse + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// GetReviewConfig gets the review config by full name. +func (c *client) GetReviewConfig(ctx context.Context, reviewName string) (*v1pb.ReviewConfig, error) { + body, err := c.getResource(ctx, reviewName) + if err != nil { + return nil, err + } + + var res v1pb.ReviewConfig + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// UpsertReviewConfig updates or creates the review config. +func (c *client) UpsertReviewConfig(ctx context.Context, patch *v1pb.ReviewConfig, updateMasks []string) (*v1pb.ReviewConfig, error) { + body, err := c.updateResource(ctx, patch.Name, patch, updateMasks, true /* allow missing */) + if err != nil { + return nil, err + } + + var res v1pb.ReviewConfig + if err := ProtojsonUnmarshaler.Unmarshal(body, &res); err != nil { + return nil, err + } + + return &res, nil +} + +// DeleteReviewConfig deletes the review config. +func (c *client) DeleteReviewConfig(ctx context.Context, reviewName string) error { + return c.deleteResource(ctx, reviewName) +} diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md index 6779230..571116c 100644 --- a/docs/data-sources/group.md +++ b/docs/data-sources/group.md @@ -24,6 +24,7 @@ The group data source. - `description` (String) The group description. - `id` (String) The ID of this resource. - `members` (Set of Object) The members in the group. (see [below for nested schema](#nestedatt--members)) +- `roles` (Set of String) The group's roles in the workspace level - `source` (String) Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID. - `title` (String) The group title. diff --git a/docs/data-sources/group_list.md b/docs/data-sources/group_list.md index a21bd4f..fb47f7b 100644 --- a/docs/data-sources/group_list.md +++ b/docs/data-sources/group_list.md @@ -28,6 +28,7 @@ Read-Only: - `description` (String) - `members` (Set of Object) (see [below for nested schema](#nestedobjatt--groups--members)) - `name` (String) +- `roles` (Set of String) - `source` (String) - `title` (String) diff --git a/docs/data-sources/review_config.md b/docs/data-sources/review_config.md new file mode 100644 index 0000000..bae2a43 --- /dev/null +++ b/docs/data-sources/review_config.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_review_config Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The review config data source. +--- + +# bytebase_review_config (Data Source) + +The review config data source. + + + + +## Schema + +### Read-Only + +- `enabled` (Boolean) Enable the SQL review config +- `id` (String) The ID of this resource. +- `resource_id` (String) The unique resource id for the review config. +- `resources` (Set of String) Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample. +- `rules` (Set of Object) The SQL review rules. (see [below for nested schema](#nestedatt--rules)) +- `title` (String) The title for the review config. + + +### Nested Schema for `rules` + +Read-Only: + +- `comment` (String) +- `engine` (String) +- `level` (String) +- `payload` (String) +- `type` (String) + + diff --git a/docs/data-sources/review_config_list.md b/docs/data-sources/review_config_list.md new file mode 100644 index 0000000..c3bcb7a --- /dev/null +++ b/docs/data-sources/review_config_list.md @@ -0,0 +1,45 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_review_config_list Data Source - terraform-provider-bytebase" +subcategory: "" +description: |- + The review config data source list. +--- + +# bytebase_review_config_list (Data Source) + +The review config data source list. + + + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `review_configs` (List of Object) (see [below for nested schema](#nestedatt--review_configs)) + + +### Nested Schema for `review_configs` + +Read-Only: + +- `enabled` (Boolean) +- `resource_id` (String) +- `resources` (Set of String) +- `rules` (Set of Object) (see [below for nested schema](#nestedobjatt--review_configs--rules)) +- `title` (String) + + +### Nested Schema for `review_configs.rules` + +Read-Only: + +- `comment` (String) +- `engine` (String) +- `level` (String) +- `payload` (String) +- `type` (String) + + diff --git a/docs/resources/group.md b/docs/resources/group.md index daa7cd9..84ed19f 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -24,6 +24,7 @@ The group resource. Workspace domain is required for creating groups. ### Optional - `description` (String) The group description. +- `roles` (Set of String) The group's roles in the workspace level ### Read-Only diff --git a/docs/resources/review_config.md b/docs/resources/review_config.md new file mode 100644 index 0000000..5bda0cc --- /dev/null +++ b/docs/resources/review_config.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "bytebase_review_config Resource - terraform-provider-bytebase" +subcategory: "" +description: |- + The review config resource. +--- + +# bytebase_review_config (Resource) + +The review config resource. + + + + +## Schema + +### Required + +- `enabled` (Boolean) Enable the SQL review config +- `resource_id` (String) The unique resource id for the review config. +- `rules` (Block Set, Min: 1) The SQL review rules. (see [below for nested schema](#nestedblock--rules)) +- `title` (String) The title for the review config. + +### Optional + +- `resources` (Set of String) Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `rules` + +Required: + +- `engine` (String) The rule for the database engine. +- `level` (String) The rule level. +- `type` (String) The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules + +Optional: + +- `comment` (String) The comment for the rule. +- `payload` (String) The payload for the rule. + + diff --git a/docs/resources/user.md b/docs/resources/user.md index 8cfe660..71d19a7 100644 --- a/docs/resources/user.md +++ b/docs/resources/user.md @@ -25,6 +25,7 @@ The user resource. - `password` (String, Sensitive) The user login password. - `phone` (String) The user phone. - `roles` (Set of String) The user's roles in the workspace level +- `type` (String) The user type. ### Read-Only @@ -33,8 +34,8 @@ The user resource. - `last_login_time` (String) The user last login time. - `mfa_enabled` (Boolean) The mfa_enabled flag means if the user has enabled MFA. - `name` (String) The user name in users/{user id or email} format. +- `service_key` (String) The service key for service account. - `source` (String) Source means where the user comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID. - `state` (String) The user is deleted or not. -- `type` (String) The user type. diff --git a/examples/environments/main.tf b/examples/environments/main.tf index 288369d..b7c07ea 100644 --- a/examples/environments/main.tf +++ b/examples/environments/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 4c8764c..2b88420 100644 --- a/examples/groups/main.tf +++ b/examples/groups/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 b3626b6..206ea08 100644 --- a/examples/instances/main.tf +++ b/examples/instances/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 6f45f4c..bd39820 100644 --- a/examples/policies/main.tf +++ b/examples/policies/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 d3d3f61..cec7e85 100644 --- a/examples/projects/main.tf +++ b/examples/projects/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 92b116c..f9112a2 100644 --- a/examples/roles/main.tf +++ b/examples/roles/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 8f0d146..614bceb 100644 --- a/examples/settings/main.tf +++ b/examples/settings/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/examples/setup/main.tf b/examples/setup/main.tf index 116af61..6401533 100644 --- a/examples/setup/main.tf +++ b/examples/setup/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 new file mode 100644 index 0000000..786d4c3 --- /dev/null +++ b/examples/setup/sql_review.tf @@ -0,0 +1,24 @@ +resource "bytebase_review_config" "sample" { + depends_on = [ + bytebase_environment.test, + bytebase_environment.prod + ] + + resource_id = "review-config-sample" + title = "Sample SQL Review Config" + enabled = true + resources = toset([ + bytebase_environment.test.name, + bytebase_environment.prod.name + ]) + rules { + type = "column.no-null" + engine = "MYSQL" + level = "WARNING" + } + rules { + type = "table.require-pk" + engine = "MYSQL" + level = "ERROR" + } +} diff --git a/examples/setup/users.tf b/examples/setup/users.tf index d8c178a..c681f47 100644 --- a/examples/setup/users.tf +++ b/examples/setup/users.tf @@ -33,16 +33,28 @@ resource "bytebase_user" "project_developer" { roles = ["roles/projectViewer"] } +resource "bytebase_user" "service_account" { + depends_on = [ + bytebase_user.project_developer + ] + title = "CI Bot" + email = "ci-bot@service.bytebase.com" + type = "SERVICE_ACCOUNT" + roles = ["roles/workspaceDBA"] +} + + # Create or update the group. resource "bytebase_group" "developers" { depends_on = [ bytebase_user.workspace_dba, bytebase_user.project_developer, + bytebase_user.service_account, # group requires the domain. bytebase_setting.workspace_profile ] - email = "developers@bytebase.com" + email = "developers+dba@bytebase.com" title = "Bytebase Developers" members { @@ -54,4 +66,6 @@ resource "bytebase_group" "developers" { member = format("users/%s", bytebase_user.project_developer.email) role = "MEMBER" } + + roles = ["roles/projectViewer"] } diff --git a/examples/sql_review/main.tf b/examples/sql_review/main.tf new file mode 100644 index 0000000..aef9f8e --- /dev/null +++ b/examples/sql_review/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + bytebase = { + version = "1.0.20" + # For local development, please use "terraform.local/bytebase/bytebase" instead + source = "registry.terraform.io/bytebase/bytebase" + } + } +} + +provider "bytebase" { + # You need to replace the account and key with your Bytebase service account. + service_account = "terraform@service.bytebase.com" + service_key = "bbs_BxVIp7uQsARl8nR92ZZV" + # The Bytebase service URL. You can use the external URL in production. + # Check the docs about external URL: https://www.bytebase.com/docs/get-started/install/external-url + url = "https://bytebase.example.com" +} + +data "bytebase_review_config_list" "all" { +} + +output "all_review_configs" { + value = data.bytebase_review_config_list.all +} diff --git a/examples/users/main.tf b/examples/users/main.tf index 4c2d7ed..093b690 100644 --- a/examples/users/main.tf +++ b/examples/users/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # 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 d72c644..cf2c1d4 100644 --- a/examples/vcs/main.tf +++ b/examples/vcs/main.tf @@ -1,7 +1,7 @@ terraform { required_providers { bytebase = { - version = "1.0.19" + version = "1.0.20" # For local development, please use "terraform.local/bytebase/bytebase" instead source = "registry.terraform.io/bytebase/bytebase" } diff --git a/provider/data_source_group.go b/provider/data_source_group.go index 26e3fee..e343abc 100644 --- a/provider/data_source_group.go +++ b/provider/data_source_group.go @@ -42,6 +42,14 @@ func dataSourceGroup() *schema.Resource { Computed: true, Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.", }, + "roles": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The group's roles in the workspace level", + }, "members": { Type: schema.TypeSet, Computed: true, @@ -75,10 +83,15 @@ func dataSourceGroupRead(ctx context.Context, d *schema.ResourceData, m interfac } d.SetId(group.Name) - return setGroup(d, group) + return setGroup(ctx, c, d, group) } -func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics { +func setGroup(ctx context.Context, client api.Client, d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics { + workspaceIAM, err := client.GetWorkspaceIAMPolicy(ctx) + if err != nil { + return diag.Errorf("cannot get workspace IAM with error: %s", err.Error()) + } + if err := d.Set("name", group.Name); err != nil { return diag.Errorf("cannot set name for group: %s", err.Error()) } @@ -102,6 +115,13 @@ func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics { if err := d.Set("members", schema.NewSet(memberHash, memberList)); err != nil { return diag.Errorf("cannot set members for group: %s", err.Error()) } + groupEmail, err := internal.GetGroupEmail(group.Name) + if err != nil { + return diag.Errorf("failed to parse group email: %v", err) + } + if err := d.Set("roles", getRolesInIAM(workspaceIAM, fmt.Sprintf("group:%s", groupEmail))); err != nil { + return diag.Errorf("cannot set roles for user: %s", err.Error()) + } return nil } diff --git a/provider/data_source_group_list.go b/provider/data_source_group_list.go index 35abc02..d597a07 100644 --- a/provider/data_source_group_list.go +++ b/provider/data_source_group_list.go @@ -2,6 +2,7 @@ package provider import ( "context" + "fmt" "strconv" "time" @@ -9,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" ) func dataSourceGroupList() *schema.Resource { @@ -41,6 +43,14 @@ func dataSourceGroupList() *schema.Resource { Computed: true, Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.", }, + "roles": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The group's roles in the workspace level", + }, "members": { Type: schema.TypeSet, Computed: true, @@ -76,6 +86,11 @@ func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m inte return diag.FromErr(err) } + workspaceIAM, err := c.GetWorkspaceIAMPolicy(ctx) + if err != nil { + return diag.Errorf("cannot get workspace IAM with error: %s", err.Error()) + } + groups := make([]map[string]interface{}, 0) for _, group := range response.Groups { raw := make(map[string]interface{}) @@ -92,6 +107,12 @@ func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m inte memberList = append(memberList, rawMember) } raw["members"] = schema.NewSet(memberHash, memberList) + + groupEmail, err := internal.GetGroupEmail(group.Name) + if err != nil { + return diag.Errorf("failed to parse group email: %v", err) + } + raw["roles"] = getRolesInIAM(workspaceIAM, fmt.Sprintf("group:%s", groupEmail)) groups = append(groups, raw) } diff --git a/provider/data_source_review_config.go b/provider/data_source_review_config.go new file mode 100644 index 0000000..9ad7f6f --- /dev/null +++ b/provider/data_source_review_config.go @@ -0,0 +1,93 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceReviewConfig() *schema.Resource { + return &schema.Resource{ + Description: "The review config data source.", + ReadContext: dataSourceReviewConfigRead, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique resource id for the review config.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The title for the review config.", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable the SQL review config", + }, + "resources": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.", + }, + "rules": { + Type: schema.TypeSet, + Computed: true, + Description: "The SQL review rules.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules", + }, + "engine": { + Type: schema.TypeString, + Computed: true, + Description: "The rule for the database engine.", + }, + "level": { + Type: schema.TypeString, + Computed: true, + Description: "The rule level.", + }, + "payload": { + Type: schema.TypeString, + Computed: true, + Description: "The payload for the rule.", + }, + "comment": { + Type: schema.TypeString, + Computed: true, + Description: "The comment for the rule.", + }, + }, + }, + Set: reviewRuleHash, + }, + }, + } +} + +func dataSourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + reviewName := fmt.Sprintf("%s%s", internal.ReviewConfigNamePrefix, d.Get("resource_id").(string)) + + review, err := c.GetReviewConfig(ctx, reviewName) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(review.Name) + + return setReviewConfig(d, review) +} diff --git a/provider/data_source_review_config_list.go b/provider/data_source_review_config_list.go new file mode 100644 index 0000000..a3d6500 --- /dev/null +++ b/provider/data_source_review_config_list.go @@ -0,0 +1,122 @@ +package provider + +import ( + "context" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func dataSourceReviewConfigList() *schema.Resource { + return &schema.Resource{ + Description: "The review config data source list.", + ReadContext: dataSourceReviewConfigListRead, + Schema: map[string]*schema.Schema{ + "review_configs": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique resource id for the review config.", + }, + "title": { + Type: schema.TypeString, + Computed: true, + Description: "The title for the review config.", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable the SQL review config", + }, + "resources": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.", + }, + "rules": { + Type: schema.TypeSet, + Computed: true, + Description: "The SQL review rules.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules", + }, + "engine": { + Type: schema.TypeString, + Computed: true, + Description: "The rule for the database engine.", + }, + "level": { + Type: schema.TypeString, + Computed: true, + Description: "The rule level.", + }, + "payload": { + Type: schema.TypeString, + Computed: true, + Description: "The payload for the rule.", + }, + "comment": { + Type: schema.TypeString, + Computed: true, + Description: "The comment for the rule.", + }, + }, + }, + Set: reviewRuleHash, + }, + }, + }, + }, + }, + } +} + +func dataSourceReviewConfigListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + response, err := c.ListReviewConfig(ctx) + if err != nil { + return diag.FromErr(err) + } + + reviews := make([]map[string]interface{}, 0) + for _, review := range response.ReviewConfigs { + raw := make(map[string]interface{}) + reviewID, err := internal.GetReviewConfigID(review.Name) + if err != nil { + return diag.Errorf("failed to parse id from review name %s with error: %v", review.Name, err.Error()) + } + raw["resource_id"] = reviewID + raw["title"] = review.Title + raw["enabled"] = review.Enabled + raw["resources"] = review.Resources + raw["rules"] = schema.NewSet(reviewRuleHash, flattenReviewRules(review.Rules)) + + reviews = append(reviews, raw) + } + + if err := d.Set("review_configs", reviews); err != nil { + return diag.FromErr(err) + } + + // always refresh + d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) + + return nil +} diff --git a/provider/data_source_user.go b/provider/data_source_user.go index 8553765..644a060 100644 --- a/provider/data_source_user.go +++ b/provider/data_source_user.go @@ -99,13 +99,12 @@ func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, m interface return setUser(ctx, c, d, user) } -func getUserRoles(iamPolicy *v1pb.IamPolicy, email string) []string { - userBinding := fmt.Sprintf("user:%s", email) +func getRolesInIAM(iamPolicy *v1pb.IamPolicy, memberBinding string) []string { roles := []string{} for _, binding := range iamPolicy.Bindings { for _, member := range binding.Members { - if member == userBinding { + if member == memberBinding { roles = append(roles, binding.Role) } } @@ -153,7 +152,7 @@ func setUser(ctx context.Context, client api.Client, d *schema.ResourceData, use return diag.Errorf("cannot set source for user: %s", err.Error()) } } - if err := d.Set("roles", getUserRoles(workspaceIAM, user.Email)); err != nil { + if err := d.Set("roles", getRolesInIAM(workspaceIAM, fmt.Sprintf("user:%s", user.Email))); err != nil { return diag.Errorf("cannot set roles for user: %s", err.Error()) } diff --git a/provider/data_source_user_list.go b/provider/data_source_user_list.go index 8acb045..be88fcc 100644 --- a/provider/data_source_user_list.go +++ b/provider/data_source_user_list.go @@ -2,6 +2,7 @@ package provider import ( "context" + "fmt" "strconv" "time" @@ -120,7 +121,7 @@ func dataSourceUserListRead(ctx context.Context, d *schema.ResourceData, m inter raw["last_login_time"] = p.LastLoginTime.AsTime().UTC().Format(time.RFC3339) raw["last_change_password_time"] = p.LastChangePasswordTime.AsTime().UTC().Format(time.RFC3339) } - raw["roles"] = getUserRoles(workspaceIAM, user.Email) + raw["roles"] = getRolesInIAM(workspaceIAM, fmt.Sprintf("user:%s", user.Email)) users = append(users, raw) } if err := d.Set("users", users); err != nil { diff --git a/provider/internal/mock_client.go b/provider/internal/mock_client.go index c2f88a4..b16ad51 100644 --- a/provider/internal/mock_client.go +++ b/provider/internal/mock_client.go @@ -901,3 +901,23 @@ func (c *mockClient) DeleteRole(_ context.Context, roleName string) error { delete(c.roleMap, roleName) return nil } + +// ListReviewConfig will return review configs. +func (*mockClient) ListReviewConfig(_ context.Context) (*v1pb.ListReviewConfigsResponse, error) { + return &v1pb.ListReviewConfigsResponse{}, nil +} + +// GetReviewConfig gets the review config by full name. +func (*mockClient) GetReviewConfig(_ context.Context, _ string) (*v1pb.ReviewConfig, error) { + return &v1pb.ReviewConfig{}, nil +} + +// UpsertReviewConfig updates or creates the review config. +func (*mockClient) UpsertReviewConfig(_ context.Context, _ *v1pb.ReviewConfig, _ []string) (*v1pb.ReviewConfig, error) { + return &v1pb.ReviewConfig{}, nil +} + +// DeleteReviewConfig deletes the review config. +func (*mockClient) DeleteReviewConfig(_ context.Context, _ string) error { + return nil +} diff --git a/provider/internal/utils.go b/provider/internal/utils.go index d40dd3b..44a1600 100644 --- a/provider/internal/utils.go +++ b/provider/internal/utils.go @@ -37,6 +37,8 @@ const ( GroupNamePrefix = "groups/" // RoleNamePrefix is the prefix for role name. RoleNamePrefix = "roles/" + // ReviewConfigNamePrefix is the prefix for the review config name. + ReviewConfigNamePrefix = "reviewConfigs/" // DatabaseCatalogNameSuffix is the suffix for the database catalog name. DatabaseCatalogNameSuffix = "/catalog" // ResourceIDPattern is the pattern for resource id. @@ -80,6 +82,31 @@ func ResourceNameValidation(regexs ...*regexp.Regexp) schema.SchemaValidateDiagF } } +// EngineValidation validate the engine type. +var EngineValidation = validation.StringInSlice([]string{ + v1pb.Engine_CLICKHOUSE.String(), + v1pb.Engine_MYSQL.String(), + v1pb.Engine_POSTGRES.String(), + v1pb.Engine_SNOWFLAKE.String(), + v1pb.Engine_SQLITE.String(), + v1pb.Engine_TIDB.String(), + v1pb.Engine_MONGODB.String(), + v1pb.Engine_REDIS.String(), + v1pb.Engine_ORACLE.String(), + v1pb.Engine_SPANNER.String(), + v1pb.Engine_MSSQL.String(), + v1pb.Engine_REDSHIFT.String(), + v1pb.Engine_MARIADB.String(), + v1pb.Engine_OCEANBASE.String(), + v1pb.Engine_DM.String(), + v1pb.Engine_RISINGWAVE.String(), + v1pb.Engine_OCEANBASE_ORACLE.String(), + v1pb.Engine_STARROCKS.String(), + v1pb.Engine_DORIS.String(), + v1pb.Engine_HIVE.String(), + v1pb.Engine_ELASTICSEARCH.String(), +}, false) + // GetPolicyParentAndType returns the policy parent and type by the name. func GetPolicyParentAndType(name string) (string, v1pb.PolicyType, error) { names := strings.Split(name, PolicyNamePrefix) @@ -153,6 +180,24 @@ func GetRoleID(name string) (string, error) { return tokens[0], nil } +// GetGroupEmail will parse the email from group full name. +func GetGroupEmail(name string) (string, error) { + tokens, err := getNameParentTokens(name, GroupNamePrefix) + if err != nil { + return "", err + } + return tokens[0], nil +} + +// GetReviewConfigID will parse the id from review config full name. +func GetReviewConfigID(name string) (string, error) { + tokens, err := getNameParentTokens(name, ReviewConfigNamePrefix) + if err != nil { + return "", err + } + return tokens[0], nil +} + // GetInstanceDatabaseID will parse the instance resource id and database name. func GetInstanceDatabaseID(name string) (string, string, error) { // the instance request should be instances/{instance-id}/databases/{database-id} diff --git a/provider/provider.go b/provider/provider.go index 7702d4d..9136a1b 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -70,6 +70,8 @@ func NewProvider() *schema.Provider { "bytebase_group_list": dataSourceGroupList(), "bytebase_database": dataSourceDatabase(), "bytebase_database_list": dataSourceDatabaseList(), + "bytebase_review_config": dataSourceReviewConfig(), + "bytebase_review_config_list": dataSourceReviewConfigList(), }, ResourcesMap: map[string]*schema.Resource{ "bytebase_environment": resourceEnvironment(), @@ -83,6 +85,7 @@ func NewProvider() *schema.Provider { "bytebase_role": resourceRole(), "bytebase_group": resourceGroup(), "bytebase_database": resourceDatabase(), + "bytebase_review_config": resourceReviewConfig(), }, } } diff --git a/provider/resource_group.go b/provider/resource_group.go index b6c7e4d..cb9bb69 100644 --- a/provider/resource_group.go +++ b/provider/resource_group.go @@ -55,6 +55,14 @@ func resourceGroup() *schema.Resource { Computed: true, Description: "Source means where the group comes from. For now we support Entra ID SCIM sync, so the source could be Entra ID.", }, + "roles": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "The group's roles in the workspace level", + }, "members": { Type: schema.TypeSet, Required: true, @@ -96,7 +104,7 @@ func resourceGroupRead(ctx context.Context, d *schema.ResourceData, m interface{ return diag.FromErr(err) } - return setGroup(d, group) + return setGroup(ctx, c, d, group) } func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { @@ -172,6 +180,19 @@ func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, m interfac } } + roles, diagnostic := getRoles(d) + if diagnostic != nil { + return diagnostic + } + if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("group:%s", groupEmail), roles); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to patch group roles", + Detail: fmt.Sprintf("Update roles for group %s failed, error: %v", groupName, err), + }) + return diags + } + d.SetId(groupName) diag := resourceGroupRead(ctx, d, m) @@ -225,6 +246,22 @@ func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, m interfac } } + if d.HasChange("roles") { + roles, diagnostic := getRoles(d) + if diagnostic != nil { + return diagnostic + } + groupEmail := d.Get("email").(string) + if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("group:%s", groupEmail), roles); err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Failed to patch group roles", + Detail: fmt.Sprintf("Update roles for group %s failed, error: %v", groupName, err), + }) + return diags + } + } + diag := resourceGroupRead(ctx, d, m) if diag != nil { diags = append(diags, diag...) diff --git a/provider/resource_instance.go b/provider/resource_instance.go index 5902917..739f3d0 100644 --- a/provider/resource_instance.go +++ b/provider/resource_instance.go @@ -59,32 +59,10 @@ func resourceInstance() *schema.Resource { Description: "The instance full name in instances/{resource id} format.", }, "engine": { - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.StringInSlice([]string{ - v1pb.Engine_CLICKHOUSE.String(), - v1pb.Engine_MYSQL.String(), - v1pb.Engine_POSTGRES.String(), - v1pb.Engine_SNOWFLAKE.String(), - v1pb.Engine_SQLITE.String(), - v1pb.Engine_TIDB.String(), - v1pb.Engine_MONGODB.String(), - v1pb.Engine_REDIS.String(), - v1pb.Engine_ORACLE.String(), - v1pb.Engine_SPANNER.String(), - v1pb.Engine_MSSQL.String(), - v1pb.Engine_REDSHIFT.String(), - v1pb.Engine_MARIADB.String(), - v1pb.Engine_OCEANBASE.String(), - v1pb.Engine_DM.String(), - v1pb.Engine_RISINGWAVE.String(), - v1pb.Engine_OCEANBASE_ORACLE.String(), - v1pb.Engine_STARROCKS.String(), - v1pb.Engine_DORIS.String(), - v1pb.Engine_HIVE.String(), - v1pb.Engine_ELASTICSEARCH.String(), - }, false), - Description: "The instance engine. Support MYSQL, POSTGRES, TIDB, SNOWFLAKE, CLICKHOUSE, MONGODB, SQLITE, REDIS, ORACLE, SPANNER, MSSQL, REDSHIFT, MARIADB, OCEANBASE.", + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.EngineValidation, + Description: "The instance engine. Support MYSQL, POSTGRES, TIDB, SNOWFLAKE, CLICKHOUSE, MONGODB, SQLITE, REDIS, ORACLE, SPANNER, MSSQL, REDSHIFT, MARIADB, OCEANBASE.", }, "engine_version": { Type: schema.TypeString, diff --git a/provider/resource_review_config.go b/provider/resource_review_config.go new file mode 100644 index 0000000..3561dbd --- /dev/null +++ b/provider/resource_review_config.go @@ -0,0 +1,269 @@ +package provider + +import ( + "bytes" + "context" + "fmt" + "strings" + + v1pb "github.com/bytebase/bytebase/proto/generated-go/v1" + "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" + "github.com/pkg/errors" + + "github.com/bytebase/terraform-provider-bytebase/api" + "github.com/bytebase/terraform-provider-bytebase/provider/internal" +) + +func resourceReviewConfig() *schema.Resource { + return &schema.Resource{ + Description: "The review config resource.", + ReadContext: resourceReviewConfigRead, + DeleteContext: resourceReviewConfigDelete, + CreateContext: resourceReviewConfigUpsert, + UpdateContext: resourceReviewConfigUpsert, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: internal.ResourceIDValidation, + Description: "The unique resource id for the review config.", + }, + "title": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + Description: "The title for the review config.", + }, + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "Enable the SQL review config", + }, + "resources": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "Resources using the config. We support attach the review config for environments or projects with format {resurce}/{resource id}. For example, environments/test, projects/sample.", + }, + "rules": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Description: "The SQL review rules.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: "The rule unique type. Check https://www.bytebase.com/docs/sql-review/review-rules for all rules", + }, + "engine": { + Type: schema.TypeString, + Required: true, + Description: "The rule for the database engine.", + ValidateFunc: internal.EngineValidation, + }, + "level": { + Type: schema.TypeString, + Required: true, + Description: "The rule level.", + ValidateFunc: validation.StringInSlice([]string{ + v1pb.SQLReviewRuleLevel_WARNING.String(), + v1pb.SQLReviewRuleLevel_ERROR.String(), + v1pb.SQLReviewRuleLevel_DISABLED.String(), + }, false), + }, + "payload": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The payload for the rule.", + }, + "comment": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The comment for the rule.", + }, + }, + }, + Set: reviewRuleHash, + }, + }, + } +} + +func resourceReviewConfigRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + fullName := d.Id() + review, err := c.GetReviewConfig(ctx, fullName) + if err != nil { + return diag.FromErr(err) + } + + return setReviewConfig(d, review) +} + +func resourceReviewConfigDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + fullName := d.Id() + + // Warning or errors can be collected in a slice type + var diags diag.Diagnostics + + if err := c.DeleteReviewConfig(ctx, fullName); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return diags +} + +func resourceReviewConfigUpsert(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(api.Client) + + reviewID := d.Get("resource_id").(string) + reviewName := fmt.Sprintf("%s%s", internal.ReviewConfigNamePrefix, reviewID) + + rules, err := convertToV1RuleList(d) + if err != nil { + return diag.FromErr(err) + } + + reviewConfig := &v1pb.ReviewConfig{ + Name: reviewName, + Title: d.Get("title").(string), + Enabled: d.Get("enabled").(bool), + Rules: rules, + } + review, err := c.UpsertReviewConfig(ctx, reviewConfig, []string{ + "title", + "enabled", + "rules", + }) + if err != nil { + return diag.FromErr(err) + } + + patchTagPolicy(ctx, c, d, review.Name) + d.SetId(review.Name) + + return resourceReviewConfigRead(ctx, d, m) +} + +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 { + return nil + } + for _, raw := range rawSet.List() { + resource := raw.(string) + if !strings.HasPrefix(resource, internal.ProjectNamePrefix) && !strings.HasPrefix(resource, internal.EnvironmentNamePrefix) { + return diag.Errorf("invalid resource, only support projects/{id} or environments/{id}") + } + policyName := fmt.Sprintf("%s/%s%s", resource, internal.PolicyNamePrefix, v1pb.PolicyType_TAG.String()) + if _, err := client.UpsertPolicy(ctx, &v1pb.Policy{ + Name: policyName, + Enforce: true, + Type: v1pb.PolicyType_TAG, + Policy: &v1pb.Policy_TagPolicy{ + TagPolicy: &v1pb.TagPolicy{ + Tags: map[string]string{ + "bb.tag.review_config": reviewName, + }, + }, + }, + }, []string{"payload", "enforce"}); err != nil { + return diag.Errorf("failed to attach review config %s to resource %s with error: %v", reviewName, resource, err.Error()) + } + } + + return nil +} + +func setReviewConfig(d *schema.ResourceData, review *v1pb.ReviewConfig) diag.Diagnostics { + reviewID, err := internal.GetReviewConfigID(review.Name) + if err != nil { + return diag.Errorf("failed to parse id from review name %s with error: %v", review.Name, err.Error()) + } + + if err := d.Set("resource_id", reviewID); err != nil { + return diag.Errorf("cannot set resource_id for review: %s", err.Error()) + } + if err := d.Set("title", review.Title); err != nil { + return diag.Errorf("cannot set title for review: %s", err.Error()) + } + if err := d.Set("enabled", review.Enabled); err != nil { + return diag.Errorf("cannot set enabled for review: %s", err.Error()) + } + if err := d.Set("resources", review.Resources); err != nil { + return diag.Errorf("cannot set resources for review: %s", err.Error()) + } + if err := d.Set("rules", schema.NewSet(reviewRuleHash, flattenReviewRules(review.Rules))); err != nil { + return diag.Errorf("cannot set rules for review: %s", err.Error()) + } + + return nil +} + +func flattenReviewRules(rules []*v1pb.SQLReviewRule) []interface{} { + ruleList := []interface{}{} + for _, rule := range rules { + rawRule := map[string]interface{}{} + rawRule["type"] = rule.Type + rawRule["engine"] = rule.Engine.String() + rawRule["level"] = rule.Level.String() + rawRule["comment"] = rule.Comment + rawRule["payload"] = rule.Payload + ruleList = append(ruleList, rawRule) + } + return ruleList +} + +func reviewRuleHash(rawRule interface{}) int { + var buf bytes.Buffer + raw := rawRule.(map[string]interface{}) + + if v, ok := raw["type"].(string); ok { + _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + } + if v, ok := raw["engine"].(string); ok { + _, _ = buf.WriteString(fmt.Sprintf("%s-", v)) + } + + return internal.ToHashcodeInt(buf.String()) +} + +func convertToV1RuleList(d *schema.ResourceData) ([]*v1pb.SQLReviewRule, error) { + ruleSet, ok := d.Get("rules").(*schema.Set) + if !ok || ruleSet.Len() == 0 { + return nil, errors.Errorf("rules is required") + } + + ruleList := []*v1pb.SQLReviewRule{} + + for _, r := range ruleSet.List() { + rawRule := r.(map[string]interface{}) + payload := rawRule["payload"].(string) + if payload == "" { + payload = "{}" + } + ruleList = append(ruleList, &v1pb.SQLReviewRule{ + Type: rawRule["type"].(string), + Comment: rawRule["comment"].(string), + Payload: payload, + Engine: v1pb.Engine(v1pb.Engine_value[rawRule["engine"].(string)]), + Level: v1pb.SQLReviewRuleLevel(v1pb.SQLReviewRuleLevel_value[rawRule["level"].(string)]), + }) + } + return ruleList, nil +} diff --git a/provider/resource_user.go b/provider/resource_user.go index 66b9002..2248dfa 100644 --- a/provider/resource_user.go +++ b/provider/resource_user.go @@ -166,11 +166,11 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface Summary: "User is deleted", Detail: fmt.Sprintf("User %s already deleted, try to undelete the user", userName), }) - if _, err := c.UndeleteUser(ctx, userName); err != nil { + if _, err := c.UndeleteUser(ctx, existedUser.Name); err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Failed to undelete user", - Detail: fmt.Sprintf("Undelete user %s failed, error: %v", userName, err), + Detail: fmt.Sprintf("Undelete user %s failed, error: %v", existedUser.Name, err), }) return diags } @@ -230,7 +230,11 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface d.SetId(user.Name) } - if err := patchWorkspaceIAMPolicy(ctx, c, email, getRoles(d)); err != nil { + roles, diagnostic := getRoles(d) + if diagnostic != nil { + return diagnostic + } + if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("user:%s", email), roles); err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Failed to patch user roles", @@ -316,7 +320,11 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface } if d.HasChange("roles") { - if err := patchWorkspaceIAMPolicy(ctx, c, email, getRoles(d)); err != nil { + roles, diagnostic := getRoles(d) + if diagnostic != nil { + return diagnostic + } + if err := patchWorkspaceIAMPolicy(ctx, c, fmt.Sprintf("user:%s", email), roles); err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: "Failed to patch user roles", @@ -334,36 +342,39 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface return diags } -func getRoles(d *schema.ResourceData) []string { +func getRoles(d *schema.ResourceData) ([]string, diag.Diagnostics) { rawRoles := d.Get("roles").(*schema.Set) roleList := []string{} for _, rawRole := range rawRoles.List() { + role := rawRole.(string) + if !strings.HasPrefix(role, internal.RoleNamePrefix) { + return nil, diag.Errorf("role must in the roles/{id} format") + } roleList = append(roleList, rawRole.(string)) } - return roleList + return roleList, nil } -func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, email string, roles []string) error { +func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, member string, roles []string) error { workspaceIamPolicy, err := client.GetWorkspaceIAMPolicy(ctx) if err != nil { return errors.Errorf("cannot get workspace IAM with error: %s", err.Error()) } - patchMember := fmt.Sprintf("user:%s", email) roleMap := map[string]bool{} for _, role := range roles { roleMap[role] = true } for _, binding := range workspaceIamPolicy.Bindings { - index := slices.Index(binding.Members, patchMember) + index := slices.Index(binding.Members, member) if !roleMap[binding.Role] { if index >= 0 { binding.Members = slices.Delete(binding.Members, index, index+1) } } else { if index < 0 { - binding.Members = append(binding.Members, patchMember) + binding.Members = append(binding.Members, member) } } @@ -374,7 +385,7 @@ func patchWorkspaceIAMPolicy(ctx context.Context, client api.Client, email strin workspaceIamPolicy.Bindings = append(workspaceIamPolicy.Bindings, &v1pb.Binding{ Role: role, Members: []string{ - patchMember, + member, }, }) }