|
| 1 | +#!/usr/bin/python |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +# |
| 4 | +# Copyright: (c) 2024, ONODERA Masaru <[email protected]> |
| 5 | +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) |
| 6 | + |
| 7 | + |
| 8 | +from __future__ import absolute_import, division, print_function |
| 9 | + |
| 10 | +__metaclass__ = type |
| 11 | + |
| 12 | + |
| 13 | +DOCUMENTATION = """ |
| 14 | +--- |
| 15 | +module: zabbix_mfa |
| 16 | +
|
| 17 | +short_description: Create/update/delete Zabbix MFA method |
| 18 | +
|
| 19 | +
|
| 20 | +description: |
| 21 | + - This module allows you to create, update and delete Zabbix MFA method. |
| 22 | +
|
| 23 | +author: |
| 24 | + - ONODERA Masaru(@masa-orca) |
| 25 | +
|
| 26 | +requirements: |
| 27 | + - "python >= 3.11" |
| 28 | +
|
| 29 | +version_added: 3.1.0 |
| 30 | +
|
| 31 | +options: |
| 32 | + name: |
| 33 | + description: |
| 34 | + - Name of this MFA method |
| 35 | + type: str |
| 36 | + required: true |
| 37 | + method_type: |
| 38 | + description: |
| 39 | + - A type of this MFA method |
| 40 | + type: str |
| 41 | + choices: |
| 42 | + - "totp" |
| 43 | + - "duo_universal_prompt" |
| 44 | + hash_function: |
| 45 | + description: |
| 46 | + - Type of the hash function for generating TOTP codes. |
| 47 | + - Required when C(method_type=totp). |
| 48 | + type: str |
| 49 | + choices: |
| 50 | + - "sha-1" |
| 51 | + - "sha-256" |
| 52 | + - "sha-512" |
| 53 | + code_length: |
| 54 | + description: |
| 55 | + - Verification code length. |
| 56 | + - Required when C(method_type=totp). |
| 57 | + type: int |
| 58 | + choices: |
| 59 | + - 6 |
| 60 | + - 8 |
| 61 | + api_hostname: |
| 62 | + description: |
| 63 | + - API hostname provided by the Duo authentication service. |
| 64 | + - Required when C(method_type=duo_universal_prompt). |
| 65 | + type: str |
| 66 | + clientid: |
| 67 | + description: |
| 68 | + - Client ID provided by the Duo authentication service. |
| 69 | + - Required when C(method_type=duo_universal_prompt). |
| 70 | + type: str |
| 71 | + client_secret: |
| 72 | + description: |
| 73 | + - Client secret provided by the Duo authentication service. |
| 74 | + - Required when C(method_type=duo_universal_prompt). |
| 75 | + type: str |
| 76 | + state: |
| 77 | + description: |
| 78 | + - State of this MFA. |
| 79 | + type: str |
| 80 | + choices: ['present', 'absent'] |
| 81 | + default: 'present' |
| 82 | +
|
| 83 | +
|
| 84 | +notes: |
| 85 | + - Only Zabbix >= 7.0 is supported. |
| 86 | + - This module returns changed=true when I(method_type) is C(duo_universal_prompt) as Zabbix API |
| 87 | + will not return any sensitive information back for module to compare. |
| 88 | +
|
| 89 | +extends_documentation_fragment: |
| 90 | + - community.zabbix.zabbix |
| 91 | +
|
| 92 | +""" |
| 93 | + |
| 94 | +EXAMPLES = """ |
| 95 | +# If you want to use Username and Password to be authenticated by Zabbix Server |
| 96 | +- name: Set credentials to access Zabbix Server API |
| 97 | + ansible.builtin.set_fact: |
| 98 | + ansible_user: Admin |
| 99 | + ansible_httpapi_pass: zabbix |
| 100 | +
|
| 101 | +# If you want to use API token to be authenticated by Zabbix Server |
| 102 | +# https://www.zabbix.com/documentation/current/en/manual/web_interface/frontend_sections/administration/general#api-tokens |
| 103 | +- name: Set API token |
| 104 | + ansible.builtin.set_fact: |
| 105 | + ansible_zabbix_auth_key: 8ec0d52432c15c91fcafe9888500cf9a607f44091ab554dbee860f6b44fac895 |
| 106 | +
|
| 107 | +- name: Create a 'Zabbix TOTP' MFA method |
| 108 | + # set task level variables as we change ansible_connection plugin here |
| 109 | + vars: |
| 110 | + ansible_network_os: community.zabbix.zabbix |
| 111 | + ansible_connection: httpapi |
| 112 | + ansible_httpapi_port: 443 |
| 113 | + ansible_httpapi_use_ssl: true |
| 114 | + ansible_httpapi_validate_certs: false |
| 115 | + ansible_zabbix_url_path: 'zabbixeu' # If Zabbix WebUI runs on non-default (zabbix) path ,e.g. http://<FQDN>/zabbixeu |
| 116 | + ansible_host: zabbix-example-fqdn.org |
| 117 | + community.zabbix.zabbix_mfa: |
| 118 | + name: Zabbix TOTP |
| 119 | + method_type: totp |
| 120 | + hash_function: sha-1 |
| 121 | + code_length: 6 |
| 122 | +""" |
| 123 | + |
| 124 | +RETURN = """ |
| 125 | +msg: |
| 126 | + description: The result of the creating operation |
| 127 | + returned: success |
| 128 | + type: str |
| 129 | + sample: 'Successfully created MFA method' |
| 130 | +""" |
| 131 | + |
| 132 | + |
| 133 | +from ansible.module_utils.basic import AnsibleModule |
| 134 | + |
| 135 | +from ansible_collections.community.zabbix.plugins.module_utils.base import ZabbixBase |
| 136 | +from ansible.module_utils.compat.version import LooseVersion |
| 137 | + |
| 138 | +import ansible_collections.community.zabbix.plugins.module_utils.helpers as zabbix_utils |
| 139 | + |
| 140 | + |
| 141 | +class MFA(ZabbixBase): |
| 142 | + def __init__(self, module, zbx=None, zapi_wrapper=None): |
| 143 | + super(MFA, self).__init__(module, zbx, zapi_wrapper) |
| 144 | + if LooseVersion(self._zbx_api_version) < LooseVersion("7.0"): |
| 145 | + module.fail_json( |
| 146 | + msg="This module doesn't support Zabbix versions lower than 7.0" |
| 147 | + ) |
| 148 | + |
| 149 | + def get_mfa(self, mfa_name): |
| 150 | + try: |
| 151 | + mfas = self._zapi.mfa.get( |
| 152 | + { |
| 153 | + "output": "extend", |
| 154 | + "search": {"name": mfa_name}, |
| 155 | + } |
| 156 | + ) |
| 157 | + mfa = None |
| 158 | + for _mfa in mfas: |
| 159 | + if (_mfa["name"] == mfa_name): |
| 160 | + mfa = _mfa |
| 161 | + return mfa |
| 162 | + except Exception as e: |
| 163 | + self._module.fail_json( |
| 164 | + msg="Failed to get MFA method: %s" % e |
| 165 | + ) |
| 166 | + |
| 167 | + def delete_mfa(self, mfa): |
| 168 | + try: |
| 169 | + parameter = [mfa["mfaid"]] |
| 170 | + if self._module.check_mode: |
| 171 | + self._module.exit_json(changed=True) |
| 172 | + self._zapi.mfa.delete(parameter) |
| 173 | + self._module.exit_json( |
| 174 | + changed=True, msg="Successfully deleted MFA method." |
| 175 | + ) |
| 176 | + except Exception as e: |
| 177 | + self._module.fail_json( |
| 178 | + msg="Failed to delete MFA method: %s" % e |
| 179 | + ) |
| 180 | + |
| 181 | + def _convert_to_parameter(self, name, method_type, hash_function, code_length, api_hostname, clientid, client_secret): |
| 182 | + parameter = {} |
| 183 | + parameter['name'] = name |
| 184 | + parameter['type'] = str(zabbix_utils.helper_to_numeric_value( |
| 185 | + [ |
| 186 | + None, |
| 187 | + "totp", |
| 188 | + "duo_universal_prompt" |
| 189 | + ], |
| 190 | + method_type |
| 191 | + )) |
| 192 | + if (method_type == 'totp'): |
| 193 | + parameter['hash_function'] = str(zabbix_utils.helper_to_numeric_value( |
| 194 | + [ |
| 195 | + None, |
| 196 | + "sha-1", |
| 197 | + "sha-256", |
| 198 | + "sha-512" |
| 199 | + ], |
| 200 | + hash_function |
| 201 | + )) |
| 202 | + parameter['code_length'] = str(code_length) |
| 203 | + else: |
| 204 | + parameter['api_hostname'] = str(api_hostname) |
| 205 | + parameter['clientid'] = str(clientid) |
| 206 | + parameter['client_secret'] = str(client_secret) |
| 207 | + return parameter |
| 208 | + |
| 209 | + def create_mfa(self, name, method_type, hash_function, code_length, api_hostname, clientid, client_secret): |
| 210 | + parameter = self._convert_to_parameter(name, method_type, hash_function, code_length, api_hostname, clientid, client_secret) |
| 211 | + try: |
| 212 | + if self._module.check_mode: |
| 213 | + self._module.exit_json(changed=True) |
| 214 | + self._zapi.mfa.create(parameter) |
| 215 | + self._module.exit_json( |
| 216 | + changed=True, msg="Successfully created MFA method." |
| 217 | + ) |
| 218 | + except Exception as e: |
| 219 | + self._module.fail_json( |
| 220 | + msg="Failed to create MFA method: %s" % e |
| 221 | + ) |
| 222 | + |
| 223 | + def update_mfa(self, current_mfa, name, method_type, hash_function, code_length, api_hostname, clientid, client_secret): |
| 224 | + try: |
| 225 | + parameter = self._convert_to_parameter(name, method_type, hash_function, code_length, api_hostname, clientid, client_secret) |
| 226 | + parameter.update({'mfaid': current_mfa['mfaid']}) |
| 227 | + if (method_type == 'totp'): |
| 228 | + current_mfa = zabbix_utils.helper_normalize_data( |
| 229 | + current_mfa, del_keys=["api_hostname", "clientid"] |
| 230 | + )[0] |
| 231 | + difference = {} |
| 232 | + zabbix_utils.helper_compare_dictionaries(parameter, current_mfa, difference) |
| 233 | + if (difference == {}): |
| 234 | + self._module.exit_json(changed=False) |
| 235 | + |
| 236 | + if self._module.check_mode: |
| 237 | + self._module.exit_json(changed=True) |
| 238 | + self._zapi.mfa.update(parameter) |
| 239 | + self._module.exit_json( |
| 240 | + changed=True, msg="Successfully updated MFA method." |
| 241 | + ) |
| 242 | + except Exception as e: |
| 243 | + self._module.fail_json( |
| 244 | + msg="Failed to update MFA method: %s" % e |
| 245 | + ) |
| 246 | + |
| 247 | + |
| 248 | +def main(): |
| 249 | + """Main ansible module function""" |
| 250 | + |
| 251 | + argument_spec = zabbix_utils.zabbix_common_argument_spec() |
| 252 | + argument_spec.update( |
| 253 | + dict( |
| 254 | + name=dict(type="str", required=True), |
| 255 | + method_type=dict( |
| 256 | + type="str", |
| 257 | + choices=[ |
| 258 | + "totp", |
| 259 | + "duo_universal_prompt" |
| 260 | + ], |
| 261 | + ), |
| 262 | + hash_function=dict( |
| 263 | + type="str", |
| 264 | + choices=[ |
| 265 | + "sha-1", |
| 266 | + "sha-256", |
| 267 | + "sha-512" |
| 268 | + ], |
| 269 | + ), |
| 270 | + code_length=dict( |
| 271 | + type="int", |
| 272 | + choices=[6, 8], |
| 273 | + ), |
| 274 | + api_hostname=dict(type="str"), |
| 275 | + clientid=dict(type="str"), |
| 276 | + client_secret=dict(type="str", no_log=True), |
| 277 | + state=dict( |
| 278 | + type="str", |
| 279 | + default="present", |
| 280 | + choices=["present", "absent"] |
| 281 | + ) |
| 282 | + ) |
| 283 | + ) |
| 284 | + |
| 285 | + module = AnsibleModule( |
| 286 | + argument_spec=argument_spec, |
| 287 | + required_if=[ |
| 288 | + [ |
| 289 | + "method_type", |
| 290 | + "totp", |
| 291 | + [ |
| 292 | + "hash_function", |
| 293 | + "code_length" |
| 294 | + ] |
| 295 | + ], |
| 296 | + [ |
| 297 | + "method_type", |
| 298 | + "duo_universal_prompt", |
| 299 | + [ |
| 300 | + "api_hostname", |
| 301 | + "clientid", |
| 302 | + "client_secret" |
| 303 | + ] |
| 304 | + ] |
| 305 | + ], |
| 306 | + mutually_exclusive=[ |
| 307 | + ('hash_function', 'api_hostname') |
| 308 | + ], |
| 309 | + required_by={ |
| 310 | + 'hash_function': 'method_type', |
| 311 | + 'code_length': 'method_type', |
| 312 | + 'api_hostname': 'method_type', |
| 313 | + 'clientid': 'method_type', |
| 314 | + 'client_secret': 'method_type' |
| 315 | + }, |
| 316 | + supports_check_mode=True, |
| 317 | + ) |
| 318 | + |
| 319 | + name = module.params["name"] |
| 320 | + method_type = module.params["method_type"] |
| 321 | + hash_function = module.params["hash_function"] |
| 322 | + code_length = module.params["code_length"] |
| 323 | + api_hostname = module.params["api_hostname"] |
| 324 | + clientid = module.params["clientid"] |
| 325 | + client_secret = module.params["client_secret"] |
| 326 | + state = module.params["state"] |
| 327 | + |
| 328 | + mfa_class_obj = MFA(module) |
| 329 | + mfa = mfa_class_obj.get_mfa(name) |
| 330 | + |
| 331 | + if state == "absent": |
| 332 | + if mfa: |
| 333 | + mfa_class_obj.delete_mfa(mfa) |
| 334 | + else: |
| 335 | + module.exit_json(changed=False) |
| 336 | + else: |
| 337 | + if mfa: |
| 338 | + mfa_class_obj.update_mfa(mfa, name, method_type, hash_function, code_length, api_hostname, clientid, client_secret) |
| 339 | + else: |
| 340 | + mfa_class_obj.create_mfa(name, method_type, hash_function, code_length, api_hostname, clientid, client_secret) |
| 341 | + |
| 342 | + |
| 343 | +if __name__ == "__main__": |
| 344 | + main() |
0 commit comments