From f9f0b33542146f76d10ec6b4f8a574285a9accb7 Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 26 Jun 2025 12:00:00 -0600 Subject: [PATCH] Add iamConfiguration support to gcp_storage_bucket You can now set the iam configuration for a given bucket, you can set: 1. publicAccessPrevention and 2. uniformBucketLevelAccess no support for bucketPolicyOnly because according to the storage docs: Note: iamConfiguration also includes the bucketPolicyOnly field, which uses a legacy name but has the same functionality as the uniformBucketLevelAccess field. We recommend only using uniformBucketLevelAccess, as specifying both fields may result in unreliable behavior. Also added integration tests for this feature Signed-off-by: Jorge Gallegos --- plugins/modules/gcp_storage_bucket.py | 93 ++++++++++++++++++- .../tasks/iam_configuration.yml | 86 +++++++++++++++++ .../targets/gcp_storage_bucket/tasks/main.yml | 3 + 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 tests/integration/targets/gcp_storage_bucket/tasks/iam_configuration.yml diff --git a/plugins/modules/gcp_storage_bucket.py b/plugins/modules/gcp_storage_bucket.py index 18645d95..766aae0c 100644 --- a/plugins/modules/gcp_storage_bucket.py +++ b/plugins/modules/gcp_storage_bucket.py @@ -186,6 +186,33 @@ options: - 'Some valid choices include: "OWNER", "READER"' required: true type: str + iam_configuration: + description: + - IAM configuration for the storage bucket. + required: false + type: dict + suboptions: + public_access_prevention: + description: + - The bucket's public access prevention status. + required: false + type: str + default: inherited + choices: + - inherited + - enforced + uniform_bucket_level_access: + description: + - The bucket's uniform bucket-level access configuration. + required: false + type: dict + suboptions: + enabled: + description: + - Whether or not the bucket uses uniform bucket-level access. + - If set, access checks only use bucket-level IAM policies or above. + required: false + type: bool lifecycle: description: - The bucket's lifecycle configuration. @@ -209,7 +236,7 @@ options: suboptions: storage_class: description: - - Target storage class. Required iff the type of the action is SetStorageClass. + - Target storage class. Required if the type of the action is SetStorageClass. required: false type: str type: @@ -627,6 +654,29 @@ defaultObjectAcl: - The access permission for the entity. returned: success type: str +iamConfiguration: + description: + - IAM configuration for the storage bucket. + returned: success + type: complex + contains: + publicAccessPrevention: + description: + - The bucket's public access prevention status. + returned: success + type: str + uniformBucketLevelAccess: + description: + - The bucket's uniform bucket-level access configuration. + returned: success + type: complex + contains: + enabled: + description: + - Whether or not the bucket uses uniform bucket-level access. + - If set, access checks only use bucket-level IAM policies or above. + returned: success + type: bool id: description: - The ID of the bucket. For buckets, the id and name properities are the same. @@ -922,6 +972,21 @@ def main(): role=dict(required=True, type='str'), ), ), + iam_configuration=dict( + type='dict', + options=dict( + public_access_prevention=dict( + type='str', default='inherited', + choices=['inherited', 'enforced'], + ), + uniform_bucket_level_access=dict( + type='dict', + options=dict( + enabled=dict(type='bool'), + ), + ), + ), + ), lifecycle=dict( type='dict', options=dict( @@ -1017,6 +1082,7 @@ def resource_to_request(module): u'cors': BucketCorsArray(module.params.get('cors', []), module).to_request(), u'defaultEventBasedHold': module.params.get('default_event_based_hold'), u'defaultObjectAcl': BucketDefaultobjectaclArray(module.params.get('default_object_acl', []), module).to_request(), + u'iamConfiguration': BucketIamconfiguration(module.params.get('iam_configuration', {}), module).to_request(), u'lifecycle': BucketLifecycle(module.params.get('lifecycle', {}), module).to_request(), u'location': module.params.get('location'), u'logging': BucketLogging(module.params.get('logging', {}), module).to_request(), @@ -1095,8 +1161,9 @@ def response_to_hash(module, response): u'acl': BucketAclArray(response.get(u'acl', []), module).from_response(), u'cors': BucketCorsArray(response.get(u'cors', []), module).from_response(), u'defaultEventBasedHold': response.get(u'defaultEventBasedHold'), - u'defaultObjectAcl': BucketDefaultobjectaclArray(module.params.get('default_object_acl', []), module).to_request(), + u'defaultObjectAcl': BucketDefaultobjectaclArray(module.params.get('default_object_acl', []), module).from_response(), u'id': response.get(u'id'), + u'iamConfiguration': BucketIamconfiguration(response.get('iamConfiguration', {}), module).from_response(), u'lifecycle': BucketLifecycle(response.get(u'lifecycle', {}), module).from_response(), u'location': response.get(u'location'), u'logging': BucketLogging(response.get(u'logging', {}), module).from_response(), @@ -1429,5 +1496,27 @@ class BucketWebsite(object): return remove_nones_from_dict({u'mainPageSuffix': self.request.get(u'mainPageSuffix'), u'notFoundPage': self.request.get(u'notFoundPage')}) +class BucketIamconfiguration(object): + def __init__(self, transport, module): + self.module = module + # transport can be either the request or response objects + if transport: + self.transport = transport + else: + self.transport = {} + + def to_request(self): + return remove_nones_from_dict({ + u'publicAccessPrevention': self.transport.get('public_access_prevention'), + u'uniformBucketLevelAccess': self.transport.get('uniform_bucket_level_access'), + }) + + def from_response(self): + return remove_nones_from_dict({ + u'publicAccessPrevention': self.transport.get('publicAccessPrevention'), + u'uniformBucketLevelAccess': self.transport.get('uniformBucketLevelAccess'), + }) + + if __name__ == '__main__': main() diff --git a/tests/integration/targets/gcp_storage_bucket/tasks/iam_configuration.yml b/tests/integration/targets/gcp_storage_bucket/tasks/iam_configuration.yml new file mode 100644 index 00000000..5e20e5a1 --- /dev/null +++ b/tests/integration/targets/gcp_storage_bucket/tasks/iam_configuration.yml @@ -0,0 +1,86 @@ +--- +- name: Run test cases + block: + # -------------------------------------------------------------------------- + - name: Create default bucket + google.cloud.gcp_storage_bucket: + name: "{{ resource_name }}-default" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + register: result + + - name: Assert changed is true and default values are returned + ansible.builtin.assert: + that: + - result.changed == true + - result.iamConfiguration.publicAccessPrevention == 'inherited' + - result.iamConfiguration.uniformBucketLevelAccess.enabled == false + # -------------------------------------------------------------------------- + - name: Create bucket with enforced PAP + google.cloud.gcp_storage_bucket: + name: "{{ resource_name }}-pap" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + iam_configuration: + public_access_prevention: enforced + register: result + + - name: Assert changed is true and IAM PAP is 'enforced' + ansible.builtin.assert: + that: + - result.changed == true + - result.iamConfiguration.publicAccessPrevention == 'enforced' + # -------------------------------------------------------------------------- + - name: Create bucket with UBLA enabled + google.cloud.gcp_storage_bucket: + name: "{{ resource_name }}-ublae" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + iam_configuration: + uniform_bucket_level_access: + enabled: true + register: result + + - name: Assert changed is true and IAM UBLA is enabled + ansible.builtin.assert: + that: + - result.changed == true + - result.iamConfiguration.uniformBucketLevelAccess.enabled == true + # -------------------------------------------------------------------------- + - name: Create bucket with UBLA disabled + google.cloud.gcp_storage_bucket: + name: "{{ resource_name }}-ublad" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: present + iam_configuration: + uniform_bucket_level_access: + enabled: false + register: result + + - name: Assert changed is true and IAM UBLA is disabled + ansible.builtin.assert: + that: + - result.changed == true + - result.iamConfiguration.uniformBucketLevelAccess.enabled == false + # -------------------------------------------------------------------------- + always: + - name: Clean up buckets + google.cloud.gcp_storage_bucket: + name: "{{ resource_name }}-{{ item }}" + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file | default(omit) }}" + state: absent + loop: + - default + - pap + - ublae + - ublad diff --git a/tests/integration/targets/gcp_storage_bucket/tasks/main.yml b/tests/integration/targets/gcp_storage_bucket/tasks/main.yml index fe47378c..d15a535c 100644 --- a/tests/integration/targets/gcp_storage_bucket/tasks/main.yml +++ b/tests/integration/targets/gcp_storage_bucket/tasks/main.yml @@ -1,3 +1,6 @@ --- - name: Generated tests ansible.builtin.include_tasks: autogen.yml + +- name: Tests for IAM Configuration support + ansible.builtin.include_tasks: iam_configuration.yml