diff --git a/plugins/module_utils/cdp_iam.py b/plugins/module_utils/cdp_iam.py new file mode 100644 index 00000000..e2c9ac09 --- /dev/null +++ b/plugins/module_utils/cdp_iam.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +# Copyright 2025 Cloudera, Inc. All Rights Reserved. +# +# 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. + +""" +A REST client for the Cloudera on Cloud Platform (CDP) IAM API +""" + +from typing import Any, Dict, List, Optional + +from ansible_collections.cloudera.cloud.plugins.module_utils.cdp_client import ( + RestClient, + CdpClient, +) + + +class CdpIamClient(CdpClient): + """CDP IAM API client.""" + + def __init__(self, api_client: RestClient): + """ + Initialize CDP IAM client. + + Args: + api_client: RestClient instance for managing HTTP method calls + """ + super().__init__(api_client=api_client) + + @RestClient.paginated() + def list_groups( + self, + group_names: Optional[List[str]] = None, + pageToken: Optional[str] = None, + pageSize: Optional[int] = None, + ) -> Dict[str, Any]: + """ + List IAM groups with automatic pagination. + + Args: + group_names: Optional list of group names or CRNs to filter by + pageToken: Token for pagination (automatically handled by decorator) + pageSize: Page size for pagination (automatically handled by decorator) + + Returns: + Response with automatic pagination handling containing groups list + """ + json_data: Dict[str, Any] = {} + + # Add group names filter if provided + if group_names is not None: + json_data["groupNames"] = group_names + + # Add pagination parameters if provided + # Note: IAM API uses "startingToken" for requests, but decorator uses "pageToken" + if pageToken is not None: + json_data["startingToken"] = pageToken + if pageSize is not None: + json_data["pageSize"] = pageSize + + return self.post( + "/api/v1/iam/listGroups", + json_data=json_data, + ) diff --git a/plugins/modules/iam_group_info.py b/plugins/modules/iam_group_info.py index 745e586a..302af276 100644 --- a/plugins/modules/iam_group_info.py +++ b/plugins/modules/iam_group_info.py @@ -23,6 +23,7 @@ author: - "Webster Mudge (@wmudge)" - "Dan Chaffelson (@chaffelson)" + - "Ronald Suplina (@rsuplina)" version_added: "1.0.0" options: name: @@ -36,21 +37,24 @@ aliases: - group_name extends_documentation_fragment: - - cloudera.cloud.cdp_sdk_options - - cloudera.cloud.cdp_auth_options + - cloudera.cloud.cdp_client """ EXAMPLES = r""" # Note: These examples do not set authentication details. -# Gather information about all Groups -- cloudera.cloud.iam_group_info: - -# Gather information about a named Group -- cloudera.cloud.iam_group_info: - name: example-01 - -# Gather information about several named Groups + - name: Gather information about all Groups + cloudera.cloud.iam_group_info: + + - name: Gather information about a named Group + cloudera.cloud.iam_group_info: + name: example-01 + - name: Gather information about several named Groups + cloudera.cloud.iam_group_info: + name: + - example-01 + - example-02 + - example-03 - cloudera.cloud.iam_group_info: name: - example-01 @@ -60,12 +64,14 @@ RETURN = r""" groups: - description: The information about the named Group or Groups + description: + - Returns a list of group records. + - Each record represents a CDP IAM group and its details. type: list returned: always elements: dict contains: - creationDate: + # creation_date: description: The date when this group record was created. returned: on success type: str @@ -74,7 +80,7 @@ description: The CRN of the group. returned: on success type: str - groupName: + # group_name: description: The group name. returned: on success type: str @@ -95,76 +101,88 @@ type: list elements: dict contains: - resourceCrn: + # resource_crn: description: The CRN of the resource granted the rights of the role. returned: on success type: str - resourceRoleCrn: + # resource_role_crn: description: The CRN of the CDP Role. returned: on success type: str - syncMembershipOnUserLogin: - description: Flag indicating whether group membership is synced when a user logs in. The default is to sync group - membership. + # sync_membership_on_user_login: + description: Flag indicating whether group membership is synced when a user logs in. The default is to sync group membership. returned: when supported type: bool sdk_out: - description: Returns the captured CDP SDK log. + description: Returns the captured API HTTP log. returned: when supported type: str sdk_out_lines: - description: Returns a list of each line of the captured CDP SDK log. + description: Returns a list of each line of the captured API HTTP log. returned: when supported type: list elements: str """ -from ansible.module_utils.basic import AnsibleModule -from ansible_collections.cloudera.cloud.plugins.module_utils.cdp_common import CdpModule +from typing import Any +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict -class IAMGroupInfo(CdpModule): - def __init__(self, module): - super(IAMGroupInfo, self).__init__(module) +from ansible_collections.cloudera.cloud.plugins.module_utils.common import ( + ServicesModule, +) +from ansible_collections.cloudera.cloud.plugins.module_utils.cdp_iam import ( + CdpIamClient, +) - # Set variables - self.name = self._get_param("name") - # Initialize the return values - self.info = [] +class IAMGroupInfo(ServicesModule): + def __init__(self): + super().__init__( + argument_spec=dict( + name=dict( + required=False, + type="list", + elements="str", + aliases=["group_name"], + ), + ), + supports_check_mode=True, + ) - # Execute logic process - self.process() + # Set parameters + self.name = self.get_param("name") + + # Initialize the return values + self.groups = [] - @CdpModule._Decorators.process_debug def process(self): - self.info = self.cdpy.iam.gather_groups(self.name) + client = CdpIamClient(api_client=self.api_client) + result = client.list_groups(group_names=self.name) + self.groups = result.get("groups", []) -def main(): - module = AnsibleModule( - argument_spec=CdpModule.argument_spec( - name=dict( - required=False, - type="list", - elements="str", - aliases=["group_name"], - ), - ), - supports_check_mode=True, - ) +# NOTE: Snake_case conversion deferred until 4.0 to maintain backward compatibility. +# self.groups = [ +# camel_dict_to_snake_dict(group) for group in result.get("groups", []) +# ] + - result = IAMGroupInfo(module) +def main(): + result = IAMGroupInfo() - output = dict( + output: dict[str, Any] = dict( changed=False, - groups=result.info, + groups=result.groups, ) - if result.debug: - output.update(sdk_out=result.log_out, sdk_out_lines=result.log_lines) + if result.debug_log: + output.update( + sdk_out=result.log_out, + sdk_out_lines=result.log_lines, + ) - module.exit_json(**output) + result.module.exit_json(**output) if __name__ == "__main__": diff --git a/tests/unit/plugins/modules/iam_group_info/test_iam_group_info.py b/tests/unit/plugins/modules/iam_group_info/test_iam_group_info.py new file mode 100644 index 00000000..5baec40f --- /dev/null +++ b/tests/unit/plugins/modules/iam_group_info/test_iam_group_info.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +# Copyright 2025 Cloudera, Inc. All Rights Reserved. +# +# 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. + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest + +from ansible_collections.cloudera.cloud.tests.unit import ( + AnsibleFailJson, + AnsibleExitJson, +) + +from ansible_collections.cloudera.cloud.plugins.module_utils.cdp_client import ( + RestClient, +) +from ansible_collections.cloudera.cloud.plugins.module_utils.cdp_iam import ( + CdpIamClient, +) + + +BASE_URL = "https://cloudera.internal/api" +ACCESS_KEY = "test-access-key" +PRIVATE_KEY = "test-private-key" + + +class TestCdpIamClient: + """Unit tests for CdpIamClient.""" + + def test_list_groups_no_filter(self, mocker): + """Test listing all IAM groups.""" + + # Mock response data + mock_response = { + "groups": [ + { + "groupName": "group1", + "crn": "crn:cdp:iam:us-west-1:account:group:group1", + "creationDate": "2025-01-01T00:00:00Z", + }, + { + "groupName": "group2", + "crn": "crn:cdp:iam:us-west-1:account:group:group2", + "creationDate": "2025-01-02T00:00:00Z", + }, + ], + } + + # Mock the RestClient instance + api_client = mocker.create_autospec(RestClient, instance=True) + api_client._post.return_value = mock_response + + # Create the CdpIamClient instance + client = CdpIamClient(api_client=api_client) + + response = client.list_groups() + + assert "groups" in response + assert len(response["groups"]) == 2 + assert response["groups"][0]["groupName"] == "group1" + + # Verify that the post method was called with correct parameters + api_client._post.assert_called_once_with( + "/api/v1/iam/listGroups", + None, + {"pageSize": 100}, + ) + + def test_list_groups_with_filter(self, mocker): + """Test listing IAM groups with group name filter.""" + + # Mock response data + mock_response = { + "groups": [ + { + "groupName": "specific-group", + "crn": "crn:cdp:iam:us-west-1:account:group:specific-group", + "creationDate": "2025-01-01T00:00:00Z", + }, + ], + } + + # Mock the RestClient instance + api_client = mocker.create_autospec(RestClient, instance=True) + api_client._post.return_value = mock_response + + # Create the CdpIamClient instance + client = CdpIamClient(api_client=api_client) + + response = client.list_groups(group_names=["specific-group"]) + + assert "groups" in response + assert len(response["groups"]) == 1 + assert response["groups"][0]["groupName"] == "specific-group" + + # Verify that the post method was called with correct parameters + api_client._post.assert_called_once_with( + "/api/v1/iam/listGroups", + None, + {"groupNames": ["specific-group"], "pageSize": 100}, + ) + + +@pytest.mark.integration_api +class TestCdpIamClientIntegration: + """Integration tests for CdpIamClient.""" + + def test_list_groups(self, api_client): + """Integration test for listing IAM groups.""" + + # Create the CdpIamClient instance + client = CdpIamClient(api_client=api_client) + + response = client.list_groups() + + assert "groups" in response + assert len(response["groups"]) > 0 + assert isinstance(response["groups"][0], dict)