diff --git a/VERSION b/VERSION
index 2fa3901..1c2de38 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.0.22
\ No newline at end of file
+1.0.23
\ No newline at end of file
diff --git a/docs/data-sources/group.md b/docs/data-sources/group.md
index 571116c..6779230 100644
--- a/docs/data-sources/group.md
+++ b/docs/data-sources/group.md
@@ -24,7 +24,6 @@ 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 fb47f7b..a21bd4f 100644
--- a/docs/data-sources/group_list.md
+++ b/docs/data-sources/group_list.md
@@ -28,7 +28,6 @@ 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/iam_policy.md b/docs/data-sources/iam_policy.md
new file mode 100644
index 0000000..c05a213
--- /dev/null
+++ b/docs/data-sources/iam_policy.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "bytebase_iam_policy Data Source - terraform-provider-bytebase"
+subcategory: ""
+description: |-
+ The IAM policy data source.
+---
+
+# bytebase_iam_policy (Data Source)
+
+The IAM policy data source.
+
+
+
+
+## Schema
+
+### Required
+
+- `parent` (String) The IAM policy parent name for the policy, support "projects/{resource id}" or "workspaces/-"
+
+### Optional
+
+- `iam_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--iam_policy))
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `iam_policy`
+
+Optional:
+
+- `binding` (Block Set) The binding in the IAM policy. (see [below for nested schema](#nestedblock--iam_policy--binding))
+
+
+### Nested Schema for `iam_policy.binding`
+
+Optional:
+
+- `condition` (Block Set) Match the condition limit. (see [below for nested schema](#nestedblock--iam_policy--binding--condition))
+- `members` (Set of String) A set of memebers. The value can be "allUsers", "user:{email}" or "group:{email}".
+- `role` (String) The role full name in roles/{id} format.
+
+
+### Nested Schema for `iam_policy.binding.condition`
+
+Optional:
+
+- `database` (String) The accessible database full name in instances/{instance resource id}/databases/{database name} format
+- `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ssZ format
+- `row_limit` (Number) The export row limit for exporter role
+- `schema` (String) The accessible schema in the database
+- `tables` (Set of String) The accessible table list
+
+
diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md
index 4e07b32..42bdd48 100644
--- a/docs/data-sources/project.md
+++ b/docs/data-sources/project.md
@@ -27,30 +27,9 @@ The project data source.
- `databases` (Set of String) The databases full name in the resource.
- `enforce_issue_title` (Boolean) Enforce issue title created by user instead of generated by Bytebase.
- `id` (String) The ID of this resource.
-- `members` (Set of Object) The members in the project. (see [below for nested schema](#nestedatt--members))
- `name` (String) The project full name in projects/{resource id} format.
- `postgres_database_tenant_mode` (Boolean) Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended "set role " statement.
- `skip_backup_errors` (Boolean) Whether to skip backup errors and continue the data migration.
- `title` (String) The project title.
-
-### Nested Schema for `members`
-
-Read-Only:
-
-- `condition` (Set of Object) (see [below for nested schema](#nestedobjatt--members--condition))
-- `member` (String)
-- `role` (String)
-
-
-### Nested Schema for `members.condition`
-
-Read-Only:
-
-- `database` (String)
-- `expire_timestamp` (String)
-- `row_limit` (Number)
-- `schema` (String)
-- `tables` (Set of String)
-
diff --git a/docs/data-sources/project_list.md b/docs/data-sources/project_list.md
index 965c24c..07a03b6 100644
--- a/docs/data-sources/project_list.md
+++ b/docs/data-sources/project_list.md
@@ -36,31 +36,10 @@ Read-Only:
- `auto_resolve_issue` (Boolean)
- `databases` (Set of String)
- `enforce_issue_title` (Boolean)
-- `members` (Set of Object) (see [below for nested schema](#nestedobjatt--projects--members))
- `name` (String)
- `postgres_database_tenant_mode` (Boolean)
- `resource_id` (String)
- `skip_backup_errors` (Boolean)
- `title` (String)
-
-### Nested Schema for `projects.members`
-
-Read-Only:
-
-- `condition` (Set of Object) (see [below for nested schema](#nestedobjatt--projects--members--condition))
-- `member` (String)
-- `role` (String)
-
-
-### Nested Schema for `projects.members.condition`
-
-Read-Only:
-
-- `database` (String)
-- `expire_timestamp` (String)
-- `row_limit` (Number)
-- `schema` (String)
-- `tables` (Set of String)
-
diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md
index a51e7c7..381d6e1 100644
--- a/docs/data-sources/user.md
+++ b/docs/data-sources/user.md
@@ -27,7 +27,6 @@ The user data source.
- `last_login_time` (String) The user last login time.
- `mfa_enabled` (Boolean) The mfa_enabled flag means if the user has enabled MFA.
- `phone` (String) The user phone.
-- `roles` (Set of String) The user's roles in the workspace level
- `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.
- `title` (String) The user title.
diff --git a/docs/data-sources/user_list.md b/docs/data-sources/user_list.md
index 2d9ba4d..b1e2870 100644
--- a/docs/data-sources/user_list.md
+++ b/docs/data-sources/user_list.md
@@ -39,7 +39,6 @@ Read-Only:
- `mfa_enabled` (Boolean)
- `name` (String)
- `phone` (String)
-- `roles` (Set of String)
- `source` (String)
- `state` (String)
- `title` (String)
diff --git a/docs/resources/group.md b/docs/resources/group.md
index 84ed19f..daa7cd9 100644
--- a/docs/resources/group.md
+++ b/docs/resources/group.md
@@ -24,7 +24,6 @@ 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/iam_policy.md b/docs/resources/iam_policy.md
new file mode 100644
index 0000000..3457946
--- /dev/null
+++ b/docs/resources/iam_policy.md
@@ -0,0 +1,57 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "bytebase_iam_policy Resource - terraform-provider-bytebase"
+subcategory: ""
+description: |-
+ The IAM policy resource.
+---
+
+# bytebase_iam_policy (Resource)
+
+The IAM policy resource.
+
+
+
+
+## Schema
+
+### Required
+
+- `parent` (String) The IAM policy parent name for the policy, support "projects/{resource id}" or "workspaces/-"
+
+### Optional
+
+- `iam_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--iam_policy))
+
+### Read-Only
+
+- `id` (String) The ID of this resource.
+
+
+### Nested Schema for `iam_policy`
+
+Optional:
+
+- `binding` (Block Set) The binding in the IAM policy. (see [below for nested schema](#nestedblock--iam_policy--binding))
+
+
+### Nested Schema for `iam_policy.binding`
+
+Optional:
+
+- `condition` (Block Set) Match the condition limit. (see [below for nested schema](#nestedblock--iam_policy--binding--condition))
+- `members` (Set of String) A set of memebers. The value can be "allUsers", "user:{email}" or "group:{email}".
+- `role` (String) The role full name in roles/{id} format.
+
+
+### Nested Schema for `iam_policy.binding.condition`
+
+Optional:
+
+- `database` (String) The accessible database full name in instances/{instance resource id}/databases/{database name} format
+- `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ssZ format
+- `row_limit` (Number) The export row limit for exporter role
+- `schema` (String) The accessible schema in the database
+- `tables` (Set of String) The accessible table list
+
+
diff --git a/docs/resources/project.md b/docs/resources/project.md
index 5973d01..a03b88e 100644
--- a/docs/resources/project.md
+++ b/docs/resources/project.md
@@ -27,7 +27,6 @@ The project resource.
- `auto_resolve_issue` (Boolean) Enable auto resolve issue.
- `databases` (Set of String) The databases full name in the resource.
- `enforce_issue_title` (Boolean) Enforce issue title created by user instead of generated by Bytebase.
-- `members` (Block Set) The members in the project. (see [below for nested schema](#nestedblock--members))
- `postgres_database_tenant_mode` (Boolean) Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended "set role " statement.
- `skip_backup_errors` (Boolean) Whether to skip backup errors and continue the data migration.
@@ -36,24 +35,4 @@ The project resource.
- `id` (String) The ID of this resource.
- `name` (String) The project full name in projects/{resource id} format.
-
-### Nested Schema for `members`
-
-Optional:
-
-- `condition` (Block Set) Match the condition limit. (see [below for nested schema](#nestedblock--members--condition))
-- `member` (String) The member in user:{email} or group:{email} format.
-- `role` (String) The role full name in roles/{id} format.
-
-
-### Nested Schema for `members.condition`
-
-Optional:
-
-- `database` (String) The accessible database full name in instances/{instance resource id}/databases/{database name} format
-- `expire_timestamp` (String) The expiration timestamp in YYYY-MM-DDThh:mm:ssZ format
-- `row_limit` (Number) The export row limit for exporter role
-- `schema` (String) The accessible schema in the database
-- `tables` (Set of String) The accessible table list
-
diff --git a/docs/resources/user.md b/docs/resources/user.md
index 71d19a7..4bb327f 100644
--- a/docs/resources/user.md
+++ b/docs/resources/user.md
@@ -24,7 +24,6 @@ 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
diff --git a/examples/database/main.tf b/examples/database/main.tf
index d100e82..2ef4fe9 100644
--- a/examples/database/main.tf
+++ b/examples/database/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 62de4e5..655382f 100644
--- a/examples/environments/main.tf
+++ b/examples/environments/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 4d9014f..306b2ba 100644
--- a/examples/groups/main.tf
+++ b/examples/groups/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/iamPolicy/main.tf b/examples/iamPolicy/main.tf
new file mode 100644
index 0000000..8b1f4e0
--- /dev/null
+++ b/examples/iamPolicy/main.tf
@@ -0,0 +1,34 @@
+terraform {
+ required_providers {
+ bytebase = {
+ version = "1.0.23"
+ # 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_iam_policy" "workspace_iam" {
+ parent = "workspaces/-"
+}
+
+output "workspace_iam" {
+ value = data.bytebase_iam_policy.workspace_iam
+}
+
+data "bytebase_iam_policy" "project_iam" {
+ parent = "projects/project-sample"
+}
+
+output "project_iam" {
+ value = data.bytebase_iam_policy.project_iam
+}
diff --git a/examples/instances/main.tf b/examples/instances/main.tf
index 3793fe9..6ee057d 100644
--- a/examples/instances/main.tf
+++ b/examples/instances/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 b5b09dd..4660ef3 100644
--- a/examples/policies/main.tf
+++ b/examples/policies/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
@@ -27,7 +27,8 @@ output "masking_exception_policy" {
}
data "bytebase_policy" "global_masking_policy" {
- type = "MASKING_RULE"
+ parent = "workspaces/-"
+ type = "MASKING_RULE"
}
output "global_masking_policy" {
diff --git a/examples/projects/main.tf b/examples/projects/main.tf
index eafb10e..f45471a 100644
--- a/examples/projects/main.tf
+++ b/examples/projects/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 69fa358..b183285 100644
--- a/examples/roles/main.tf
+++ b/examples/roles/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 94c3fa4..a3cddd2 100644
--- a/examples/settings/main.tf
+++ b/examples/settings/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# For local development, please use "terraform.local/bytebase/bytebase" instead
source = "registry.terraform.io/bytebase/bytebase"
}
diff --git a/examples/setup/iam.tf b/examples/setup/iam.tf
new file mode 100644
index 0000000..1ebfdbc
--- /dev/null
+++ b/examples/setup/iam.tf
@@ -0,0 +1,88 @@
+# Workspace level IAM.
+resource "bytebase_iam_policy" "workspace_iam" {
+ depends_on = [
+ bytebase_user.workspace_dba,
+ bytebase_user.workspace_auditor,
+ bytebase_user.project_developer,
+ bytebase_user.service_account,
+ bytebase_group.developers,
+ bytebase_role.auditor
+ ]
+
+ parent = "workspaces/-"
+
+ iam_policy {
+ binding {
+ role = "roles/workspaceAdmin"
+ members = [
+ format("user:%s", local.service_account),
+ format("user:%s", bytebase_user.workspace_dba.email),
+ ]
+ }
+
+ binding {
+ role = "roles/workspaceDBA"
+ members = [
+ format("user:%s", bytebase_user.workspace_dba.email),
+ format("user:%s", bytebase_user.service_account.email),
+ ]
+ }
+
+ binding {
+ role = bytebase_role.auditor.name
+ members = [
+ format("user:%s", bytebase_user.workspace_auditor.email)
+ ]
+ }
+
+ binding {
+ role = "roles/projectViewer"
+ members = [
+ format("user:%s", bytebase_user.project_developer.email),
+ format("group:%s", bytebase_group.developers.email),
+ ]
+ }
+ }
+}
+
+# Project level IAM
+resource "bytebase_iam_policy" "project_iam" {
+ depends_on = [
+ bytebase_project.sample_project,
+ bytebase_user.workspace_dba,
+ bytebase_user.project_developer,
+ bytebase_group.developers
+ ]
+
+ parent = bytebase_project.sample_project.name
+
+ iam_policy {
+ binding {
+ role = "roles/projectOwner"
+ members = [
+ format("user:%s", bytebase_user.workspace_dba.email)
+ ]
+ }
+
+ binding {
+ role = "roles/projectDeveloper"
+ members = [
+ "allUsers",
+ format("group:%s", bytebase_group.developers.email)
+ ]
+ }
+
+ binding {
+ role = "roles/projectExporter"
+ members = [
+ format("user:%s", bytebase_user.project_developer.email)
+ ]
+ condition {
+ database = "instances/test-sample-instance/databases/employee"
+ tables = ["dept_emp", "dept_manager"]
+ row_limit = 10000
+ expire_timestamp = "2027-03-09T16:17:49Z"
+ }
+ }
+ }
+}
diff --git a/examples/setup/main.tf b/examples/setup/main.tf
index 079e548..0924ede 100644
--- a/examples/setup/main.tf
+++ b/examples/setup/main.tf
@@ -1,24 +1,16 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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"
-}
-
# Correspond to the sample data Bytebase generates during onboarding.
locals {
+ service_account = "terraform@service.bytebase.com"
environment_id_test = "test"
environment_id_prod = "prod"
instance_id_test = "test-sample-instance"
@@ -26,6 +18,16 @@ locals {
project_id = "project-sample"
}
+
+provider "bytebase" {
+ # You need to replace the account and key with your Bytebase service account.
+ service_account = local.service_account
+ 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"
+}
+
resource "bytebase_setting" "workspace_profile" {
name = "bb.workspace.profile"
diff --git a/examples/setup/project.tf b/examples/setup/project.tf
index 2cd13ff..688fd56 100644
--- a/examples/setup/project.tf
+++ b/examples/setup/project.tf
@@ -1,35 +1,11 @@
# Create or update sample project, and grant roles for users and groups.
resource "bytebase_project" "sample_project" {
depends_on = [
- bytebase_user.workspace_dba,
- bytebase_user.project_developer,
- bytebase_group.developers,
bytebase_instance.test
]
resource_id = local.project_id
title = "Sample project"
- members {
- member = format("user:%s", bytebase_user.workspace_dba.email)
- role = "roles/projectOwner"
- }
-
- members {
- member = format("group:%s", bytebase_group.developers.email)
- role = "roles/projectDeveloper"
- }
-
- members {
- member = format("user:%s", bytebase_user.project_developer.email)
- role = "roles/projectExporter"
- condition {
- database = "instances/test-sample-instance/databases/employee"
- tables = ["dept_emp", "dept_manager"]
- row_limit = 10000
- expire_timestamp = "2027-03-09T16:17:49Z"
- }
- }
-
databases = bytebase_instance.test.databases
}
diff --git a/examples/setup/users.tf b/examples/setup/users.tf
index c681f47..43fd6a4 100644
--- a/examples/setup/users.tf
+++ b/examples/setup/users.tf
@@ -2,54 +2,31 @@
resource "bytebase_user" "workspace_dba" {
title = "DBA"
email = "dba@bytebase.com"
-
- # Grant workspace level roles.
- roles = ["roles/workspaceDBA"]
}
# Create or update the user.
resource "bytebase_user" "workspace_auditor" {
- depends_on = [
- bytebase_user.workspace_dba,
- bytebase_role.auditor
- ]
title = "Auditor"
email = "auditor@bytebase.com"
-
- # Grant workspace level roles.
- roles = [bytebase_role.auditor.name]
}
# Create or update the user.
resource "bytebase_user" "project_developer" {
- depends_on = [
- bytebase_user.workspace_auditor
- ]
-
title = "Developer"
email = "developer@bytebase.com"
-
- # Grant workspace level roles, will grant projectViewer for this user in all
- 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
]
@@ -66,6 +43,4 @@ 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
index 93d5512..4328808 100644
--- a/examples/sql_review/main.tf
+++ b/examples/sql_review/main.tf
@@ -1,7 +1,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 8d219c3..5206a3f 100644
--- a/examples/users/main.tf
+++ b/examples/users/main.tf
@@ -2,7 +2,7 @@
terraform {
required_providers {
bytebase = {
- version = "1.0.22"
+ version = "1.0.23"
# 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 e343abc..9f11af5 100644
--- a/provider/data_source_group.go
+++ b/provider/data_source_group.go
@@ -1,6 +1,7 @@
package provider
import (
+ "bytes"
"context"
"fmt"
"regexp"
@@ -42,14 +43,6 @@ 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,
@@ -83,15 +76,10 @@ func dataSourceGroupRead(ctx context.Context, d *schema.ResourceData, m interfac
}
d.SetId(group.Name)
- return setGroup(ctx, c, d, group)
+ return setGroup(d, group)
}
-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())
- }
-
+func setGroup(d *schema.ResourceData, group *v1pb.Group) diag.Diagnostics {
if err := d.Set("name", group.Name); err != nil {
return diag.Errorf("cannot set name for group: %s", err.Error())
}
@@ -115,13 +103,20 @@ func setGroup(ctx context.Context, client api.Client, d *schema.ResourceData, gr
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)
+
+ return nil
+}
+
+func memberHash(rawMember interface{}) int {
+ var buf bytes.Buffer
+ member := rawMember.(map[string]interface{})
+
+ if v, ok := member["member"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
}
- 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())
+ if v, ok := member["role"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
}
- return nil
+ return internal.ToHashcodeInt(buf.String())
}
diff --git a/provider/data_source_group_list.go b/provider/data_source_group_list.go
index d597a07..3e320cc 100644
--- a/provider/data_source_group_list.go
+++ b/provider/data_source_group_list.go
@@ -2,7 +2,6 @@ package provider
import (
"context"
- "fmt"
"strconv"
"time"
@@ -10,7 +9,6 @@ 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 {
@@ -43,14 +41,6 @@ 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,
@@ -86,11 +76,6 @@ 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{})
@@ -108,11 +93,6 @@ func dataSourceGroupListRead(ctx context.Context, d *schema.ResourceData, m inte
}
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_iam_policy.go b/provider/data_source_iam_policy.go
new file mode 100644
index 0000000..299b1f2
--- /dev/null
+++ b/provider/data_source_iam_policy.go
@@ -0,0 +1,272 @@
+package provider
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/pkg/errors"
+
+ v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
+
+ "github.com/bytebase/terraform-provider-bytebase/api"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
+)
+
+func dataSourceIAMPolicy() *schema.Resource {
+ return &schema.Resource{
+ Description: "The IAM policy data source.",
+ ReadContext: dataSourceIAMPolicyRead,
+ Schema: map[string]*schema.Schema{
+ "parent": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateDiagFunc: internal.ResourceNameValidation(
+ // workspace policy
+ regexp.MustCompile("^workspaces/-$"),
+ // project policy
+ regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)),
+ ),
+ Description: `The IAM policy parent name for the policy, support "projects/{resource id}" or "workspaces/-"`,
+ },
+ "iam_policy": getIAMPolicySchema(true),
+ },
+ }
+}
+
+func getIAMPolicySchema(computed bool) *schema.Schema {
+ return &schema.Schema{
+ Computed: computed,
+ Optional: true,
+ Default: nil,
+ Type: schema.TypeList,
+ MinItems: 0,
+ MaxItems: 1,
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "binding": getIAMBindingSchema(false),
+ },
+ },
+ }
+}
+
+func getIAMBindingSchema(computed bool) *schema.Schema {
+ return &schema.Schema{
+ Type: schema.TypeSet,
+ Computed: computed,
+ Optional: !computed,
+ Description: "The binding in the IAM policy.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "role": {
+ Type: schema.TypeString,
+ Computed: computed,
+ Optional: !computed,
+ Description: "The role full name in roles/{id} format.",
+ },
+ "members": {
+ Type: schema.TypeSet,
+ Computed: computed,
+ Optional: !computed,
+ Description: `A set of memebers. The value can be "allUsers", "user:{email}" or "group:{email}".`,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ },
+ "condition": {
+ Type: schema.TypeSet,
+ Computed: computed,
+ Optional: true,
+ Description: "Match the condition limit.",
+ Elem: &schema.Resource{
+ Schema: map[string]*schema.Schema{
+ "database": {
+ Type: schema.TypeString,
+ Computed: computed,
+ Optional: true,
+ Description: "The accessible database full name in instances/{instance resource id}/databases/{database name} format",
+ },
+ "schema": {
+ Type: schema.TypeString,
+ Computed: computed,
+ Optional: true,
+ Description: "The accessible schema in the database",
+ },
+ "tables": {
+ Type: schema.TypeSet,
+ Computed: computed,
+ Optional: true,
+ Elem: &schema.Schema{
+ Type: schema.TypeString,
+ },
+ Set: schema.HashString,
+ Description: "The accessible table list",
+ },
+ "row_limit": {
+ Type: schema.TypeInt,
+ Computed: computed,
+ Optional: true,
+ Description: "The export row limit for exporter role",
+ },
+ "expire_timestamp": {
+ Type: schema.TypeString,
+ Computed: computed,
+ Optional: true,
+ Description: "The expiration timestamp in YYYY-MM-DDThh:mm:ssZ format",
+ },
+ },
+ },
+ Set: func(i interface{}) int {
+ return internal.ToHashcodeInt(conditionHash(i))
+ },
+ },
+ },
+ },
+ Set: bindingHash,
+ }
+}
+
+func dataSourceIAMPolicyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+ parent := d.Get("parent").(string)
+
+ var iamPolicy *v1pb.IamPolicy
+
+ if strings.HasPrefix(parent, internal.ProjectNamePrefix) {
+ projectIAM, err := c.GetProjectIAMPolicy(ctx, parent)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ iamPolicy = projectIAM
+ } else {
+ workspaceIAM, err := c.GetWorkspaceIAMPolicy(ctx)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ iamPolicy = workspaceIAM
+ }
+
+ d.SetId(parent)
+ return setIAMPolicyMessage(d, iamPolicy)
+}
+
+func setIAMPolicyMessage(d *schema.ResourceData, iamPolicy *v1pb.IamPolicy) diag.Diagnostics {
+ flattenPolicy, err := flattenIAMPolicy(iamPolicy)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ if err := d.Set("iam_policy", flattenPolicy); err != nil {
+ return diag.Errorf("cannot set iam_policy: %s", err.Error())
+ }
+ return nil
+}
+
+func flattenIAMPolicy(p *v1pb.IamPolicy) ([]interface{}, error) {
+ bindingList := []interface{}{}
+ for _, binding := range p.Bindings {
+ rawBinding := map[string]interface{}{}
+ rawCondition := map[string]interface{}{}
+ if condition := binding.Condition; condition != nil && condition.Expression != "" {
+ expressions := strings.Split(condition.Expression, " && ")
+ for _, expression := range expressions {
+ if strings.HasPrefix(expression, `resource.database == "`) {
+ rawCondition["database"] = strings.TrimSuffix(
+ strings.TrimPrefix(expression, `resource.database == "`),
+ `"`,
+ )
+ }
+ if strings.HasPrefix(expression, `resource.schema == "`) {
+ rawCondition["schema"] = strings.TrimSuffix(
+ strings.TrimPrefix(expression, `resource.schema == "`),
+ `"`,
+ )
+ }
+ if strings.HasPrefix(expression, `resource.table in [`) {
+ tableStr := strings.TrimSuffix(
+ strings.TrimPrefix(expression, `resource.table in [`),
+ `]`,
+ )
+ rawTableList := []string{}
+ for _, t := range strings.Split(tableStr, ",") {
+ rawTableList = append(rawTableList, strings.TrimSuffix(
+ strings.TrimPrefix(t, `"`),
+ `"`,
+ ))
+ }
+ rawCondition["tables"] = rawTableList
+ }
+ if strings.HasPrefix(expression, `request.row_limit <= `) {
+ i, err := strconv.Atoi(strings.TrimPrefix(expression, `request.row_limit <= `))
+ if err != nil {
+ return nil, errors.Errorf("cannot convert %s to int with error: %s", expression, err.Error())
+ }
+ rawCondition["row_limit"] = i
+ }
+ if strings.HasPrefix(expression, "request.time < ") {
+ rawCondition["expire_timestamp"] = strings.TrimSuffix(
+ strings.TrimPrefix(expression, `request.time < timestamp("`),
+ `")`,
+ )
+ }
+ }
+ }
+
+ rawBinding["condition"] = schema.NewSet(func(i interface{}) int {
+ return internal.ToHashcodeInt(conditionHash(i))
+ }, []interface{}{rawCondition})
+ rawBinding["role"] = binding.Role
+ rawBinding["members"] = binding.Members
+ bindingList = append(bindingList, rawBinding)
+ }
+
+ policy := map[string]interface{}{
+ "binding": schema.NewSet(bindingHash, bindingList),
+ }
+ return []interface{}{policy}, nil
+}
+
+func bindingHash(rawBinding interface{}) int {
+ var buf bytes.Buffer
+ binding := rawBinding.(map[string]interface{})
+
+ if v, ok := binding["role"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+
+ if condition, ok := binding["condition"].(*schema.Set); ok && condition.Len() > 0 && condition.List()[0] != nil {
+ rawCondition := condition.List()[0].(map[string]interface{})
+ _, _ = buf.WriteString(conditionHash(rawCondition))
+ }
+
+ return internal.ToHashcodeInt(buf.String())
+}
+
+func conditionHash(rawCondition interface{}) string {
+ var buf bytes.Buffer
+ condition := rawCondition.(map[string]interface{})
+
+ if v, ok := condition["database"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+ if v, ok := condition["schema"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+ if v, ok := condition["tables"].(*schema.Set); ok {
+ for _, t := range v.List() {
+ _, _ = buf.WriteString(fmt.Sprintf("table.%s-", t.(string)))
+ }
+ }
+ if v, ok := condition["row_limit"].(int); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%d-", v))
+ }
+ if v, ok := condition["expire_timestamp"].(string); ok {
+ _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
+ }
+
+ return buf.String()
+}
diff --git a/provider/data_source_policy.go b/provider/data_source_policy.go
index a94ada7..699cd9a 100644
--- a/provider/data_source_policy.go
+++ b/provider/data_source_policy.go
@@ -29,7 +29,7 @@ func dataSourcePolicy() *schema.Resource {
Default: "",
ValidateDiagFunc: internal.ResourceNameValidation(
// workspace policy
- regexp.MustCompile("^$"),
+ regexp.MustCompile("^workspaces/-$"),
// environment policy
regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.EnvironmentNamePrefix, internal.ResourceIDPattern)),
// instance policy
diff --git a/provider/data_source_policy_list.go b/provider/data_source_policy_list.go
index 809b39f..b4d3a5f 100644
--- a/provider/data_source_policy_list.go
+++ b/provider/data_source_policy_list.go
@@ -27,7 +27,7 @@ func dataSourcePolicyList() *schema.Resource {
Default: "",
ValidateDiagFunc: internal.ResourceNameValidation(
// workspace policy
- regexp.MustCompile("^$"),
+ regexp.MustCompile("^workspaces/-$"),
// environment policy
regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.EnvironmentNamePrefix, internal.ResourceIDPattern)),
// instance policy
diff --git a/provider/data_source_project.go b/provider/data_source_project.go
index fa2b710..9bc7e3a 100644
--- a/provider/data_source_project.go
+++ b/provider/data_source_project.go
@@ -1,17 +1,13 @@
package provider
import (
- "bytes"
"context"
"fmt"
- "strconv"
- "strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
- "github.com/pkg/errors"
"github.com/bytebase/terraform-provider-bytebase/api"
"github.com/bytebase/terraform-provider-bytebase/provider/internal"
@@ -70,7 +66,6 @@ func dataSourceProject() *schema.Resource {
Computed: true,
Description: "Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended \"set role \" statement.",
},
- "members": getProjectMembersSchema(true),
"databases": getDatabasesSchema(true),
},
}
@@ -88,79 +83,6 @@ func getDatabasesSchema(computed bool) *schema.Schema {
}
}
-func getProjectMembersSchema(computed bool) *schema.Schema {
- return &schema.Schema{
- Type: schema.TypeSet,
- Computed: computed,
- Optional: !computed,
- Description: "The members in the project.",
- Elem: &schema.Resource{
- Schema: map[string]*schema.Schema{
- "member": {
- Type: schema.TypeString,
- Computed: computed,
- Optional: !computed,
- Description: "The member in user:{email} or group:{email} format.",
- },
- "role": {
- Type: schema.TypeString,
- Computed: computed,
- Optional: !computed,
- Description: "The role full name in roles/{id} format.",
- },
- "condition": {
- Type: schema.TypeSet,
- Computed: computed,
- Optional: true,
- Description: "Match the condition limit.",
- Elem: &schema.Resource{
- Schema: map[string]*schema.Schema{
- "database": {
- Type: schema.TypeString,
- Computed: computed,
- Optional: true,
- Description: "The accessible database full name in instances/{instance resource id}/databases/{database name} format",
- },
- "schema": {
- Type: schema.TypeString,
- Computed: computed,
- Optional: true,
- Description: "The accessible schema in the database",
- },
- "tables": {
- Type: schema.TypeSet,
- Computed: computed,
- Optional: true,
- Elem: &schema.Schema{
- Type: schema.TypeString,
- },
- Set: schema.HashString,
- Description: "The accessible table list",
- },
- "row_limit": {
- Type: schema.TypeInt,
- Computed: computed,
- Optional: true,
- Description: "The export row limit for exporter role",
- },
- "expire_timestamp": {
- Type: schema.TypeString,
- Computed: computed,
- Optional: true,
- Description: "The expiration timestamp in YYYY-MM-DDThh:mm:ssZ format",
- },
- },
- },
- Set: func(i interface{}) int {
- return internal.ToHashcodeInt(conditionHash(i))
- },
- },
- },
- },
- Set: memberHash,
- }
-}
-
func dataSourceProjectRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
c := m.(api.Client)
projectName := fmt.Sprintf("%s%s", internal.ProjectNamePrefix, d.Get("resource_id").(string))
@@ -173,67 +95,6 @@ func dataSourceProjectRead(ctx context.Context, d *schema.ResourceData, m interf
return setProject(ctx, c, d, project)
}
-func flattenMemberList(iamPolicy *v1pb.IamPolicy) ([]interface{}, error) {
- memberList := []interface{}{}
- for _, binding := range iamPolicy.Bindings {
- rawCondition := map[string]interface{}{}
- if condition := binding.Condition; condition != nil && condition.Expression != "" {
- expressions := strings.Split(condition.Expression, " && ")
- for _, expression := range expressions {
- if strings.HasPrefix(expression, `resource.database == "`) {
- rawCondition["database"] = strings.TrimSuffix(
- strings.TrimPrefix(expression, `resource.database == "`),
- `"`,
- )
- }
- if strings.HasPrefix(expression, `resource.schema == "`) {
- rawCondition["schema"] = strings.TrimSuffix(
- strings.TrimPrefix(expression, `resource.schema == "`),
- `"`,
- )
- }
- if strings.HasPrefix(expression, `resource.table in [`) {
- tableStr := strings.TrimSuffix(
- strings.TrimPrefix(expression, `resource.table in [`),
- `]`,
- )
- rawTableList := []string{}
- for _, t := range strings.Split(tableStr, ",") {
- rawTableList = append(rawTableList, strings.TrimSuffix(
- strings.TrimPrefix(t, `"`),
- `"`,
- ))
- }
- rawCondition["tables"] = rawTableList
- }
- if strings.HasPrefix(expression, `request.row_limit <= `) {
- i, err := strconv.Atoi(strings.TrimPrefix(expression, `request.row_limit <= `))
- if err != nil {
- return nil, errors.Errorf("cannot convert %s to int with error: %s", expression, err.Error())
- }
- rawCondition["row_limit"] = i
- }
- if strings.HasPrefix(expression, "request.time < ") {
- rawCondition["expire_timestamp"] = strings.TrimSuffix(
- strings.TrimPrefix(expression, `request.time < timestamp("`),
- `")`,
- )
- }
- }
- }
- for _, member := range binding.Members {
- rawMember := map[string]interface{}{}
- rawMember["member"] = member
- rawMember["role"] = binding.Role
- rawMember["condition"] = schema.NewSet(func(i interface{}) int {
- return internal.ToHashcodeInt(conditionHash(i))
- }, []interface{}{rawCondition})
- memberList = append(memberList, rawMember)
- }
- }
- return memberList, nil
-}
-
func flattenDatabaseList(databases []*v1pb.Database) []interface{} {
dbList := []interface{}{}
for _, database := range databases {
@@ -257,11 +118,6 @@ func setProject(
return diag.FromErr(err)
}
- iamPolicy, err := client.GetProjectIAMPolicy(ctx, project.Name)
- if err != nil {
- return diag.Errorf("failed to get project iam with error: %v", err)
- }
-
d.SetId(project.Name)
projectID, err := internal.GetProjectID(project.Name)
@@ -310,64 +166,5 @@ func setProject(
"ms": time.Since(startTime).Milliseconds(),
})
- startTime = time.Now()
- memberList, err := flattenMemberList(iamPolicy)
- if err != nil {
- return diag.FromErr(err)
- }
- if err := d.Set("members", schema.NewSet(memberHash, memberList)); err != nil {
- return diag.Errorf("cannot set members for project: %s", err.Error())
- }
-
- tflog.Debug(ctx, "[read project] set project members", map[string]interface{}{
- "project": project.Name,
- "members": len(memberList),
- "ms": time.Since(startTime).Milliseconds(),
- })
-
return nil
}
-
-func memberHash(rawMember interface{}) int {
- var buf bytes.Buffer
- member := rawMember.(map[string]interface{})
-
- if v, ok := member["member"].(string); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
- }
- if v, ok := member["role"].(string); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
- }
-
- if condition, ok := member["condition"].(*schema.Set); ok && condition.Len() > 0 && condition.List()[0] != nil {
- rawCondition := condition.List()[0].(map[string]interface{})
- _, _ = buf.WriteString(conditionHash(rawCondition))
- }
-
- return internal.ToHashcodeInt(buf.String())
-}
-
-func conditionHash(rawCondition interface{}) string {
- var buf bytes.Buffer
- condition := rawCondition.(map[string]interface{})
-
- if v, ok := condition["database"].(string); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
- }
- if v, ok := condition["schema"].(string); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
- }
- if v, ok := condition["tables"].(*schema.Set); ok {
- for _, t := range v.List() {
- _, _ = buf.WriteString(fmt.Sprintf("table.%s-", t.(string)))
- }
- }
- if v, ok := condition["row_limit"].(int); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%d-", v))
- }
- if v, ok := condition["expire_timestamp"].(string); ok {
- _, _ = buf.WriteString(fmt.Sprintf("%s-", v))
- }
-
- return buf.String()
-}
diff --git a/provider/data_source_project_list.go b/provider/data_source_project_list.go
index 050e694..2f6a24f 100644
--- a/provider/data_source_project_list.go
+++ b/provider/data_source_project_list.go
@@ -90,7 +90,6 @@ func dataSourceProjectList() *schema.Resource {
Computed: true,
Description: "Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended \"set role \" statement.",
},
- "members": getProjectMembersSchema(true),
"databases": getDatabasesSchema(true),
},
},
@@ -146,16 +145,6 @@ func dataSourceProjectListRead(ctx context.Context, d *schema.ResourceData, m in
databaseList := flattenDatabaseList(databases)
proj["databases"] = databaseList
- iamPolicy, err := c.GetProjectIAMPolicy(ctx, project.Name)
- if err != nil {
- return diag.Errorf("failed to get project iam with error: %v", err)
- }
- memberList, err := flattenMemberList(iamPolicy)
- if err != nil {
- return diag.FromErr(err)
- }
- proj["members"] = schema.NewSet(memberHash, memberList)
-
projects = append(projects, proj)
}
diff --git a/provider/data_source_setting.go b/provider/data_source_setting.go
index 4886711..65dae93 100644
--- a/provider/data_source_setting.go
+++ b/provider/data_source_setting.go
@@ -331,6 +331,7 @@ func getWorkspaceProfileSetting(computed bool) *schema.Schema {
}
}
+// TODO(ed): API changed.
func getWorkspaceApprovalSetting(computed bool) *schema.Schema {
return &schema.Schema{
Computed: computed,
diff --git a/provider/data_source_user.go b/provider/data_source_user.go
index 644a060..cdc3ecb 100644
--- a/provider/data_source_user.go
+++ b/provider/data_source_user.go
@@ -43,14 +43,6 @@ func dataSourceUser() *schema.Resource {
Computed: true,
Description: "The user phone.",
},
- "roles": {
- Type: schema.TypeSet,
- Computed: true,
- Elem: &schema.Schema{
- Type: schema.TypeString,
- },
- Description: "The user's roles in the workspace level",
- },
"type": {
Type: schema.TypeString,
Computed: true,
@@ -96,28 +88,10 @@ func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, m interface
d.SetId(user.Name)
- return setUser(ctx, c, d, user)
-}
-
-func getRolesInIAM(iamPolicy *v1pb.IamPolicy, memberBinding string) []string {
- roles := []string{}
-
- for _, binding := range iamPolicy.Bindings {
- for _, member := range binding.Members {
- if member == memberBinding {
- roles = append(roles, binding.Role)
- }
- }
- }
- return roles
+ return setUser(d, user)
}
-func setUser(ctx context.Context, client api.Client, d *schema.ResourceData, user *v1pb.User) diag.Diagnostics {
- workspaceIAM, err := client.GetWorkspaceIAMPolicy(ctx)
- if err != nil {
- return diag.Errorf("cannot get workspace IAM with error: %s", err.Error())
- }
-
+func setUser(d *schema.ResourceData, user *v1pb.User) diag.Diagnostics {
if err := d.Set("title", user.Title); err != nil {
return diag.Errorf("cannot set title for user: %s", err.Error())
}
@@ -152,9 +126,5 @@ 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", getRolesInIAM(workspaceIAM, fmt.Sprintf("user:%s", user.Email))); err != nil {
- return diag.Errorf("cannot set roles for user: %s", err.Error())
- }
-
return nil
}
diff --git a/provider/data_source_user_list.go b/provider/data_source_user_list.go
index f928233..d5332da 100644
--- a/provider/data_source_user_list.go
+++ b/provider/data_source_user_list.go
@@ -91,14 +91,6 @@ func dataSourceUserList() *schema.Resource {
Computed: true,
Description: "The user type.",
},
- "roles": {
- Type: schema.TypeSet,
- Computed: true,
- Elem: &schema.Schema{
- Type: schema.TypeString,
- },
- Description: "The user's roles in the workspace level",
- },
"mfa_enabled": {
Type: schema.TypeBool,
Computed: true,
@@ -159,11 +151,6 @@ func dataSourceUserListRead(ctx context.Context, d *schema.ResourceData, m inter
return diag.FromErr(err)
}
- workspaceIAM, err := c.GetWorkspaceIAMPolicy(ctx)
- if err != nil {
- return diag.Errorf("cannot get workspace IAM with error: %s", err.Error())
- }
-
users := make([]map[string]interface{}, 0)
for _, user := range allUsers {
raw := make(map[string]interface{})
@@ -179,7 +166,6 @@ 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"] = 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/utils.go b/provider/internal/utils.go
index 44a1600..24a9e8d 100644
--- a/provider/internal/utils.go
+++ b/provider/internal/utils.go
@@ -229,6 +229,9 @@ func getNameParentTokens(name string, tokenPrefixes ...string) ([]string, error)
// ValidateMemberBinding checks the member binding format.
func ValidateMemberBinding(member string) error {
+ if member == "allUsers" {
+ return nil
+ }
if !strings.HasPrefix(member, "user:") && !strings.HasPrefix(member, "group:") {
return errors.Errorf("invalid member format")
}
diff --git a/provider/provider.go b/provider/provider.go
index dc49d1c..5d39023 100644
--- a/provider/provider.go
+++ b/provider/provider.go
@@ -68,6 +68,7 @@ func NewProvider() *schema.Provider {
"bytebase_database_list": dataSourceDatabaseList(),
"bytebase_review_config": dataSourceReviewConfig(),
"bytebase_review_config_list": dataSourceReviewConfigList(),
+ "bytebase_iam_policy": dataSourceIAMPolicy(),
},
ResourcesMap: map[string]*schema.Resource{
"bytebase_environment": resourceEnvironment(),
@@ -80,6 +81,7 @@ func NewProvider() *schema.Provider {
"bytebase_group": resourceGroup(),
"bytebase_database": resourceDatabase(),
"bytebase_review_config": resourceReviewConfig(),
+ "bytebase_iam_policy": resourceIAMPolicy(),
},
}
}
diff --git a/provider/resource_environment.go b/provider/resource_environment.go
index 35e08d6..9a017b3 100644
--- a/provider/resource_environment.go
+++ b/provider/resource_environment.go
@@ -18,6 +18,7 @@ import (
var environmentTitleRegex = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
+// TODO(ed): API changed.
func resourceEnvironment() *schema.Resource {
return &schema.Resource{
Description: "The environment resource.",
diff --git a/provider/resource_group.go b/provider/resource_group.go
index cb9bb69..b6c7e4d 100644
--- a/provider/resource_group.go
+++ b/provider/resource_group.go
@@ -55,14 +55,6 @@ 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,
@@ -104,7 +96,7 @@ func resourceGroupRead(ctx context.Context, d *schema.ResourceData, m interface{
return diag.FromErr(err)
}
- return setGroup(ctx, c, d, group)
+ return setGroup(d, group)
}
func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -180,19 +172,6 @@ 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)
@@ -246,22 +225,6 @@ 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_iam_policy.go b/provider/resource_iam_policy.go
new file mode 100644
index 0000000..186f361
--- /dev/null
+++ b/provider/resource_iam_policy.go
@@ -0,0 +1,163 @@
+package provider
+
+import (
+ "context"
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+ "github.com/pkg/errors"
+ "google.golang.org/genproto/googleapis/type/expr"
+
+ v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
+
+ "github.com/bytebase/terraform-provider-bytebase/api"
+ "github.com/bytebase/terraform-provider-bytebase/provider/internal"
+)
+
+func resourceIAMPolicy() *schema.Resource {
+ return &schema.Resource{
+ Description: "The IAM policy resource.",
+ CreateContext: resourceIAMPolicyUpsert,
+ ReadContext: dataSourceIAMPolicyRead,
+ UpdateContext: resourceIAMPolicyUpsert,
+ DeleteContext: resourceIAMPolicyDelete,
+ Importer: &schema.ResourceImporter{
+ StateContext: schema.ImportStatePassthroughContext,
+ },
+ Schema: map[string]*schema.Schema{
+ "parent": {
+ Type: schema.TypeString,
+ Required: true,
+ ValidateDiagFunc: internal.ResourceNameValidation(
+ // workspace policy
+ regexp.MustCompile("^workspaces/-$"),
+ // project policy
+ regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.ProjectNamePrefix, internal.ResourceIDPattern)),
+ ),
+ Description: `The IAM policy parent name for the policy, support "projects/{resource id}" or "workspaces/-"`,
+ },
+ "iam_policy": getIAMPolicySchema(false),
+ },
+ }
+}
+
+func resourceIAMPolicyUpsert(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
+ c := m.(api.Client)
+ parent := d.Get("parent").(string)
+
+ iamPolicy, err := convertToIAMPolicy(d)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ request := &v1pb.SetIamPolicyRequest{
+ Resource: parent,
+ Policy: iamPolicy,
+ }
+ if strings.HasPrefix(parent, internal.ProjectNamePrefix) {
+ if _, err := c.SetProjectIAMPolicy(ctx, parent, request); err != nil {
+ return diag.FromErr(err)
+ }
+ } else {
+ if _, err := c.SetWorkspaceIAMPolicy(ctx, request); err != nil {
+ return diag.FromErr(err)
+ }
+ }
+ d.SetId(parent)
+ return dataSourceIAMPolicyRead(ctx, d, m)
+}
+
+func resourceIAMPolicyDelete(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics {
+ var diags diag.Diagnostics
+
+ diags = append(diags, diag.Diagnostic{
+ Severity: diag.Warning,
+ Summary: "Unsupport delete IAM policy",
+ })
+ d.SetId("")
+
+ return diags
+}
+
+func convertToIAMPolicy(d *schema.ResourceData) (*v1pb.IamPolicy, error) {
+ rawList, ok := d.Get("iam_policy").([]interface{})
+ if !ok || len(rawList) != 1 {
+ return nil, errors.Errorf("invalid iam_policy")
+ }
+
+ raw := rawList[0].(map[string]interface{})
+ bindingList, ok := raw["binding"].(*schema.Set)
+ if !ok {
+ return nil, errors.Errorf("invalid binding")
+ }
+
+ policy := &v1pb.IamPolicy{}
+
+ for _, binding := range bindingList.List() {
+ rawBinding := binding.(map[string]interface{})
+
+ role := rawBinding["role"].(string)
+ if !strings.HasPrefix(role, internal.RoleNamePrefix) {
+ return nil, errors.Errorf("invalid role format, role must in roles/{id} format")
+ }
+
+ binding := &v1pb.Binding{
+ Role: role,
+ }
+
+ members, ok := rawBinding["members"].(*schema.Set)
+ if !ok {
+ return nil, errors.Errorf("invalid members")
+ }
+ if members.Len() == 0 {
+ return nil, errors.Errorf("empty members")
+ }
+ for _, member := range members.List() {
+ if err := internal.ValidateMemberBinding(member.(string)); err != nil {
+ return nil, errors.Wrapf(err, "invalid member: %v", member)
+ }
+ binding.Members = append(binding.Members, member.(string))
+ }
+
+ expressions := []string{}
+ if condition, ok := rawBinding["condition"].(*schema.Set); ok {
+ if condition.Len() > 1 {
+ return nil, errors.Errorf("should only set one condition")
+ }
+ if condition.Len() == 1 && condition.List()[0] != nil {
+ rawCondition := condition.List()[0].(map[string]interface{})
+ if database, ok := rawCondition["database"].(string); ok && database != "" {
+ expressions = append(expressions, fmt.Sprintf(`resource.database == "%s"`, database))
+ }
+ if schema, ok := rawCondition["schema"].(string); ok {
+ expressions = append(expressions, fmt.Sprintf(`resource.schema == "%s"`, schema))
+ }
+ if tables, ok := rawCondition["tables"].(*schema.Set); ok && tables.Len() > 0 {
+ tableList := []string{}
+ for _, table := range tables.List() {
+ tableList = append(tableList, fmt.Sprintf(`"%s"`, table.(string)))
+ }
+ expressions = append(expressions, fmt.Sprintf(`resource.table in [%s]`, strings.Join(tableList, ",")))
+ }
+ if rowLimit, ok := rawCondition["row_limit"].(int); ok && rowLimit > 0 {
+ expressions = append(expressions, fmt.Sprintf(`request.row_limit <= %d`, rowLimit))
+ }
+ if expire, ok := rawCondition["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)))
+ }
+ }
+ }
+ binding.Condition = &expr.Expr{
+ Expression: strings.Join(expressions, " && "),
+ }
+ policy.Bindings = append(policy.Bindings, binding)
+ }
+ return policy, nil
+}
diff --git a/provider/resource_policy.go b/provider/resource_policy.go
index c9bc8d3..d9ad87c 100644
--- a/provider/resource_policy.go
+++ b/provider/resource_policy.go
@@ -35,7 +35,7 @@ func resourcePolicy() *schema.Resource {
Required: true,
ValidateDiagFunc: internal.ResourceNameValidation(
// workspace policy
- regexp.MustCompile("^$"),
+ regexp.MustCompile("^workspaces/-$"),
// environment policy
regexp.MustCompile(fmt.Sprintf("^%s%s$", internal.EnvironmentNamePrefix, internal.ResourceIDPattern)),
// instance policy
@@ -303,6 +303,9 @@ func convertToMaskingExceptionPolicy(d *schema.ResourceData) (*v1pb.MaskingExcep
expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339)))
}
member := rawException["member"].(string)
+ if member == "allUsers" {
+ return nil, errors.Errorf("not support allUsers in masking_exception_policy")
+ }
if err := internal.ValidateMemberBinding(member); err != nil {
return nil, err
}
diff --git a/provider/resource_project.go b/provider/resource_project.go
index c879065..9de0d6d 100644
--- a/provider/resource_project.go
+++ b/provider/resource_project.go
@@ -3,15 +3,12 @@ package provider
import (
"context"
"fmt"
- "strings"
"time"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
- "github.com/pkg/errors"
- "google.golang.org/genproto/googleapis/type/expr"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/bytebase/terraform-provider-bytebase/api"
@@ -87,7 +84,6 @@ func resourceProjct() *schema.Resource {
Computed: true,
Description: "Whether to enable the database tenant mode for PostgreSQL. If enabled, the issue will be created with the pre-appended \"set role \" statement.",
},
- "members": getProjectMembersSchema(false),
"databases": getDatabasesSchema(false),
},
}
@@ -202,11 +198,6 @@ func resourceProjectCreate(ctx context.Context, d *schema.ResourceData, m interf
return diags
}
- if diag := updateMembersInProject(ctx, d, c, d.Id()); diag != nil {
- diags = append(diags, diag...)
- return diags
- }
-
tflog.Debug(ctx, "[upsert project] start reading project", map[string]interface{}{
"project": projectName,
})
@@ -307,13 +298,6 @@ func resourceProjectUpdate(ctx context.Context, d *schema.ResourceData, m interf
}
}
- if d.HasChange("members") {
- if diag := updateMembersInProject(ctx, d, c, d.Id()); diag != nil {
- diags = append(diags, diag...)
- return diags
- }
- }
-
diag := resourceProjectRead(ctx, d, m)
if diag != nil {
diags = append(diags, diag...)
@@ -355,88 +339,6 @@ func resourceProjectDelete(ctx context.Context, d *schema.ResourceData, m interf
return diags
}
-func updateMembersInProject(ctx context.Context, d *schema.ResourceData, client api.Client, projectName string) diag.Diagnostics {
- memberSet, ok := d.Get("members").(*schema.Set)
- if !ok {
- return nil
- }
-
- iamPolicy := &v1pb.IamPolicy{}
- existProjectOwner := false
-
- for _, m := range memberSet.List() {
- rawMember := m.(map[string]interface{})
- expressions := []string{}
-
- if condition, ok := rawMember["condition"].(*schema.Set); ok {
- if condition.Len() > 1 {
- return diag.Errorf("should only set one condition")
- }
- if condition.Len() == 1 && condition.List()[0] != nil {
- rawCondition := condition.List()[0].(map[string]interface{})
- if database, ok := rawCondition["database"].(string); ok && database != "" {
- expressions = append(expressions, fmt.Sprintf(`resource.database == "%s"`, database))
- }
- if schema, ok := rawCondition["schema"].(string); ok {
- expressions = append(expressions, fmt.Sprintf(`resource.schema == "%s"`, schema))
- }
- if tables, ok := rawCondition["tables"].(*schema.Set); ok && tables.Len() > 0 {
- tableList := []string{}
- for _, table := range tables.List() {
- tableList = append(tableList, fmt.Sprintf(`"%s"`, table.(string)))
- }
- expressions = append(expressions, fmt.Sprintf(`resource.table in [%s]`, strings.Join(tableList, ",")))
- }
- if rowLimit, ok := rawCondition["row_limit"].(int); ok && rowLimit > 0 {
- expressions = append(expressions, fmt.Sprintf(`request.row_limit <= %d`, rowLimit))
- }
- if expire, ok := rawCondition["expire_timestamp"].(string); ok && expire != "" {
- formattedTime, err := time.Parse(time.RFC3339, expire)
- if err != nil {
- return diag.FromErr(errors.Wrapf(err, "invalid time: %v", expire))
- }
- expressions = append(expressions, fmt.Sprintf(`request.time < timestamp("%s")`, formattedTime.Format(time.RFC3339)))
- }
- }
- }
-
- member := rawMember["member"].(string)
- role := rawMember["role"].(string)
- if role == "roles/projectOwner" {
- existProjectOwner = true
- }
-
- if err := internal.ValidateMemberBinding(member); err != nil {
- return diag.FromErr(err)
- }
- if !strings.HasPrefix(role, internal.RoleNamePrefix) {
- return diag.Errorf("invalid role format")
- }
-
- iamPolicy.Bindings = append(iamPolicy.Bindings, &v1pb.Binding{
- Members: []string{member},
- Role: role,
- Condition: &expr.Expr{
- Expression: strings.Join(expressions, " && "),
- },
- })
- }
-
- if len(iamPolicy.Bindings) > 0 {
- if !existProjectOwner {
- return diag.Errorf("require at least 1 member with roles/projectOwner role")
- }
-
- if _, err := client.SetProjectIAMPolicy(ctx, projectName, &v1pb.SetIamPolicyRequest{
- Policy: iamPolicy,
- Etag: iamPolicy.Etag,
- }); err != nil {
- return diag.Errorf("failed to update iam for project %s with error: %v", projectName, err.Error())
- }
- }
- return nil
-}
-
const batchSize = 100
func updateDatabasesInProject(ctx context.Context, d *schema.ResourceData, client api.Client, projectName string) diag.Diagnostics {
diff --git a/provider/resource_project_test.go b/provider/resource_project_test.go
index 2add3a5..04e931a 100644
--- a/provider/resource_project_test.go
+++ b/provider/resource_project_test.go
@@ -72,11 +72,6 @@ func testAccCheckProjectResource(identifier, resourceID, title string) string {
resource "bytebase_project" "%s" {
resource_id = "%s"
title = "%s"
-
- members {
- member = "user:mock@bytebase.com"
- role = "roles/projectOwner"
- }
}
`, identifier, resourceID, title)
}
diff --git a/provider/resource_user.go b/provider/resource_user.go
index 2248dfa..d7c0503 100644
--- a/provider/resource_user.go
+++ b/provider/resource_user.go
@@ -3,7 +3,6 @@ package provider
import (
"context"
"fmt"
- "slices"
"strings"
v1pb "github.com/bytebase/bytebase/proto/generated-go/v1"
@@ -11,7 +10,6 @@ import (
"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"
@@ -57,14 +55,6 @@ func resourceUser() *schema.Resource {
Computed: true,
Description: "The service key for service account.",
},
- "roles": {
- Type: schema.TypeSet,
- Optional: true,
- Elem: &schema.Schema{
- Type: schema.TypeString,
- },
- Description: "The user's roles in the workspace level",
- },
"type": {
Type: schema.TypeString,
Optional: true,
@@ -118,7 +108,7 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{}
return diag.FromErr(err)
}
- return setUser(ctx, c, d, user)
+ return setUser(d, user)
}
func resourceUserDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
@@ -230,19 +220,6 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface
d.SetId(user.Name)
}
- 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",
- Detail: fmt.Sprintf("Update roles for user %s failed, error: %v", userName, err),
- })
- return diags
- }
-
diag := resourceUserRead(ctx, d, m)
if diag != nil {
diags = append(diags, diag...)
@@ -319,21 +296,6 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface
}
}
- if d.HasChange("roles") {
- 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",
- Detail: fmt.Sprintf("Update roles for user %s failed, error: %v", userName, err),
- })
- return diags
- }
- }
-
diag := resourceUserRead(ctx, d, m)
if diag != nil {
diags = append(diags, diag...)
@@ -341,61 +303,3 @@ func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface
return diags
}
-
-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, nil
-}
-
-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())
- }
- roleMap := map[string]bool{}
- for _, role := range roles {
- roleMap[role] = true
- }
-
- for _, binding := range workspaceIamPolicy.Bindings {
- 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, member)
- }
- }
-
- delete(roleMap, binding.Role)
- }
-
- for role := range roleMap {
- workspaceIamPolicy.Bindings = append(workspaceIamPolicy.Bindings, &v1pb.Binding{
- Role: role,
- Members: []string{
- member,
- },
- })
- }
-
- if _, err := client.SetWorkspaceIAMPolicy(ctx, &v1pb.SetIamPolicyRequest{
- Policy: workspaceIamPolicy,
- Etag: workspaceIamPolicy.Etag,
- }); err != nil {
- return err
- }
-
- return nil
-}