rundeck_acl_policy: fix project acls are put/posted to the wrong endpoint (#10097)

* Fix project acls are put/posted to the wrong endpoint

* Add changelog fragment.

* Fix 2.7 sanity errors in github

* Fix fragment extension and use 2.7 syntax in test

* Update changelogs/fragments/10097-fix-rundeck_acl_policy-project-endpoint.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Fix pep8 formatting

* Add licensing to unit test

---------

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
kjoyce77 2025-05-17 01:01:32 -05:00 committed by GitHub
parent 2b4cb6dabc
commit ff0ed6f912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 277 additions and 7 deletions

View file

@ -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).

View file

@ -129,11 +129,18 @@ from ansible_collections.community.general.plugins.module_utils.rundeck import (
class RundeckACLManager: class RundeckACLManager:
def __init__(self, module): def __init__(self, module):
self.module = 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): def get_acl(self):
resp, info = api_request( resp, info = api_request(
module=self.module, module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], endpoint=self.endpoint,
) )
return resp return resp
@ -147,7 +154,7 @@ class RundeckACLManager:
resp, info = api_request( resp, info = api_request(
module=self.module, module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], endpoint=self.endpoint,
method="POST", method="POST",
data={"contents": self.module.params["policy"]}, data={"contents": self.module.params["policy"]},
) )
@ -171,7 +178,7 @@ class RundeckACLManager:
resp, info = api_request( resp, info = api_request(
module=self.module, module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], endpoint=self.endpoint,
method="PUT", method="PUT",
data={"contents": self.module.params["policy"]}, data={"contents": self.module.params["policy"]},
) )
@ -194,7 +201,7 @@ class RundeckACLManager:
if not self.module.check_mode: if not self.module.check_mode:
api_request( api_request(
module=self.module, module=self.module,
endpoint="system/acl/%s.aclpolicy" % self.module.params["name"], endpoint=self.endpoint,
method="DELETE", method="DELETE",
) )

View file

@ -6,3 +6,32 @@
rundeck_url: http://localhost:4440 rundeck_url: http://localhost:4440
rundeck_api_version: 39 rundeck_api_version: 39
rundeck_job_id: 3b8a6e54-69fb-42b7-b98f-f82e59238478 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

View file

@ -15,6 +15,9 @@
RD_USER: admin RD_USER: admin
RD_PASSWORD: admin RD_PASSWORD: admin
register: rundeck_api_token register: rundeck_api_token
retries: 3
until: rundeck_api_token.rc == 0
changed_when: true
- name: Create a Rundeck project - name: Create a Rundeck project
community.general.rundeck_project: community.general.rundeck_project:
@ -24,6 +27,71 @@
token: "{{ rundeck_api_token.stdout_lines[-1] }}" token: "{{ rundeck_api_token.stdout_lines[-1] }}"
state: present 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 - name: Copy test_job definition to /tmp
copy: copy:
src: test_job.yaml src: test_job.yaml

View file

@ -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) # 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 # 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_version: 5.11.1-20250415
rundeck_cli_url: https://github.com/rundeck/rundeck-cli/releases/download/v1.3.10/rundeck-cli-1.3.10-all.jar 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"

View file

@ -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) # 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 # SPDX-License-Identifier: GPL-3.0-or-later
openjdk_pkg: java-1.8.0-openjdk openjdk_pkg: java-11-openjdk-headless

View file

@ -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'