diff --git a/changelogs/fragments/10097-fix-rundeck_acl_policy-project-endpoint.yml b/changelogs/fragments/10097-fix-rundeck_acl_policy-project-endpoint.yml new file mode 100644 index 0000000000..0386edc090 --- /dev/null +++ b/changelogs/fragments/10097-fix-rundeck_acl_policy-project-endpoint.yml @@ -0,0 +1,2 @@ +bugfixes: + - rundeck_acl_policy - ensure that project ACLs are sent to the correct endpoint (https://github.com/ansible-collections/community.general/pull/10097). diff --git a/plugins/modules/rundeck_acl_policy.py b/plugins/modules/rundeck_acl_policy.py index aa22e6e6ea..0c089792b8 100644 --- a/plugins/modules/rundeck_acl_policy.py +++ b/plugins/modules/rundeck_acl_policy.py @@ -129,11 +129,18 @@ from ansible_collections.community.general.plugins.module_utils.rundeck import ( class RundeckACLManager: def __init__(self, module): self.module = module + if module.params.get("project"): + self.endpoint = "project/%s/acl/%s.aclpolicy" % ( + self.module.params["project"], + self.module.params["name"], + ) + else: + self.endpoint = "system/acl/%s.aclpolicy" % self.module.params["name"] def get_acl(self): resp, info = api_request( module=self.module, - endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], + endpoint=self.endpoint, ) return resp @@ -147,7 +154,7 @@ class RundeckACLManager: resp, info = api_request( module=self.module, - endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], + endpoint=self.endpoint, method="POST", data={"contents": self.module.params["policy"]}, ) @@ -171,7 +178,7 @@ class RundeckACLManager: resp, info = api_request( module=self.module, - endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], + endpoint=self.endpoint, method="PUT", data={"contents": self.module.params["policy"]}, ) @@ -194,7 +201,7 @@ class RundeckACLManager: if not self.module.check_mode: api_request( module=self.module, - endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], + endpoint=self.endpoint, method="DELETE", ) diff --git a/tests/integration/targets/rundeck/defaults/main.yml b/tests/integration/targets/rundeck/defaults/main.yml index 4d7ea31468..503f627857 100644 --- a/tests/integration/targets/rundeck/defaults/main.yml +++ b/tests/integration/targets/rundeck/defaults/main.yml @@ -6,3 +6,32 @@ rundeck_url: http://localhost:4440 rundeck_api_version: 39 rundeck_job_id: 3b8a6e54-69fb-42b7-b98f-f82e59238478 + +system_acl_policy: | + description: Test ACL + context: + application: 'rundeck' + for: + project: + - allow: + - read + by: + group: + - users + +project_acl_policy: | + description: Test project acl + for: + resource: + - equals: + kind: node + allow: [read,refresh] + - equals: + kind: event + allow: [read] + job: + - allow: [run,kill] + node: + - allow: [read,run] + by: + group: users diff --git a/tests/integration/targets/rundeck/tasks/main.yml b/tests/integration/targets/rundeck/tasks/main.yml index e42780b9b7..7762832d10 100644 --- a/tests/integration/targets/rundeck/tasks/main.yml +++ b/tests/integration/targets/rundeck/tasks/main.yml @@ -15,6 +15,9 @@ RD_USER: admin RD_PASSWORD: admin register: rundeck_api_token + retries: 3 + until: rundeck_api_token.rc == 0 + changed_when: true - name: Create a Rundeck project community.general.rundeck_project: @@ -24,6 +27,71 @@ token: "{{ rundeck_api_token.stdout_lines[-1] }}" state: present +- name: Create a system ACL + community.general.rundeck_acl_policy: + name: test_acl + api_version: "{{ rundeck_api_version }}" + url: "{{ rundeck_url }}" + token: "{{ rundeck_api_token.stdout_lines[-1] }}" + state: present + policy: "{{ system_acl_policy }}" + +- name: Create a project ACL + community.general.rundeck_acl_policy: + name: test_acl + api_version: "{{ rundeck_api_version }}" + url: "{{ rundeck_url }}" + token: "{{ rundeck_api_token.stdout_lines[-1] }}" + state: present + policy: "{{ project_acl_policy }}" + project: test_project + +- name: Retrieve ACLs + ansible.builtin.uri: + url: "{{ rundeck_url }}/api/{{ rundeck_api_version }}/{{ item }}" + headers: + accept: application/json + x-rundeck-auth-token: "{{ rundeck_api_token.stdout_lines[-1] }}" + register: acl_policy_check + loop: + - system/acl/test_acl.aclpolicy + - project/test_project/acl/test_acl.aclpolicy + +- name: Assert ACL content is correct + ansible.builtin.assert: + that: + - acl_policy_check['results'][0]['json']['contents'] == system_acl_policy + - acl_policy_check['results'][1]['json']['contents'] == project_acl_policy + +- name: Remove system ACL + community.general.rundeck_acl_policy: + name: test_acl + api_version: "{{ rundeck_api_version }}" + url: "{{ rundeck_url }}" + token: "{{ rundeck_api_token.stdout_lines[-1] }}" + state: absent + +- name: Remove project ACL + community.general.rundeck_acl_policy: + name: test_acl + api_version: "{{ rundeck_api_version }}" + url: "{{ rundeck_url }}" + token: "{{ rundeck_api_token.stdout_lines[-1] }}" + state: absent + project: test_project + +- name: Check that ACLs have been removed + ansible.builtin.uri: + url: "{{ rundeck_url }}/api/{{ rundeck_api_version }}/{{ item }}" + headers: + accept: application/json + x-rundeck-auth-token: "{{ rundeck_api_token.stdout_lines[-1] }}" + status_code: + - 404 + loop: + - system/acl/test_acl.aclpolicy + - project/test_project/acl/test_acl.aclpolicy + - name: Copy test_job definition to /tmp copy: src: test_job.yaml diff --git a/tests/integration/targets/setup_rundeck/defaults/main.yml b/tests/integration/targets/setup_rundeck/defaults/main.yml index c842901c0f..45b3710470 100644 --- a/tests/integration/targets/setup_rundeck/defaults/main.yml +++ b/tests/integration/targets/setup_rundeck/defaults/main.yml @@ -3,5 +3,13 @@ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -rundeck_war_url: https://packagecloud.io/pagerduty/rundeck/packages/java/org.rundeck/rundeck-3.4.4-20210920.war/artifacts/rundeck-3.4.4-20210920.war/download -rundeck_cli_url: https://github.com/rundeck/rundeck-cli/releases/download/v1.3.10/rundeck-cli-1.3.10-all.jar +rundeck_version: 5.11.1-20250415 +rundeck_cli_version: "2.0.8" + +rundeck_war_url: + "https://packagecloud.io/pagerduty/rundeck/packages/java/org.rundeck/\ + rundeck-{{ rundeck_version }}.war/artifacts/rundeck-{{ rundeck_version }}.war/download" + +rundeck_cli_url: + "https://github.com/rundeck/rundeck-cli/releases/download/\ + v{{ rundeck_cli_version }}/rundeck-cli-{{ rundeck_cli_version }}-all.jar" diff --git a/tests/integration/targets/setup_rundeck/vars/RedHat.yml b/tests/integration/targets/setup_rundeck/vars/RedHat.yml index 314f0ef415..bba076aecd 100644 --- a/tests/integration/targets/setup_rundeck/vars/RedHat.yml +++ b/tests/integration/targets/setup_rundeck/vars/RedHat.yml @@ -3,4 +3,4 @@ # GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) # SPDX-License-Identifier: GPL-3.0-or-later -openjdk_pkg: java-1.8.0-openjdk +openjdk_pkg: java-11-openjdk-headless diff --git a/tests/unit/plugins/modules/test_rundeck_acl_policy.py b/tests/unit/plugins/modules/test_rundeck_acl_policy.py new file mode 100644 index 0000000000..564446cf3e --- /dev/null +++ b/tests/unit/plugins/modules/test_rundeck_acl_policy.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest +from ansible_collections.community.general.plugins.modules import rundeck_acl_policy +from ansible_collections.community.internal_test_tools.tests.unit.compat.mock import patch +from ansible_collections.community.internal_test_tools.tests.unit.plugins.modules.utils import ( + set_module_args, + AnsibleExitJson, + exit_json, + fail_json +) + + +@pytest.fixture(autouse=True) +def module(): + with patch.multiple( + "ansible.module_utils.basic.AnsibleModule", + exit_json=exit_json, + fail_json=fail_json, + ): + yield + + +# define our two table entries: system ACL vs. project ACL +PROJECT_TABLE = [ + (None, "system/acl"), + ("test_project", "project/test_project/acl"), +] + + +@pytest.mark.parametrize("project, prefix", PROJECT_TABLE) +@patch.object(rundeck_acl_policy, 'api_request') +def test_acl_create(api_request_mock, project, prefix): + """Test creating a new ACL, both system-level and project-level.""" + name = "my_policy" + policy = "test_policy_yaml" + # simulate: GET→404, POST→201, final GET→200 + api_request_mock.side_effect = [ + (None, {'status': 404}), + (None, {'status': 201}), + ({"contents": policy}, {'status': 200}), + ] + args = { + 'name': name, + 'url': "https://rundeck.example.org", + 'api_token': "mytoken", + 'policy': policy, + } + if project: + args['project'] = project + + with pytest.raises(AnsibleExitJson): + with set_module_args(args): + rundeck_acl_policy.main() + + # should have done GET → POST → GET + assert api_request_mock.call_count == 3 + args, kwargs = api_request_mock.call_args_list[1] + assert kwargs['endpoint'] == "%s/%s.aclpolicy" % (prefix, name) + assert kwargs['method'] == 'POST' + + +@pytest.mark.parametrize("project, prefix", PROJECT_TABLE) +@patch.object(rundeck_acl_policy, 'api_request') +def test_acl_unchanged(api_request_mock, project, prefix): + """Test no-op when existing ACL contents match the desired policy.""" + name = "unchanged_policy" + policy = "same_policy_yaml" + # first GET returns matching contents + api_request_mock.return_value = ({"contents": policy}, {'status': 200}) + + args = { + 'name': name, + 'url': "https://rundeck.example.org", + 'api_token': "mytoken", + 'policy': policy, + } + if project: + args['project'] = project + + with pytest.raises(AnsibleExitJson): + with set_module_args(args): + rundeck_acl_policy.main() + + # only a single GET + assert api_request_mock.call_count == 1 + args, kwargs = api_request_mock.call_args + assert kwargs['endpoint'] == "%s/%s.aclpolicy" % (prefix, name) + # default method is GET + assert kwargs.get('method', 'GET') == 'GET' + + +@pytest.mark.parametrize("project, prefix", PROJECT_TABLE) +@patch.object(rundeck_acl_policy, 'api_request') +def test_acl_remove(api_request_mock, project, prefix): + """Test removing an existing ACL, both system- and project-level.""" + name = "remove_me" + # GET finds it, DELETE removes it + api_request_mock.side_effect = [ + ({"contents": "old_yaml"}, {'status': 200}), + (None, {'status': 204}), + ] + + args = { + 'name': name, + 'url': "https://rundeck.example.org", + 'api_token': "mytoken", + 'state': 'absent', + } + if project: + args['project'] = project + + with pytest.raises(AnsibleExitJson): + with set_module_args(args): + rundeck_acl_policy.main() + + # GET → DELETE + assert api_request_mock.call_count == 2 + args, kwargs = api_request_mock.call_args_list[1] + assert kwargs['endpoint'] == "%s/%s.aclpolicy" % (prefix, name) + assert kwargs['method'] == 'DELETE' + + +@pytest.mark.parametrize("project, prefix", PROJECT_TABLE) +@patch.object(rundeck_acl_policy, 'api_request') +def test_acl_remove_nonexistent(api_request_mock, project, prefix): + """Test removing a non-existent ACL results in no change.""" + name = "not_there" + # GET returns 404 + api_request_mock.return_value = (None, {'status': 404}) + + args = { + 'name': name, + 'url': "https://rundeck.example.org", + 'api_token': "mytoken", + 'state': 'absent', + } + if project: + args['project'] = project + + with pytest.raises(AnsibleExitJson): + with set_module_args(args): + rundeck_acl_policy.main() + + # only the initial GET + assert api_request_mock.call_count == 1 + args, kwargs = api_request_mock.call_args + assert kwargs['endpoint'] == "%s/%s.aclpolicy" % (prefix, name) + assert kwargs.get('method', 'GET') == 'GET'