diff --git a/.github/labels-issues.yml b/.github/labels-issues.yml index 2058713af..f784b7e6c 100644 --- a/.github/labels-issues.yml +++ b/.github/labels-issues.yml @@ -50,6 +50,9 @@ module:notification: module:password: - 'Component Name: password' +module:role: + - 'Component Name: role' + module:rule: - 'Component Name: rule' @@ -87,6 +90,10 @@ lookup:ldap_connection: - 'Component Name: lookup_ldap_connection' - 'Component Name: lookup_ldap_connections' +lookup:role: + - 'Component Name: lookup_role' + - 'Component Name: lookup_roles' + lookup:rule: - 'Component Name: lookup_rule' - 'Component Name: lookup_rules' diff --git a/.github/labels-prs.yml b/.github/labels-prs.yml index 965c0a896..7ebcd8137 100644 --- a/.github/labels-prs.yml +++ b/.github/labels-prs.yml @@ -79,6 +79,11 @@ module:password: - changed-files: - any-glob-to-any-file: 'plugins/modules/password.py' +module:role: + - any: + - changed-files: + - any-glob-to-any-file: 'plugins/modules/role.py' + module:rule: - any: - changed-files: @@ -138,6 +143,12 @@ lookup:ldap_connection: - any-glob-to-any-file: 'plugins/modules/lookup/ldap_connection.py' - any-glob-to-any-file: 'plugins/modules/lookup/ldap_connections.py' +lookup:role: + - any: + - changed-files: + - any-glob-to-any-file: 'plugins/lookup/role.py' + - any-glob-to-any-file: 'plugins/lookup/roles.py' + lookup:rule: - any: - changed-files: diff --git a/.github/workflows/ans-int-test-lkp-role.yaml b/.github/workflows/ans-int-test-lkp-role.yaml new file mode 100644 index 000000000..4b1ccd32e --- /dev/null +++ b/.github/workflows/ans-int-test-lkp-role.yaml @@ -0,0 +1,65 @@ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +name: Lookup Module Role Integration + +permissions: + contents: read + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' + pull_request: + branches: + - main + - devel + types: + - opened + paths: + - 'plugins/lookup/role.py' + - 'plugins/lookup/roles.py' + push: + paths: + - '.github/workflows/ans-int-test-lkp-role.yaml' + - 'plugins/lookup/role.py' + - 'plugins/lookup/roles.py' + - 'plugins/module_utils/**' + - 'tests/integration/files/includes/**' + - 'tests/integration/targets/lookup_role/**' + +jobs: + + integration: + name: "${{ matrix.checkmk_version }}.${{ matrix.checkmk_edition }}" + uses: ./.github/workflows/_template-ans-int-test.yaml + with: + module: lookup_role + checkmk_version: ${{ matrix.checkmk_version }} + checkmk_edition: ${{ matrix.checkmk_edition }} + strategy: + fail-fast: false + matrix: + checkmk_version: + - 2.3.0p46 + - 2.4.0p29 + - 2.5.0p2 + checkmk_edition: + - raw + - managed + - community + - ultimatemt + exclude: + - checkmk_version: 2.3.0p46 + checkmk_edition: community + - checkmk_version: 2.3.0p46 + checkmk_edition: ultimatemt + - checkmk_version: 2.4.0p29 + checkmk_edition: community + - checkmk_version: 2.4.0p29 + checkmk_edition: ultimatemt + - checkmk_version: 2.5.0p2 + checkmk_edition: raw + - checkmk_version: 2.5.0p2 + checkmk_edition: managed diff --git a/.github/workflows/ans-int-test-role.yaml b/.github/workflows/ans-int-test-role.yaml new file mode 100644 index 000000000..faa7f3aee --- /dev/null +++ b/.github/workflows/ans-int-test-role.yaml @@ -0,0 +1,63 @@ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +name: Module Role Integration + +permissions: + contents: read + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' + pull_request: + branches: + - main + - devel + types: + - opened + paths: + - 'plugins/modules/role.py' + push: + paths: + - '.github/workflows/ans-int-test-role.yaml' + - 'plugins/modules/role.py' + - 'plugins/module_utils/**' + - 'tests/integration/files/includes/**' + - 'tests/integration/targets/role/**' + +jobs: + + integration: + name: "${{ matrix.checkmk_version }}.${{ matrix.checkmk_edition }}" + uses: ./.github/workflows/_template-ans-int-test.yaml + with: + module: role + checkmk_version: ${{ matrix.checkmk_version }} + checkmk_edition: ${{ matrix.checkmk_edition }} + strategy: + fail-fast: false + matrix: + checkmk_version: + - 2.3.0p46 + - 2.4.0p29 + - 2.5.0p2 + checkmk_edition: + - raw + - managed + - community + - ultimatemt + exclude: + - checkmk_version: 2.3.0p46 + checkmk_edition: community + - checkmk_version: 2.3.0p46 + checkmk_edition: ultimatemt + - checkmk_version: 2.4.0p29 + checkmk_edition: community + - checkmk_version: 2.4.0p29 + checkmk_edition: ultimatemt + - checkmk_version: 2.5.0p2 + checkmk_edition: raw + - checkmk_version: 2.5.0p2 + checkmk_edition: managed diff --git a/.github/workflows/ans-unit-test-role.yaml b/.github/workflows/ans-unit-test-role.yaml new file mode 100644 index 000000000..e96a3ce5b --- /dev/null +++ b/.github/workflows/ans-unit-test-role.yaml @@ -0,0 +1,34 @@ +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +name: Module Role Unit Tests + +permissions: + contents: read + +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 0' + pull_request: + branches: + - main + - devel + paths: + - 'plugins/modules/role.py' + push: + paths: + - '.github/workflows/ans-unit-test-role.yaml' + - 'plugins/modules/role.py' + - 'plugins/module_utils/**' + - 'tests/unit/plugins/module_utils/**' + - 'tests/unit/plugins/modules/test_role.py' + +jobs: + + integration: + name: "Unit Tests" + uses: ./.github/workflows/_template-ans-unit-test.yaml + with: + testpath: tests/unit/plugins/modules/test_role.py diff --git a/README.md b/README.md index a6d2308c0..77f313919 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Name | Description | Tests [checkmk.general.folders](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/folders.py)|Look up all folders.|[![Integration Tests for Folders Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-folder.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-folder.yaml) [checkmk.general.host](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/host.py)|Look up host attributes.|[![Integration Tests for Host Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-host.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-host.yaml) [checkmk.general.hosts](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/hosts.py)|Look up all hosts.|[![Integration Tests for Hosts Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-host.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-host.yaml) +[checkmk.general.role](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/role.py)|Look up role attributes.|[![Integration Tests for Role Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-role.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-role.yaml) +[checkmk.general.roles](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/roles.py)|Look up all roles.|[![Integration Tests for Roles Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-role.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-role.yaml) [checkmk.general.rule](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/rule.py)|Look up rule attributes.|[![Integration Tests for Rule Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rules.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rules.yaml) [checkmk.general.rules](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/rules.py)|Look up all rules.|[![Integration Tests for Rules Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rules.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rules.yaml) [checkmk.general.ruleset](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/lookup/ruleset.py)|Look up ruleset attributes.|[![Integration Tests for Ruleset Lookup Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rulesets.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-lkp-rulesets.yaml) @@ -85,6 +87,7 @@ Name | Description | Tests [checkmk.general.folder](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/folder.py)|Manage folders.|[![Integration Tests for Folder Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-folder.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-folder.yaml) [checkmk.general.host_group](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/host_group.py)|Manage host groups.|[![Integration Tests for Host Group Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host_group.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host_group.yaml) [checkmk.general.host](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/host.py)|Manage hosts.|[![Integration Tests for Host Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-host.yaml) +[checkmk.general.role](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/role.py)|Manage roles and permissions.|[![Integration Tests for Role Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-role.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-role.yaml) [checkmk.general.rule](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/rule.py)|Manage rules.|[![Integration Tests for Rule Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-rule.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-rule.yaml) [checkmk.general.service_group](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/service_group.py)|Manage service groups.|[![Integration Tests for Service Group Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-service_group.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-service_group.yaml) [checkmk.general.site](https://github.com/Checkmk/ansible-collection-checkmk.general/blob/main/plugins/modules/site.py)|Manage sites.|[![Integration Tests for Site Module](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-site.yaml/badge.svg)](https://github.com/Checkmk/ansible-collection-checkmk.general/actions/workflows/ans-int-test-site.yaml) diff --git a/changelogs/fragments/role.yml b/changelogs/fragments/role.yml new file mode 100644 index 000000000..5d9d06713 --- /dev/null +++ b/changelogs/fragments/role.yml @@ -0,0 +1,4 @@ +major_changes: + - Role module - Add module to manage roles and permissions. + - Role lookup - Add lookup plugin to fetch a single role's configuration. + - Roles lookup - Add lookup plugin to list all configured roles. diff --git a/meta/runtime.yml b/meta/runtime.yml index eb2f577c1..6e8cb9f01 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -14,6 +14,7 @@ action_groups: - host - notification - password + - role - rule - service_group - site diff --git a/plugins/lookup/role.py b/plugins/lookup/role.py new file mode 100644 index 000000000..80189ab50 --- /dev/null +++ b/plugins/lookup/role.py @@ -0,0 +1,134 @@ +# Copyright: (c) 2026, Robin Gierse +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: role + author: Robin Gierse (@robin-checkmk) + version_added: "7.7.0" + + short_description: Get the configuration of a role + + description: + - Returns the configuration of a user role, including its + alias, base role, and assigned permissions. + + options: + + _terms: + description: role ID + required: True + + extends_documentation_fragment: [checkmk.general.common_lookup] + + notes: + - Like all lookups, this runs on the Ansible controller and is unaffected by other keywords such as 'become'. + If you need to use different permissions, you must change the command or run Ansible as another user. + - Alternatively, you can use a shell/command task that runs against localhost and registers the result. + - The directory of the play is used as the current working directory. + - It is B(NOT) possible to assign other variables to the variables mentioned in the C(vars) section! + This is a limitation of Ansible itself. + + seealso: + - module: checkmk.general.role + - plugin: checkmk.general.roles + plugin_type: lookup +""" + +EXAMPLES = """ +- name: "Get the configuration of a role." + ansible.builtin.debug: + msg: "Role host_manager: {{ role_config }}" + vars: + role_config: "{{ + lookup('checkmk.general.role', + 'host_manager', + server_url='https://myserver/', + site='mysite', + api_user='myuser', + api_secret='mysecret', + validate_certs=False + ) + }}" + +# --------------------------------------------------------------------------- +# Using variables from inventory +# --------------------------------------------------------------------------- +# Connection parameters can be provided via inventory variables instead of +# lookup parameters. The supported variables are: +# checkmk_var_server_url, checkmk_var_site, +# checkmk_var_api_user, checkmk_var_api_secret, +# checkmk_var_validate_certs + +- name: "Get a role configuration using inventory variables." + ansible.builtin.debug: + msg: "Role host_manager: {{ role_config }}" + vars: + checkmk_var_server_url: "https://myserver/" + checkmk_var_site: "mysite" + checkmk_var_api_user: "myuser" + checkmk_var_api_secret: "mysecret" + checkmk_var_validate_certs: false + role_config: "{{ lookup('checkmk.general.role', 'host_manager') }}" +""" + +RETURN = """ + _list: + description: + - The configuration of a particular role. + type: list + elements: dict +""" + +import json + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible_collections.checkmk.general.plugins.module_utils.lookup_api import ( + CheckMKLookupAPI, +) + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + self.set_options(var_options=variables, direct=kwargs) + server_url = self.get_option("server_url") + site = self.get_option("site") + api_auth_type = self.get_option("api_auth_type") or "bearer" + api_auth_cookie = self.get_option("api_auth_cookie") + api_user = self.get_option("api_user") + api_secret = self.get_option("api_secret") + validate_certs = self.get_option("validate_certs") + + site_url = server_url + "/" + site + + api = CheckMKLookupAPI( + site_url=site_url, + api_auth_type=api_auth_type, + api_auth_cookie=api_auth_cookie, + api_user=api_user, + api_secret=api_secret, + validate_certs=validate_certs, + ) + + ret = [] + + for term in terms: + response = json.loads(api.get("/objects/user_role/" + term)) + + if "code" in response: + raise AnsibleError( + "Received error for %s - %s: %s" + % ( + response.get("url", ""), + response.get("code", ""), + response.get("msg", ""), + ) + ) + + ret.append(response.get("extensions", {})) + + return ret diff --git a/plugins/lookup/roles.py b/plugins/lookup/roles.py new file mode 100644 index 000000000..7703c12f3 --- /dev/null +++ b/plugins/lookup/roles.py @@ -0,0 +1,127 @@ +# Copyright: (c) 2026, Robin Gierse +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: roles + author: Robin Gierse (@robin-checkmk) + version_added: "7.7.0" + + short_description: Get a list of all roles + + description: + - Returns a list of all user roles and their configuration, + including aliases, base roles, and assigned permissions. + + extends_documentation_fragment: [checkmk.general.common_lookup] + + notes: + - Like all lookups, this runs on the Ansible controller and is unaffected by other keywords such as 'become'. + If you need to use different permissions, you must change the command or run Ansible as another user. + - Alternatively, you can use a shell/command task that runs against localhost and registers the result. + - The directory of the play is used as the current working directory. + - It is B(NOT) possible to assign other variables to the variables mentioned in the C(vars) section! + This is a limitation of Ansible itself. + + seealso: + - module: checkmk.general.role + - plugin: checkmk.general.role + plugin_type: lookup +""" + +EXAMPLES = """ +- name: "Get all configured roles." + ansible.builtin.debug: + msg: "Role {{ item.id }}: {{ item.extensions }}" + loop: "{{ + lookup('checkmk.general.roles', + server_url='https://myserver/', + site='mysite', + api_user='myuser', + api_secret='mysecret', + validate_certs=False + ) + }}" + loop_control: + label: "{{ item.id }}" + +# --------------------------------------------------------------------------- +# Using variables from inventory +# --------------------------------------------------------------------------- +# Connection parameters can be provided via inventory variables instead of +# lookup parameters. The supported variables are: +# checkmk_var_server_url, checkmk_var_site, +# checkmk_var_api_user, checkmk_var_api_secret, +# checkmk_var_validate_certs + +- name: "Get all configured roles using inventory variables." + ansible.builtin.debug: + msg: "Role {{ item.id }}: {{ item.extensions }}" + vars: + checkmk_var_server_url: "https://myserver/" + checkmk_var_site: "mysite" + checkmk_var_api_user: "myuser" + checkmk_var_api_secret: "mysecret" + checkmk_var_validate_certs: false + loop: "{{ lookup('checkmk.general.roles') }}" + loop_control: + label: "{{ item.id }}" +""" + +RETURN = """ + _list: + description: + - A list of all roles and their configuration. + type: list + elements: dict +""" + +import json + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible_collections.checkmk.general.plugins.module_utils.lookup_api import ( + CheckMKLookupAPI, +) + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + self.set_options(var_options=variables, direct=kwargs) + server_url = self.get_option("server_url") + site = self.get_option("site") + api_auth_type = self.get_option("api_auth_type") or "bearer" + api_auth_cookie = self.get_option("api_auth_cookie") + api_user = self.get_option("api_user") + api_secret = self.get_option("api_secret") + validate_certs = self.get_option("validate_certs") + + site_url = server_url + "/" + site + + api = CheckMKLookupAPI( + site_url=site_url, + api_auth_type=api_auth_type, + api_auth_cookie=api_auth_cookie, + api_user=api_user, + api_secret=api_secret, + validate_certs=validate_certs, + ) + + response = json.loads(api.get("/domain-types/user_role/collections/all")) + + if "code" in response: + raise AnsibleError( + "Received error for %s - %s: %s" + % ( + response.get("url", ""), + response.get("code", ""), + response.get("msg", ""), + ) + ) + + role_list = response.get("value") + + return [role_list] diff --git a/plugins/modules/role.py b/plugins/modules/role.py new file mode 100644 index 000000000..c1efa38e6 --- /dev/null +++ b/plugins/modules/role.py @@ -0,0 +1,484 @@ +#!/usr/bin/python +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# Copyright: (c) 2026, Robin Gierse +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r""" +--- +module: role + +short_description: Manage roles in Checkmk + +version_added: "7.7.0" + +description: + - Manage roles within Checkmk. Custom roles are created by + cloning an existing built-in role. + - Built-in roles cannot be created or deleted, + but their permissions can be modified. + +extends_documentation_fragment: + - checkmk.general.common + +options: + name: + description: + - The internal ID of the role. This is used to uniquely + identify the role. It cannot be changed after creation. + required: true + type: str + title: + description: + - The human-readable title (alias) of the role. + - Optional when creating a new custom role. If omitted, the + title of the source role is used. + type: str + aliases: ["alias"] + based_on: + description: + - The ID of the built-in role to clone from when creating + a new custom role. + - This parameter is ignored when updating an existing role. + type: str + choices: ["admin", "user", "guest", "agent_registration"] + permissions: + description: + - A dictionary of permissions to set on the role. + - Keys are permission IDs (e.g., C(general.use), C(wato.edit), + C(wato.all_folders)). + - Values must be one of C(yes), C(no), or C(default). Values + must be quoted strings in YAML; unquoted C(yes) and C(no) are + interpreted as booleans and will be rejected. + - The value C(default) reverts a permission to the base role's + setting. It is only valid for custom roles. For built-in roles + (C(admin), C(user), C(guest), C(agent_registration)) use + C(yes) or C(no) explicitly. + - Permissions not listed here will remain unchanged. + - You can find the internal permission IDs in the Checkmk + GUI under I(Setup > Users > Roles & permissions) using + the inline help (available from Checkmk 2.4.0 onwards + via Werk #17953). + type: dict + state: + description: + - The desired state of the role. + - C(present) ensures the role exists with the specified + configuration. If the role does not exist, it will be + created by cloning the role specified in C(based_on). + - C(absent) ensures the custom role does not exist. + Built-in roles cannot be deleted. + type: str + default: present + choices: ["present", "absent"] + +author: + - "Robin Gierse (@robin-checkmk)" + +notes: + - Built-in roles cannot be created or deleted, but their permissions can be updated. + +seealso: + - module: checkmk.general.user + - module: checkmk.general.contact_group + - name: "Checkmk documentation on roles" + description: "Complete documentation for user roles and permissions." + link: "https://docs.checkmk.com/latest/en/wato_user.html" +""" + +EXAMPLES = r""" +# --------------------------------------------------------------------------- +# Create and delete roles +# --------------------------------------------------------------------------- + +- name: "Create a custom monitoring role." + checkmk.general.role: + server_url: "https://myserver/" + site: "mysite" + api_user: "myuser" + api_secret: "mysecret" + name: "limited_user" + title: "Limited Monitoring User" + based_on: "user" + state: "present" + +- name: "Create a custom role with tailored permissions." + checkmk.general.role: + server_url: "https://myserver/" + site: "mysite" + api_user: "myuser" + api_secret: "mysecret" + name: "host_manager" + title: "Host Manager" + based_on: "user" + permissions: + wato.all_folders: "yes" + wato.edit: "yes" + wato.manage_hosts: "yes" + general.edit_notifications: "no" + state: "present" + +- name: "Delete a custom role." + checkmk.general.role: + server_url: "https://myserver/" + site: "mysite" + api_user: "myuser" + api_secret: "mysecret" + name: "limited_user" + state: "absent" + +# --------------------------------------------------------------------------- +# Update permissions on existing roles +# --------------------------------------------------------------------------- + +- name: "Update permissions on an existing custom role." + checkmk.general.role: + server_url: "https://myserver/" + site: "mysite" + api_user: "myuser" + api_secret: "mysecret" + name: "host_manager" + permissions: + wato.all_folders: "yes" + state: "present" + +- name: "Modify permissions on the built-in user role." + checkmk.general.role: + server_url: "https://myserver/" + site: "mysite" + api_user: "myuser" + api_secret: "mysecret" + name: "user" + permissions: + general.edit_notifications: "no" + state: "present" + +# --------------------------------------------------------------------------- +# Using environment variables for authentication +# --------------------------------------------------------------------------- +# Connection parameters can be provided via environment variables instead of +# task parameters. The supported variables are: +# CHECKMK_VAR_SERVER_URL, CHECKMK_VAR_SITE, +# CHECKMK_VAR_API_USER, CHECKMK_VAR_API_SECRET, +# CHECKMK_VAR_VALIDATE_CERTS + +- name: "Create a custom role using environment variables for authentication." + checkmk.general.role: + name: "limited_user" + title: "Limited Monitoring User" + based_on: "user" + state: "present" + environment: + CHECKMK_VAR_SERVER_URL: "https://myserver/" + CHECKMK_VAR_SITE: "mysite" + CHECKMK_VAR_API_USER: "myuser" + CHECKMK_VAR_API_SECRET: "mysecret" + CHECKMK_VAR_VALIDATE_CERTS: "true" +""" + +RETURN = r""" +msg: + description: The output message that the module generates. Contains + the API response details in case of an error. + type: str + returned: always + sample: 'Role created.' +http_code: + description: The HTTP code the Checkmk API returns. + type: int + returned: always + sample: 200 +""" + +import json + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.checkmk.general.plugins.module_utils.api import ( + CheckmkAPI, +) +from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT +from ansible_collections.checkmk.general.plugins.module_utils.utils import ( + base_argument_spec, + exit_module, +) + +BUILTIN_ROLES = ("admin", "user", "guest", "agent_registration") +VALID_PERMISSION_VALUES = frozenset(("yes", "no", "default")) + +# 404 is not failed for GET — we use it to detect absence. +HTTP_CODES_GET = { + 200: (False, False, "Role found, nothing changed."), + 404: (False, False, "Role not found."), +} + +HTTP_CODES_CREATE = { + 200: (True, False, "Role created."), +} + +HTTP_CODES_EDIT = { + 200: (True, False, "Role updated."), +} + +HTTP_CODES_DELETE = { + 204: (True, False, "Role deleted."), +} + + +class RoleAPI(CheckmkAPI): + def __init__(self, module): + super().__init__(module) + + self.params = self.module.params + self.state = self.params.get("state") + self.name = self.params.get("name") + self.title = self.params.get("title") + self.based_on = self.params.get("based_on") + self.permissions = self.params.get("permissions") + + self.current = self._fetch( + code_mapping=HTTP_CODES_GET, + endpoint="/objects/user_role/%s" % self.name, + method="GET", + ) + + def _build_edit_data(self): + data = {} + + if self.title is not None: + data["new_alias"] = self.title + + if self.permissions is not None: + data["new_permissions"] = self.permissions + + return data + + def _get_base_role_permissions(self, based_on): + if not based_on: + return set() + result = self._fetch( + code_mapping={200: (False, False, "Base role found.")}, + endpoint="/objects/user_role/%s" % based_on, + method="GET", + ) + content = json.loads(result.content.decode("utf-8")) + return set(content.get("extensions", {}).get("permissions", [])) + + def _needs_update(self): + if self.current.http_code != 200: + return False + + current_content = json.loads(self.current.content.decode("utf-8")) + current_extensions = current_content.get("extensions", {}) + + if self.title is not None: + if current_extensions.get("alias", "") != self.title: + return True + + if self.permissions is not None: + current_perms = set(current_extensions.get("permissions", [])) + has_default = any(v == "default" for v in self.permissions.values()) + base_perms = ( + self._get_base_role_permissions(current_extensions.get("basedon", "")) + if has_default + else set() + ) + + for perm, value in self.permissions.items(): + if value == "yes" and perm not in current_perms: + return True + elif value == "no" and perm in current_perms: + return True + elif value == "default": + if (perm in base_perms) != (perm in current_perms): + return True + + return False + + def create(self): + data = { + "role_id": self.based_on, + "new_role_id": self.name, + } + + if self.title is not None: + data["new_alias"] = self.title + + # Note: permissions cannot be set on create via the API. + # They are applied via a follow-up PUT in run_module() if specified. + + return self._fetch( + code_mapping=HTTP_CODES_CREATE, + endpoint="/domain-types/user_role/collections/all", + data=data, + method="POST", + ) + + def edit(self): + data = self._build_edit_data() + + if not data: + return RESULT( + http_code=200, + msg="Role already up to date.", + content={}, + etag="", + failed=False, + changed=False, + ) + + if self.current.etag: + self.headers["If-Match"] = self.current.etag + return self._fetch( + code_mapping=HTTP_CODES_EDIT, + endpoint="/objects/user_role/%s" % self.name, + data=data, + method="PUT", + ) + + def delete(self): + if self.current.etag: + self.headers["If-Match"] = self.current.etag + return self._fetch( + code_mapping=HTTP_CODES_DELETE, + endpoint="/objects/user_role/%s" % self.name, + method="DELETE", + ) + + +def run_module(): + module_args = base_argument_spec() + module_args.update( + dict( + name=dict(type="str", required=True), + title=dict(type="str", aliases=["alias"]), + based_on=dict( + type="str", + choices=list(BUILTIN_ROLES), + ), + permissions=dict(type="dict"), + state=dict( + type="str", + default="present", + choices=["present", "absent"], + ), + ) + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + name = module.params.get("name") + permissions = module.params.get("permissions") + if permissions: + for perm, value in permissions.items(): + if value not in VALID_PERMISSION_VALUES: + module.fail_json( + msg="Invalid permission value '%s' for '%s'. Must be one of: %s." + % (value, perm, ", ".join(sorted(VALID_PERMISSION_VALUES))) + ) + if name in BUILTIN_ROLES and value == "default": + module.fail_json( + msg="Permission value 'default' is not valid for built-in role '%s'. " + "Built-in roles have no base role; use 'yes' or 'no' explicitly." + % name + ) + + role = RoleAPI(module) + result = RESULT( + http_code=0, + msg="Nothing to be done.", + content={}, + etag="", + failed=False, + changed=False, + ) + + if role.state == "present": + if role.current.http_code == 200: + if role._needs_update(): + if not module.check_mode: + result = role.edit() + else: + result = RESULT( + http_code=200, + msg="Role would be updated.", + content={}, + etag="", + failed=False, + changed=True, + ) + else: + result = RESULT( + http_code=200, + msg="Role already exists with desired state.", + content={}, + etag="", + failed=False, + changed=False, + ) + elif role.current.http_code == 404: + # based_on is only required for creation; on update it is ignored, + # so this check must run after the GET disambiguates the state. + if role.based_on is None: + module.fail_json( + msg="'based_on' is required when creating a new custom role." + ) + if not module.check_mode: + result = role.create() + # Permissions cannot be set during create; apply via PUT if needed. + # Null title so the follow-up edit only sends new_permissions. + if not result.failed and role.permissions is not None: + role.title = None + role.edit() + else: + result = RESULT( + http_code=200, + msg="Role would be created.", + content={}, + etag="", + failed=False, + changed=True, + ) + + elif role.state == "absent": + if role.current.http_code == 200: + if role.name in BUILTIN_ROLES: + module.fail_json( + msg="Built-in role '%s' cannot be deleted." % role.name + ) + if not module.check_mode: + result = role.delete() + else: + result = RESULT( + http_code=204, + msg="Role would be deleted.", + content={}, + etag="", + failed=False, + changed=True, + ) + elif role.current.http_code == 404: + result = RESULT( + http_code=0, + msg="Role already absent.", + content={}, + etag="", + failed=False, + changed=False, + ) + + exit_module(module, result=result) + + +def main(): + run_module() + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/lookup_role/tasks/main.yml b/tests/integration/targets/lookup_role/tasks/main.yml new file mode 100644 index 000000000..5ec7ab4db --- /dev/null +++ b/tests/integration/targets/lookup_role/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- name: "Include Global Variables." + ansible.builtin.include_vars: "{{ lookup('ansible.builtin.first_found', checkmk_var_params) }}" + vars: + checkmk_var_params: + files: + - global.yml + paths: + - /home/runner/work/ansible-collection-checkmk.general/ansible-collection-checkmk.general/ansible_collections/checkmk/general/tests/integration/files/includes/vars/ + - /root/ansible_collections/checkmk/general/tests/integration/files/includes/vars/ + - tests/integration/files/includes/vars/ + +- name: "Print Identifier." + ansible.builtin.debug: + msg: "{{ ansible_facts['system_vendor'] }} {{ ansible_facts['product_name'] }} running {{ ansible_facts['virtualization_type'] }}" + +- name: "Run preparations." + ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', checkmk_var_params) }}" + vars: + checkmk_var_params: + files: + - prep.yml + paths: + - /home/runner/work/ansible-collection-checkmk.general/ansible-collection-checkmk.general/ansible_collections/checkmk/general/tests/integration/files/includes/tasks/ + - /root/ansible_collections/checkmk/general/tests/integration/files/includes/tasks/ + - tests/integration/files/includes/tasks/ + when: + - ansible_facts['virtualization_type'] in ['container', 'podman', 'docker'] + - (ansible_facts['system_vendor'] == "Dell Inc." and 'Latitude' in ansible_facts['product_name']) or (ansible_facts['system_vendor'] == "QEMU" and 'Ubuntu' in ansible_facts['product_name']) + +- name: "Testing." + ansible.builtin.include_tasks: test.yml + loop: "{{ checkmk_var_test_sites }}" + loop_control: + loop_var: outer_item diff --git a/tests/integration/targets/lookup_role/tasks/test.yml b/tests/integration/targets/lookup_role/tasks/test.yml new file mode 100644 index 000000000..d3d916cae --- /dev/null +++ b/tests/integration/targets/lookup_role/tasks/test.yml @@ -0,0 +1,153 @@ +--- +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Set customer attribute." + ansible.builtin.set_fact: + checkmk_var_customer: "{{ 'provider' if outer_item.edition in ['managed', 'ultimatemt'] else None }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create custom roles for lookup tests." + checkmk.general.role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "{{ item.name }}" + title: "{{ item.title }}" + based_on: "{{ item.based_on }}" + state: "present" + loop: "{{ checkmk_var_roles }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Get all roles." + ansible.builtin.set_fact: + __checkmk_var_roles_list: "{{ lookup('checkmk.general.roles', + server_url=checkmk_var_server_url, + site=outer_item.site, + validate_certs=False, + api_user=checkmk_var_api_user, + api_secret=checkmk_var_api_secret) }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify all built-in roles are returned." + ansible.builtin.assert: + that: "item in (__checkmk_var_roles_list | map(attribute='id') | list)" + fail_msg: "Built-in role '{{ item }}' not found in lookup result!" + success_msg: "Built-in role '{{ item }}' is present." + loop: + - "admin" + - "user" + - "guest" + - "agent_registration" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify custom roles are returned." + ansible.builtin.assert: + that: "item.name in (__checkmk_var_roles_list | map(attribute='id') | list)" + fail_msg: "Custom role '{{ item.name }}' not found in lookup result!" + success_msg: "Custom role '{{ item.name }}' is present." + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify aliases of custom roles via list lookup." + ansible.builtin.assert: + that: "(__checkmk_var_roles_list | selectattr('id', 'equalto', item.name) | map(attribute='extensions.alias') | first) == item.title" + fail_msg: "Expected alias '{{ item.title }}' for role '{{ item.name }}' but got something else." + success_msg: "Alias for role '{{ item.name }}' is '{{ item.title }}' as expected." + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Get a single custom role." + ansible.builtin.set_fact: + __checkmk_var_role: "{{ lookup('checkmk.general.role', + item.name, + server_url=checkmk_var_server_url, + site=outer_item.site, + validate_certs=False, + api_user=checkmk_var_api_user, + api_secret=checkmk_var_api_secret) }}" + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + register: __checkmk_var_role_single_lookup + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify single role lookup matches expected alias." + ansible.builtin.assert: + that: "(lookup('checkmk.general.role', + item.name, + server_url=checkmk_var_server_url, + site=outer_item.site, + validate_certs=False, + api_user=checkmk_var_api_user, + api_secret=checkmk_var_api_secret)).alias == item.title" + fail_msg: "Expected alias '{{ item.title }}' for role '{{ item.name }}', got a mismatch." + success_msg: "Single role lookup of '{{ item.name }}' has alias '{{ item.title }}' as expected." + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify single role lookup reports correct basedon." + ansible.builtin.assert: + that: "(lookup('checkmk.general.role', + item.name, + server_url=checkmk_var_server_url, + site=outer_item.site, + validate_certs=False, + api_user=checkmk_var_api_user, + api_secret=checkmk_var_api_secret)).basedon == item.based_on" + fail_msg: "Expected basedon '{{ item.based_on }}' for role '{{ item.name }}'." + success_msg: "Role '{{ item.name }}' is based on '{{ item.based_on }}' as expected." + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Look up a built-in role." + ansible.builtin.set_fact: + __checkmk_var_builtin_role: "{{ lookup('checkmk.general.role', + 'admin', + server_url=checkmk_var_server_url, + site=outer_item.site, + validate_certs=False, + api_user=checkmk_var_api_user, + api_secret=checkmk_var_api_secret) }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify built-in admin role has permissions." + ansible.builtin.assert: + that: + - "__checkmk_var_builtin_role.permissions is defined" + - "__checkmk_var_builtin_role.permissions | length > 0" + fail_msg: "Built-in admin role should expose a non-empty permissions list." + success_msg: "Built-in admin role exposes {{ __checkmk_var_builtin_role.permissions | length }} permissions." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Use variables from inventory." + block: + + - name: "{{ outer_item.version }}.{{ outer_item.edition }} - Get all roles via inventory variables." + ansible.builtin.set_fact: + __checkmk_var_roles_list: "{{ lookup('checkmk.general.roles') }}" + + - name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify built-in roles via inventory variables." + ansible.builtin.assert: + that: "item in (__checkmk_var_roles_list | map(attribute='id') | list)" + fail_msg: "Built-in role '{{ item }}' not found via inventory variables!" + success_msg: "Built-in role '{{ item }}' is present." + loop: + - "admin" + - "user" + - "guest" + - "agent_registration" + + - name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify custom role lookup via inventory variables." + ansible.builtin.assert: + that: "(lookup('checkmk.general.role', item.name)).alias == item.title" + fail_msg: "Expected alias '{{ item.title }}' for role '{{ item.name }}' via inventory variables." + success_msg: "Single role lookup of '{{ item.name }}' via inventory variables has alias '{{ item.title }}'." + loop: "{{ checkmk_var_roles }}" + loop_control: + label: "{{ item.name }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Delete custom roles (cleanup)." + checkmk.general.role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "{{ item.name }}" + state: "absent" + loop: "{{ checkmk_var_roles }}" diff --git a/tests/integration/targets/lookup_role/vars/main.yml b/tests/integration/targets/lookup_role/vars/main.yml new file mode 100644 index 000000000..e5ed110d4 --- /dev/null +++ b/tests/integration/targets/lookup_role/vars/main.yml @@ -0,0 +1,13 @@ +--- +checkmk_var_test_sites: + - version: "{{ checkmk_var_version }}" + edition: "{{ checkmk_var_edition }}" + site: "testsite" + +checkmk_var_roles: + - name: "lookup_test_user_role" + title: "Lookup Test User Role" + based_on: "user" + - name: "lookup_test_admin_role" + title: "Lookup Test Admin Role" + based_on: "admin" diff --git a/tests/integration/targets/role/tasks/main.yml b/tests/integration/targets/role/tasks/main.yml new file mode 100644 index 000000000..5ec7ab4db --- /dev/null +++ b/tests/integration/targets/role/tasks/main.yml @@ -0,0 +1,35 @@ +--- +- name: "Include Global Variables." + ansible.builtin.include_vars: "{{ lookup('ansible.builtin.first_found', checkmk_var_params) }}" + vars: + checkmk_var_params: + files: + - global.yml + paths: + - /home/runner/work/ansible-collection-checkmk.general/ansible-collection-checkmk.general/ansible_collections/checkmk/general/tests/integration/files/includes/vars/ + - /root/ansible_collections/checkmk/general/tests/integration/files/includes/vars/ + - tests/integration/files/includes/vars/ + +- name: "Print Identifier." + ansible.builtin.debug: + msg: "{{ ansible_facts['system_vendor'] }} {{ ansible_facts['product_name'] }} running {{ ansible_facts['virtualization_type'] }}" + +- name: "Run preparations." + ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', checkmk_var_params) }}" + vars: + checkmk_var_params: + files: + - prep.yml + paths: + - /home/runner/work/ansible-collection-checkmk.general/ansible-collection-checkmk.general/ansible_collections/checkmk/general/tests/integration/files/includes/tasks/ + - /root/ansible_collections/checkmk/general/tests/integration/files/includes/tasks/ + - tests/integration/files/includes/tasks/ + when: + - ansible_facts['virtualization_type'] in ['container', 'podman', 'docker'] + - (ansible_facts['system_vendor'] == "Dell Inc." and 'Latitude' in ansible_facts['product_name']) or (ansible_facts['system_vendor'] == "QEMU" and 'Ubuntu' in ansible_facts['product_name']) + +- name: "Testing." + ansible.builtin.include_tasks: test.yml + loop: "{{ checkmk_var_test_sites }}" + loop_control: + loop_var: outer_item diff --git a/tests/integration/targets/role/tasks/test.yml b/tests/integration/targets/role/tasks/test.yml new file mode 100644 index 000000000..fa3686384 --- /dev/null +++ b/tests/integration/targets/role/tasks/test.yml @@ -0,0 +1,440 @@ +--- +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Set customer attribute." + ansible.builtin.set_fact: + checkmk_var_customer: "{{ 'provider' if outer_item.edition in ['managed', 'ultimatemt'] else None }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create a custom role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_custom_role" + title: "Test Custom Role" + based_on: "user" + state: "present" + register: __checkmk_var_role_create + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role creation." + ansible.builtin.assert: + that: + - __checkmk_var_role_create.changed == true + - "'Role created.' in __checkmk_var_role_create.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create the same custom role again (idempotency)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_custom_role" + title: "Test Custom Role" + based_on: "user" + state: "present" + register: __checkmk_var_role_create_again + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify idempotency." + ansible.builtin.assert: + that: + - __checkmk_var_role_create_again.changed == false + - __checkmk_var_role_create_again.msg == "Role already exists with desired state." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create a custom role with permissions." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + title: "Test Role With Permissions" + based_on: "user" + permissions: + general.edit_notifications: "no" + wato.all_folders: "yes" + state: "present" + register: __checkmk_var_role_create_with_perms + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role creation with permissions." + ansible.builtin.assert: + that: + - __checkmk_var_role_create_with_perms.changed == true + - "'Role created.' in __checkmk_var_role_create_with_perms.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create the same role with permissions again (idempotency)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + title: "Test Role With Permissions" + permissions: + general.edit_notifications: "no" + wato.all_folders: "yes" + state: "present" + register: __checkmk_var_role_create_with_perms_again + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify idempotency with permissions." + ansible.builtin.assert: + that: + - __checkmk_var_role_create_with_perms_again.changed == false + - __checkmk_var_role_create_with_perms_again.msg == "Role already exists with desired state." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Activate." + activation: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + force_foreign_changes: true + sites: + - "{{ outer_item.site }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Set a permission to default on a custom role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + permissions: + wato.all_folders: "default" + state: "present" + register: __checkmk_var_role_set_default_perm + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify setting permission to default changes role." + ansible.builtin.assert: + that: + - __checkmk_var_role_set_default_perm.changed == true + - "'Role updated.' in __checkmk_var_role_set_default_perm.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Set the same permission to default again (idempotency)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + permissions: + wato.all_folders: "default" + state: "present" + register: __checkmk_var_role_set_default_perm_again + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify default permission is idempotent." + ansible.builtin.assert: + that: + - __checkmk_var_role_set_default_perm_again.changed == false + - __checkmk_var_role_set_default_perm_again.msg == "Role already exists with desired state." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Update the title of an existing custom role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_custom_role" + title: "Updated Custom Role Title" + state: "present" + register: __checkmk_var_role_update_title + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify title update." + ansible.builtin.assert: + that: + - __checkmk_var_role_update_title.changed == true + - "'Role updated.' in __checkmk_var_role_update_title.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Update permissions on an existing custom role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + permissions: + general.edit_notifications: "yes" + state: "present" + register: __checkmk_var_role_update_perms + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify permissions update." + ansible.builtin.assert: + that: + - __checkmk_var_role_update_perms.changed == true + - "'Role updated.' in __checkmk_var_role_update_perms.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Activate." + activation: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + force_foreign_changes: true + sites: + - "{{ outer_item.site }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Modify permissions on the built-in guest role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "guest" + permissions: + general.csv_export: "no" + state: "present" + register: __checkmk_var_role_builtin_modify + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify built-in role modification." + ansible.builtin.assert: + that: + - __checkmk_var_role_builtin_modify.changed == true + - "'Role updated.' in __checkmk_var_role_builtin_modify.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Revert built-in guest role permissions (cleanup)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "guest" + permissions: + general.csv_export: "yes" + state: "present" + register: __checkmk_var_role_revert_guest + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify guest role was reverted." + ansible.builtin.assert: + that: + - __checkmk_var_role_revert_guest.changed == true + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Activate." + activation: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + force_foreign_changes: true + sites: + - "{{ outer_item.site }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Try to delete a built-in role (should fail)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "guest" + state: "absent" + register: __checkmk_var_role_delete_builtin + ignore_errors: true + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify built-in role deletion fails." + ansible.builtin.assert: + that: + - __checkmk_var_role_delete_builtin.failed == true + - "'cannot be deleted' in __checkmk_var_role_delete_builtin.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Try to create a custom role without based_on (should fail)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "nonexistent_role" + title: "This Should Fail" + state: "present" + register: __checkmk_var_role_create_no_basedon + ignore_errors: true + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify creation without based_on fails." + ansible.builtin.assert: + that: + - __checkmk_var_role_create_no_basedon.failed == true + - "'based_on' in __checkmk_var_role_create_no_basedon.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Delete a custom role." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_custom_role" + state: "absent" + register: __checkmk_var_role_delete + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role deletion." + ansible.builtin.assert: + that: + - __checkmk_var_role_delete.changed == true + - "'Role deleted.' in __checkmk_var_role_delete.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Delete the same custom role again (idempotency)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_custom_role" + state: "absent" + register: __checkmk_var_role_delete_again + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify deletion idempotency." + ansible.builtin.assert: + that: + - __checkmk_var_role_delete_again.changed == false + - __checkmk_var_role_delete_again.msg == "Role already absent." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Delete the role with permissions (cleanup)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_role_with_perms" + state: "absent" + register: __checkmk_var_role_cleanup + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify cleanup deletion." + ansible.builtin.assert: + that: + - __checkmk_var_role_cleanup.changed == true + - "'Role deleted.' in __checkmk_var_role_cleanup.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Activate." + activation: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + force_foreign_changes: true + sites: + - "{{ outer_item.site }}" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Test check_mode for role creation." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_role" + title: "Check Mode Role" + based_on: "user" + state: "present" + check_mode: true + register: __checkmk_var_role_checkmode_create + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify check_mode for creation." + ansible.builtin.assert: + that: + - __checkmk_var_role_checkmode_create.changed == true + - __checkmk_var_role_checkmode_create.msg == "Role would be created." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role was not actually created in check_mode." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_role" + state: "absent" + register: __checkmk_var_role_verify_checkmode_create + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role does not exist after check_mode." + ansible.builtin.assert: + that: + - __checkmk_var_role_verify_checkmode_create.changed == false + - __checkmk_var_role_verify_checkmode_create.msg == "Role already absent." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Create a role for check_mode update and delete tests." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_ops" + title: "Check Mode Ops Role" + based_on: "user" + state: "present" + register: __checkmk_var_role_create_checkmode_ops + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify check_mode ops role was created." + ansible.builtin.assert: + that: + - __checkmk_var_role_create_checkmode_ops.changed == true + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Test check_mode for role update." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_ops" + title: "Would Be Updated Title" + state: "present" + check_mode: true + register: __checkmk_var_role_checkmode_update + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify check_mode for update." + ansible.builtin.assert: + that: + - __checkmk_var_role_checkmode_update.changed == true + - __checkmk_var_role_checkmode_update.msg == "Role would be updated." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role was not actually updated in check_mode." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_ops" + title: "Check Mode Ops Role" + state: "present" + register: __checkmk_var_role_verify_checkmode_update + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify title unchanged after check_mode update." + ansible.builtin.assert: + that: + - __checkmk_var_role_verify_checkmode_update.changed == false + - __checkmk_var_role_verify_checkmode_update.msg == "Role already exists with desired state." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Test check_mode for role deletion." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_ops" + state: "absent" + check_mode: true + register: __checkmk_var_role_checkmode_delete + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify check_mode for deletion." + ansible.builtin.assert: + that: + - __checkmk_var_role_checkmode_delete.changed == true + - __checkmk_var_role_checkmode_delete.msg == "Role would be deleted." + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Delete check_mode ops role (cleanup)." + role: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + name: "test_checkmode_ops" + state: "absent" + register: __checkmk_var_role_cleanup_checkmode + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Verify role existed after check_mode delete." + ansible.builtin.assert: + that: + - __checkmk_var_role_cleanup_checkmode.changed == true + - "'Role deleted.' in __checkmk_var_role_cleanup_checkmode.msg" + +- name: "{{ outer_item.version }}.{{ outer_item.edition }} - Activate." + activation: + server_url: "{{ checkmk_var_server_url }}" + site: "{{ outer_item.site }}" + api_user: "{{ checkmk_var_api_user }}" + api_secret: "{{ checkmk_var_api_secret }}" + force_foreign_changes: true + sites: + - "{{ outer_item.site }}" diff --git a/tests/integration/targets/role/vars/main.yml b/tests/integration/targets/role/vars/main.yml new file mode 100644 index 000000000..7dbdddd3f --- /dev/null +++ b/tests/integration/targets/role/vars/main.yml @@ -0,0 +1,5 @@ +--- +checkmk_var_test_sites: + - version: "{{ checkmk_var_version }}" + edition: "{{ checkmk_var_edition }}" + site: "testsite" diff --git a/tests/unit/plugins/modules/test_role.py b/tests/unit/plugins/modules/test_role.py new file mode 100644 index 000000000..f7993895a --- /dev/null +++ b/tests/unit/plugins/modules/test_role.py @@ -0,0 +1,727 @@ +#!/usr/bin/env python +# -*- encoding: utf-8; py-indent-offset: 4 -*- + +# Copyright: (c) 2025, Checkmk GmbH +# GNU General Public License v3.0+ +# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json +from unittest.mock import MagicMock, patch + +import pytest + +from ansible_collections.checkmk.general.plugins.module_utils.types import RESULT +from ansible_collections.checkmk.general.plugins.modules.role import ( + BUILTIN_ROLES, + VALID_PERMISSION_VALUES, + RoleAPI, + run_module, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +COMMON_PARAMS = { + "server_url": "https://localhost/", + "site": "mysite", + "api_user": "cmkadmin", + "api_secret": "mysecret", + "validate_certs": True, +} + +ROLE_GET_RESPONSE_200 = RESULT( + http_code=200, + msg="Role found, nothing changed.", + content=json.dumps( + { + "id": "test_role", + "extensions": { + "alias": "Test Role", + "permissions": ["general.use"], + "builtin": False, + "basedon": "user", + }, + } + ).encode("utf-8"), + etag="", + failed=False, + changed=False, +) + +ROLE_GET_RESPONSE_404 = RESULT( + http_code=404, + msg="Role not found.", + content={}, + etag="", + failed=False, + changed=False, +) + +ROLE_CREATE_RESPONSE = RESULT( + http_code=200, + msg="Role created.", + content={}, + etag="", + failed=False, + changed=True, +) + +ROLE_EDIT_RESPONSE = RESULT( + http_code=200, + msg="Role updated.", + content={}, + etag="", + failed=False, + changed=True, +) + +ROLE_DELETE_RESPONSE = RESULT( + http_code=204, + msg="Role deleted.", + content={}, + etag="", + failed=False, + changed=True, +) + + +def _make_module_params(**overrides): + params = dict(COMMON_PARAMS) + params.update( + { + "name": "test_role", + "title": None, + "based_on": None, + "permissions": None, + "state": "present", + } + ) + params.update(overrides) + return params + + +def _mock_role_api(module_params, current_result): + mock_module = MagicMock() + mock_module.params = module_params + mock_module.check_mode = False + + with patch.object(RoleAPI, "__init__", lambda self, mod: None): + api = RoleAPI.__new__(RoleAPI) + api.module = mock_module + api.params = module_params + api.state = module_params["state"] + api.name = module_params["name"] + api.title = module_params.get("title") + api.based_on = module_params.get("based_on") + api.permissions = module_params.get("permissions") + api.current = current_result + api.headers = {} + + return api + + +# --------------------------------------------------------------------------- +# Tests for constants +# --------------------------------------------------------------------------- + + +class TestConstants: + def test_builtin_roles_contains_expected(self): + assert "admin" in BUILTIN_ROLES + assert "user" in BUILTIN_ROLES + assert "guest" in BUILTIN_ROLES + assert "agent_registration" in BUILTIN_ROLES + + def test_builtin_roles_length(self): + assert len(BUILTIN_ROLES) == 4 + + def test_valid_permission_values(self): + assert VALID_PERMISSION_VALUES == frozenset({"yes", "no", "default"}) + + +# --------------------------------------------------------------------------- +# Tests for RoleAPI._needs_update +# --------------------------------------------------------------------------- + + +class TestNeedsUpdate: + def test_no_update_when_role_not_found(self): + params = _make_module_params(title="New Title") + api = _mock_role_api(params, ROLE_GET_RESPONSE_404) + assert api._needs_update() is False + + def test_no_update_when_nothing_specified(self): + params = _make_module_params() + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is False + + def test_update_needed_when_title_differs(self): + params = _make_module_params(title="Different Title") + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is True + + def test_no_update_when_title_matches(self): + params = _make_module_params(title="Test Role") + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is False + + def test_update_needed_when_permission_value_differs(self): + params = _make_module_params( + permissions={ + "wato.edit": "yes" + } # wato.edit absent from enabled list → needs enabling + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is True + + def test_no_update_when_permission_matches(self): + params = _make_module_params( + permissions={ + "wato.edit": "no" + } # wato.edit absent from enabled list → already disabled + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is False + + def test_update_needed_when_new_permission_added(self): + params = _make_module_params( + permissions={"wato.all_folders": "yes"} # not in current + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is True + + def test_no_update_when_default_matches_base(self): + # general.use is in both base and current perms -> "default" = no change + params = _make_module_params(permissions={"general.use": "default"}) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + with patch.object( + api, "_get_base_role_permissions", return_value={"general.use"} + ) as mock_get_base: + assert api._needs_update() is False + mock_get_base.assert_called_once_with("user") + + def test_update_needed_when_default_differs_from_base(self): + # wato.edit in base but NOT in current -> "default" should restore it -> update needed + params = _make_module_params(permissions={"wato.edit": "default"}) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + with patch.object( + api, "_get_base_role_permissions", return_value={"wato.edit"} + ) as mock_get_base: + assert api._needs_update() is True + mock_get_base.assert_called_once_with("user") + + def test_update_needed_when_both_title_and_permissions_differ(self): + params = _make_module_params( + title="Changed Title", + permissions={"wato.edit": "yes"}, + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is True + + def test_no_update_when_title_and_permissions_match(self): + params = _make_module_params( + title="Test Role", + permissions={ + "general.use": "yes", + "wato.edit": "no", + }, + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._needs_update() is False + + +# --------------------------------------------------------------------------- +# Tests for RoleAPI._build_edit_data +# --------------------------------------------------------------------------- + + +class TestBuildEditData: + def test_empty_when_nothing_set(self): + params = _make_module_params() + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + assert api._build_edit_data() == {} + + def test_includes_alias_when_title_set(self): + params = _make_module_params(title="New Title") + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + data = api._build_edit_data() + assert data == {"new_alias": "New Title"} + + def test_includes_permissions_when_set(self): + perms = {"wato.edit": "yes", "wato.all_folders": "no"} + params = _make_module_params(permissions=perms) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + data = api._build_edit_data() + assert data == {"new_permissions": perms} + + def test_includes_both_when_both_set(self): + perms = {"wato.edit": "yes"} + params = _make_module_params(title="My Title", permissions=perms) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + data = api._build_edit_data() + assert data == {"new_alias": "My Title", "new_permissions": perms} + + +# --------------------------------------------------------------------------- +# Tests for RoleAPI.create +# --------------------------------------------------------------------------- + + +class TestCreate: + def test_create_sends_correct_data(self): + params = _make_module_params( + name="new_role", + title="New Role", + based_on="user", + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_404) + api._fetch = MagicMock(return_value=ROLE_CREATE_RESPONSE) + + result = api.create() + + api._fetch.assert_called_once() + call_kwargs = api._fetch.call_args + data = call_kwargs.kwargs.get("data") or call_kwargs[1].get("data") + assert data["role_id"] == "user" + assert data["new_role_id"] == "new_role" + assert data["new_alias"] == "New Role" + assert "permissions" not in data + assert result.changed is True + + def test_create_without_optional_fields(self): + params = _make_module_params( + name="minimal_role", + based_on="guest", + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_404) + api._fetch = MagicMock(return_value=ROLE_CREATE_RESPONSE) + + api.create() + + call_kwargs = api._fetch.call_args + data = call_kwargs.kwargs.get("data") or call_kwargs[1].get("data") + assert data["role_id"] == "guest" + assert data["new_role_id"] == "minimal_role" + assert "new_alias" not in data + assert "permissions" not in data + + +# --------------------------------------------------------------------------- +# Tests for RoleAPI.edit +# --------------------------------------------------------------------------- + + +class TestEdit: + def test_edit_returns_no_change_when_no_data(self): + params = _make_module_params() + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + + result = api.edit() + + assert result.changed is False + assert result.msg == "Role already up to date." + + def test_edit_sends_correct_data(self): + params = _make_module_params( + title="Updated Title", + permissions={"wato.all_folders": "yes"}, + ) + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + api._fetch = MagicMock(return_value=ROLE_EDIT_RESPONSE) + + result = api.edit() + + api._fetch.assert_called_once() + call_kwargs = api._fetch.call_args + data = call_kwargs.kwargs.get("data") or call_kwargs[1].get("data") + assert data["new_alias"] == "Updated Title" + assert data["new_permissions"] == {"wato.all_folders": "yes"} + assert result.changed is True + + +# --------------------------------------------------------------------------- +# Tests for RoleAPI.delete +# --------------------------------------------------------------------------- + + +class TestDelete: + def test_delete_calls_correct_endpoint(self): + params = _make_module_params(name="test_role", state="absent") + api = _mock_role_api(params, ROLE_GET_RESPONSE_200) + api._fetch = MagicMock(return_value=ROLE_DELETE_RESPONSE) + + result = api.delete() + + api._fetch.assert_called_once() + call_kwargs = api._fetch.call_args + endpoint = call_kwargs.kwargs.get("endpoint") or call_kwargs[1].get("endpoint") + assert "test_role" in endpoint + method = call_kwargs.kwargs.get("method") or call_kwargs[1].get("method") + assert method == "DELETE" + assert result.changed is True + + +# --------------------------------------------------------------------------- +# Tests for run_module (integration of the logic) +# --------------------------------------------------------------------------- + + +class TestRunModule: + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_present_creates_new_role(self, mock_ansible_module_cls, mock_role_api_cls): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="new_role", title="New", based_on="user" + ) + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "new_role" + mock_api.current = ROLE_GET_RESPONSE_404 + mock_api.based_on = "user" + mock_api.permissions = None + mock_api.create.return_value = ROLE_CREATE_RESPONSE + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.create.assert_called_once() + mock_api.edit.assert_not_called() + mock_module.exit_json.assert_called_once() + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_present_creates_role_with_permissions( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="new_role", + title="New", + based_on="user", + permissions={"wato.all_folders": "yes"}, + ) + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "new_role" + mock_api.current = ROLE_GET_RESPONSE_404 + mock_api.based_on = "user" + mock_api.permissions = {"wato.all_folders": "yes"} + mock_api.create.return_value = ROLE_CREATE_RESPONSE + mock_api.edit.return_value = ROLE_EDIT_RESPONSE + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.create.assert_called_once() + mock_api.edit.assert_called_once() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is True + assert call_kwargs["msg"] == "Role created." + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_title_nulled_before_postcreate_edit( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="new_role", + title="New", + based_on="user", + permissions={"wato.all_folders": "yes"}, + ) + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "new_role" + mock_api.current = ROLE_GET_RESPONSE_404 + mock_api.based_on = "user" + mock_api.permissions = {"wato.all_folders": "yes"} + mock_api.title = "New" + mock_api.create.return_value = ROLE_CREATE_RESPONSE + + captured_title = [] + + def capture_title(): + captured_title.append(mock_api.title) + return ROLE_EDIT_RESPONSE + + mock_api.edit.side_effect = capture_title + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.edit.assert_called_once() + assert captured_title[0] is None + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_present_updates_existing_role( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="test_role", title="Changed") + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "test_role" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_api._needs_update.return_value = True + mock_api.edit.return_value = ROLE_EDIT_RESPONSE + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.edit.assert_called_once() + mock_module.exit_json.assert_called_once() + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_present_no_change_when_up_to_date( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="test_role", title="Test Role") + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "test_role" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_api._needs_update.return_value = False + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.create.assert_not_called() + mock_api.edit.assert_not_called() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is False + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_present_fails_without_based_on( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="new_role", title="New", based_on=None + ) + mock_module.check_mode = False + mock_module.fail_json.side_effect = SystemExit + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "new_role" + mock_api.based_on = None + mock_api.current = ROLE_GET_RESPONSE_404 + mock_role_api_cls.return_value = mock_api + + with pytest.raises(SystemExit): + run_module() + + mock_module.fail_json.assert_called_once() + fail_kwargs = mock_module.fail_json.call_args[1] + assert "based_on" in fail_kwargs["msg"] + mock_api.create.assert_not_called() + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_absent_deletes_custom_role( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="test_role", state="absent") + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "absent" + mock_api.name = "test_role" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_api.delete.return_value = ROLE_DELETE_RESPONSE + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.delete.assert_called_once() + mock_module.exit_json.assert_called_once() + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_absent_fails_for_builtin_role( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="admin", state="absent") + mock_module.check_mode = False + mock_module.fail_json.side_effect = SystemExit + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "absent" + mock_api.name = "admin" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_role_api_cls.return_value = mock_api + + with pytest.raises(SystemExit): + run_module() + + mock_module.fail_json.assert_called_once() + fail_kwargs = mock_module.fail_json.call_args[1] + assert "cannot be deleted" in fail_kwargs["msg"] + mock_api.delete.assert_not_called() + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_absent_no_change_when_already_absent( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="gone_role", state="absent") + mock_module.check_mode = False + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "absent" + mock_api.name = "gone_role" + mock_api.current = ROLE_GET_RESPONSE_404 + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.delete.assert_not_called() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is False + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_check_mode_create(self, mock_ansible_module_cls, mock_role_api_cls): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="new_role", title="New", based_on="user" + ) + mock_module.check_mode = True + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "new_role" + mock_api.based_on = "user" + mock_api.current = ROLE_GET_RESPONSE_404 + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.create.assert_not_called() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is True + assert call_kwargs["msg"] == "Role would be created." + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_check_mode_update(self, mock_ansible_module_cls, mock_role_api_cls): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="test_role", title="Changed") + mock_module.check_mode = True + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "present" + mock_api.name = "test_role" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_api._needs_update.return_value = True + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.edit.assert_not_called() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is True + assert call_kwargs["msg"] == "Role would be updated." + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_invalid_permission_value_fails( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params(permissions={"wato.edit": "maybe"}) + mock_module.check_mode = False + mock_module.fail_json.side_effect = SystemExit + mock_ansible_module_cls.return_value = mock_module + + with pytest.raises(SystemExit): + run_module() + + mock_module.fail_json.assert_called_once() + fail_kwargs = mock_module.fail_json.call_args[1] + assert "maybe" in fail_kwargs["msg"] + assert "wato.edit" in fail_kwargs["msg"] + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_default_permission_on_builtin_role_fails( + self, mock_ansible_module_cls, mock_role_api_cls + ): + mock_module = MagicMock() + mock_module.params = _make_module_params( + name="guest", + permissions={"general.csv_export": "default"}, + ) + mock_module.check_mode = False + mock_module.fail_json.side_effect = SystemExit + mock_ansible_module_cls.return_value = mock_module + + with pytest.raises(SystemExit): + run_module() + + mock_module.fail_json.assert_called_once() + fail_kwargs = mock_module.fail_json.call_args[1] + assert "default" in fail_kwargs["msg"] + assert "guest" in fail_kwargs["msg"] + + @patch("ansible_collections.checkmk.general.plugins.modules.role.RoleAPI") + @patch("ansible_collections.checkmk.general.plugins.modules.role.AnsibleModule") + def test_check_mode_delete(self, mock_ansible_module_cls, mock_role_api_cls): + mock_module = MagicMock() + mock_module.params = _make_module_params(name="test_role", state="absent") + mock_module.check_mode = True + mock_ansible_module_cls.return_value = mock_module + + mock_api = MagicMock() + mock_api.state = "absent" + mock_api.name = "test_role" + mock_api.current = ROLE_GET_RESPONSE_200 + mock_role_api_cls.return_value = mock_api + + run_module() + + mock_api.delete.assert_not_called() + mock_module.exit_json.assert_called_once() + call_kwargs = mock_module.exit_json.call_args[1] + assert call_kwargs["changed"] is True + assert call_kwargs["msg"] == "Role would be deleted."