diff --git a/modules/gcs/README.md b/modules/gcs/README.md
index e79de52d6..d35d28ea9 100644
--- a/modules/gcs/README.md
+++ b/modules/gcs/README.md
@@ -9,14 +9,11 @@ module "bucket" {
prefix = "test"
name = "my-bucket"
versioning = true
- iam = {
- "roles/storage.admin" = ["group:storage@example.com"]
- }
labels = {
cost-center = "devops"
}
}
-# tftest modules=1 resources=2 inventory=simple.yaml
+# tftest modules=1 resources=1 inventory=simple.yaml
```
### Example with Cloud KMS
@@ -108,29 +105,99 @@ module "bucket" {
}
# tftest modules=1 resources=2 inventory=object-upload.yaml
```
+
+### Examples of IAM
+
+```hcl
+module "bucket" {
+ source = "./fabric/modules/gcs"
+ project_id = "myproject"
+ name = "my-bucket"
+ iam = {
+ "roles/storage.admin" = ["group:storage@example.com"]
+ }
+}
+# tftest modules=1 resources=2 inventory=iam-authoritative.yaml
+```
+
+```hcl
+module "bucket" {
+ source = "./fabric/modules/gcs"
+ project_id = "myproject"
+ name = "my-bucket"
+ iam_bindings = {
+ storage-admin-with-delegated_roles = {
+ role = "roles/storage.admin"
+ members = ["group:storage@example.com"]
+ condition = {
+ title = "delegated-role-grants"
+ expression = format(
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ join(",", formatlist("'%s'",
+ [
+ "roles/storage.objectAdmin",
+ "roles/storage.objectViewer",
+ ]
+ ))
+ )
+ }
+ }
+ }
+}
+# tftest modules=1 resources=2 inventory=iam-bindings.yaml
+```
+
+```hcl
+module "bucket" {
+ source = "./fabric/modules/gcs"
+ project_id = "myproject"
+ name = "my-bucket"
+ iam_bindings_additive = {
+ storage-admin-with-delegated_roles = {
+ role = "roles/storage.admin"
+ member = "group:storage@example.com"
+ condition = {
+ title = "delegated-role-grants"
+ expression = format(
+ "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
+ join(",", formatlist("'%s'",
+ [
+ "roles/storage.objectAdmin",
+ "roles/storage.objectViewer",
+ ]
+ ))
+ )
+ }
+ }
+ }
+}
+# tftest modules=1 resources=2 inventory=iam-bindings-additive.yaml
+```
## Variables
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [name](variables.tf#L116) | Bucket name suffix. | string | ✓ | |
-| [project_id](variables.tf#L171) | Bucket project id. | string | ✓ | |
+| [name](variables.tf#L146) | Bucket name suffix. | string | ✓ | |
+| [project_id](variables.tf#L201) | Bucket project id. | string | ✓ | |
| [cors](variables.tf#L17) | CORS configuration for the bucket. Defaults to null. | object({…}) | | null |
| [encryption_key](variables.tf#L28) | KMS key that will be used for encryption. | string | | null |
| [force_destroy](variables.tf#L34) | Optional map to set force destroy keyed by name, defaults to false. | bool | | false |
| [iam](variables.tf#L40) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [labels](variables.tf#L46) | Labels to be attached to all buckets. | map(string) | | {} |
-| [lifecycle_rules](variables.tf#L52) | Bucket lifecycle rule. | map(object({…})) | | {} |
-| [location](variables.tf#L101) | Bucket location. | string | | "EU" |
-| [logging_config](variables.tf#L107) | Bucket logging configuration. | object({…}) | | null |
-| [notification_config](variables.tf#L121) | GCS Notification configuration. | object({…}) | | null |
-| [objects_to_upload](variables.tf#L135) | Objects to be uploaded to bucket. | map(object({…})) | | {} |
-| [prefix](variables.tf#L161) | Optional prefix used to generate the bucket name. | string | | null |
-| [retention_policy](variables.tf#L176) | Bucket retention policy. | object({…}) | | null |
-| [storage_class](variables.tf#L185) | Bucket storage class. | string | | "MULTI_REGIONAL" |
-| [uniform_bucket_level_access](variables.tf#L195) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true |
-| [versioning](variables.tf#L201) | Enable versioning, defaults to false. | bool | | false |
-| [website](variables.tf#L207) | Bucket website. | object({…}) | | null |
+| [iam_bindings](variables.tf#L46) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables.tf#L61) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [labels](variables.tf#L76) | Labels to be attached to all buckets. | map(string) | | {} |
+| [lifecycle_rules](variables.tf#L82) | Bucket lifecycle rule. | map(object({…})) | | {} |
+| [location](variables.tf#L131) | Bucket location. | string | | "EU" |
+| [logging_config](variables.tf#L137) | Bucket logging configuration. | object({…}) | | null |
+| [notification_config](variables.tf#L151) | GCS Notification configuration. | object({…}) | | null |
+| [objects_to_upload](variables.tf#L165) | Objects to be uploaded to bucket. | map(object({…})) | | {} |
+| [prefix](variables.tf#L191) | Optional prefix used to generate the bucket name. | string | | null |
+| [retention_policy](variables.tf#L206) | Bucket retention policy. | object({…}) | | null |
+| [storage_class](variables.tf#L215) | Bucket storage class. | string | | "MULTI_REGIONAL" |
+| [uniform_bucket_level_access](variables.tf#L225) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | bool | | true |
+| [versioning](variables.tf#L231) | Enable versioning, defaults to false. | bool | | false |
+| [website](variables.tf#L237) | Bucket website. | object({…}) | | null |
## Outputs
diff --git a/modules/gcs/iam.tf b/modules/gcs/iam.tf
new file mode 100644
index 000000000..42bad0288
--- /dev/null
+++ b/modules/gcs/iam.tf
@@ -0,0 +1,54 @@
+/**
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+# tfdoc:file:description IAM bindings
+
+resource "google_storage_bucket_iam_binding" "authoritative" {
+ for_each = var.iam
+ bucket = google_storage_bucket.bucket.name
+ role = each.key
+ members = each.value
+}
+
+resource "google_storage_bucket_iam_binding" "bindings" {
+ for_each = var.iam_bindings
+ bucket = google_storage_bucket.bucket.name
+ role = each.value.role
+ members = each.value.members
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
+
+resource "google_storage_bucket_iam_member" "bindings" {
+ for_each = var.iam_bindings_additive
+ bucket = google_storage_bucket.bucket.name
+ role = each.value.role
+ member = each.value.member
+ dynamic "condition" {
+ for_each = each.value.condition == null ? [] : [""]
+ content {
+ expression = each.value.condition.expression
+ title = each.value.condition.title
+ description = each.value.condition.description
+ }
+ }
+}
diff --git a/modules/gcs/main.tf b/modules/gcs/main.tf
index a0354febb..4ad9434af 100644
--- a/modules/gcs/main.tf
+++ b/modules/gcs/main.tf
@@ -128,13 +128,6 @@ resource "google_storage_bucket_object" "objects" {
}
}
-resource "google_storage_bucket_iam_binding" "bindings" {
- for_each = var.iam
- bucket = google_storage_bucket.bucket.name
- role = each.key
- members = each.value
-}
-
resource "google_storage_notification" "notification" {
count = local.notification ? 1 : 0
bucket = google_storage_bucket.bucket.name
diff --git a/modules/gcs/variables.tf b/modules/gcs/variables.tf
index 5f6c5e375..253b5570f 100644
--- a/modules/gcs/variables.tf
+++ b/modules/gcs/variables.tf
@@ -43,6 +43,36 @@ variable "iam" {
default = {}
}
+variable "iam_bindings" {
+ description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary."
+ type = map(object({
+ members = list(string)
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
+variable "iam_bindings_additive" {
+ description = "Individual additive IAM bindings. Keys are arbitrary."
+ type = map(object({
+ member = string
+ role = string
+ condition = optional(object({
+ expression = string
+ title = string
+ description = optional(string)
+ }))
+ }))
+ nullable = false
+ default = {}
+}
+
variable "labels" {
description = "Labels to be attached to all buckets."
type = map(string)
diff --git a/tests/modules/gcs/examples/iam-authoritative.yaml b/tests/modules/gcs/examples/iam-authoritative.yaml
new file mode 100644
index 000000000..84398c3ce
--- /dev/null
+++ b/tests/modules/gcs/examples/iam-authoritative.yaml
@@ -0,0 +1,47 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+values:
+ module.bucket.google_storage_bucket.bucket:
+ autoclass: []
+ cors: []
+ custom_placement_config: []
+ default_event_based_hold: null
+ encryption: []
+ force_destroy: false
+ labels: null
+ lifecycle_rule: []
+ location: EU
+ logging: []
+ name: my-bucket
+ project: myproject
+ requester_pays: null
+ retention_policy: []
+ storage_class: MULTI_REGIONAL
+ timeouts: null
+ uniform_bucket_level_access: true
+ versioning:
+ - enabled: false
+ module.bucket.google_storage_bucket_iam_binding.authoritative["roles/storage.admin"]:
+ bucket: my-bucket
+ condition: []
+ members:
+ - group:storage@example.com
+ role: roles/storage.admin
+
+counts:
+ google_storage_bucket: 1
+ google_storage_bucket_iam_binding: 1
+ modules: 1
+ resources: 2
\ No newline at end of file
diff --git a/tests/modules/gcs/examples/iam-bindings-additive.yaml b/tests/modules/gcs/examples/iam-bindings-additive.yaml
new file mode 100644
index 000000000..edc6c6a77
--- /dev/null
+++ b/tests/modules/gcs/examples/iam-bindings-additive.yaml
@@ -0,0 +1,49 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+values:
+ module.bucket.google_storage_bucket.bucket:
+ autoclass: []
+ cors: []
+ custom_placement_config: []
+ default_event_based_hold: null
+ encryption: []
+ force_destroy: false
+ labels: null
+ lifecycle_rule: []
+ location: EU
+ logging: []
+ name: my-bucket
+ project: myproject
+ requester_pays: null
+ retention_policy: []
+ storage_class: MULTI_REGIONAL
+ timeouts: null
+ uniform_bucket_level_access: true
+ versioning:
+ - enabled: false
+ module.bucket.google_storage_bucket_iam_member.bindings["storage-admin-with-delegated_roles"]:
+ bucket: my-bucket
+ condition:
+ - description: null
+ expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/storage.objectAdmin','roles/storage.objectViewer'])
+ title: delegated-role-grants
+ member: group:storage@example.com
+ role: roles/storage.admin
+
+counts:
+ google_storage_bucket: 1
+ google_storage_bucket_iam_member: 1
+ modules: 1
+ resources: 2
\ No newline at end of file
diff --git a/tests/modules/gcs/examples/iam-bindings.yaml b/tests/modules/gcs/examples/iam-bindings.yaml
new file mode 100644
index 000000000..ff3740b14
--- /dev/null
+++ b/tests/modules/gcs/examples/iam-bindings.yaml
@@ -0,0 +1,50 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+values:
+ module.bucket.google_storage_bucket.bucket:
+ autoclass: []
+ cors: []
+ custom_placement_config: []
+ default_event_based_hold: null
+ encryption: []
+ force_destroy: false
+ labels: null
+ lifecycle_rule: []
+ location: EU
+ logging: []
+ name: my-bucket
+ project: myproject
+ requester_pays: null
+ retention_policy: []
+ storage_class: MULTI_REGIONAL
+ timeouts: null
+ uniform_bucket_level_access: true
+ versioning:
+ - enabled: false
+ module.bucket.google_storage_bucket_iam_binding.bindings["storage-admin-with-delegated_roles"]:
+ bucket: my-bucket
+ condition:
+ - description: null
+ expression: api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/storage.objectAdmin','roles/storage.objectViewer'])
+ title: delegated-role-grants
+ members:
+ - group:storage@example.com
+ role: roles/storage.admin
+
+counts:
+ google_storage_bucket: 1
+ google_storage_bucket_iam_binding: 1
+ modules: 1
+ resources: 2
\ No newline at end of file
diff --git a/tests/modules/gcs/examples/simple.yaml b/tests/modules/gcs/examples/simple.yaml
index bc2630b87..3e7a646de 100644
--- a/tests/modules/gcs/examples/simple.yaml
+++ b/tests/modules/gcs/examples/simple.yaml
@@ -34,13 +34,6 @@ values:
uniform_bucket_level_access: true
versioning:
- enabled: true
- module.bucket.google_storage_bucket_iam_binding.bindings["roles/storage.admin"]:
- bucket: test-my-bucket
- condition: []
- members:
- - group:storage@example.com
- role: roles/storage.admin
counts:
- google_storage_bucket: 1
- google_storage_bucket_iam_binding: 1
+ google_storage_bucket: 1
\ No newline at end of file