1#!/usr/local/bin/python3.8 2from __future__ import (absolute_import, division, print_function) 3# Copyright 2019-2020 Fortinet, Inc. 4# 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see <https://www.gnu.org/licenses/>. 17 18__metaclass__ = type 19 20ANSIBLE_METADATA = {'status': ['preview'], 21 'supported_by': 'community', 22 'metadata_version': '1.1'} 23 24DOCUMENTATION = ''' 25--- 26module: fortios_webfilter_override 27short_description: Configure FortiGuard Web Filter administrative overrides in Fortinet's FortiOS and FortiGate. 28description: 29 - This module is able to configure a FortiGate or FortiOS (FOS) device by allowing the 30 user to set and modify webfilter feature and override category. 31 Examples include all parameters and values need to be adjusted to datasources before usage. 32 Tested with FOS v6.0.0 33version_added: "2.10" 34author: 35 - Link Zheng (@chillancezen) 36 - Jie Xue (@JieX19) 37 - Hongbin Lu (@fgtdev-hblu) 38 - Frank Shen (@frankshen01) 39 - Miguel Angel Munoz (@mamunozgonzalez) 40 - Nicolas Thomas (@thomnico) 41notes: 42 - Legacy fortiosapi has been deprecated, httpapi is the preferred way to run playbooks 43 44requirements: 45 - ansible>=2.9.0 46options: 47 access_token: 48 description: 49 - Token-based authentication. 50 Generated from GUI of Fortigate. 51 type: str 52 required: false 53 enable_log: 54 description: 55 - Enable/Disable logging for task. 56 type: bool 57 required: false 58 default: false 59 vdom: 60 description: 61 - Virtual domain, among those defined previously. A vdom is a 62 virtual instance of the FortiGate that can be configured and 63 used as a different unit. 64 type: str 65 default: root 66 67 state: 68 description: 69 - Indicates whether to create or remove the object. 70 type: str 71 required: true 72 choices: 73 - present 74 - absent 75 webfilter_override: 76 description: 77 - Configure FortiGuard Web Filter administrative overrides. 78 default: null 79 type: dict 80 suboptions: 81 expires: 82 description: 83 - 'Override expiration date and time, from 5 minutes to 365 from now (format: yyyy/mm/dd hh:mm:ss).' 84 type: str 85 id: 86 description: 87 - Override rule ID. 88 required: true 89 type: int 90 initiator: 91 description: 92 - Initiating user of override (read-only setting). 93 type: str 94 ip: 95 description: 96 - IPv4 address which the override applies. 97 type: str 98 ip6: 99 description: 100 - IPv6 address which the override applies. 101 type: str 102 new_profile: 103 description: 104 - Name of the new web filter profile used by the override. Source webfilter.profile.name. 105 type: str 106 old_profile: 107 description: 108 - Name of the web filter profile which the override applies. Source webfilter.profile.name. 109 type: str 110 scope: 111 description: 112 - Override either the specific user, user group, IPv4 address, or IPv6 address. 113 type: str 114 choices: 115 - user 116 - user-group 117 - ip 118 - ip6 119 status: 120 description: 121 - Enable/disable override rule. 122 type: str 123 choices: 124 - enable 125 - disable 126 user: 127 description: 128 - Name of the user which the override applies. 129 type: str 130 user_group: 131 description: 132 - Specify the user group for which the override applies. Source user.group.name. 133 type: str 134''' 135 136EXAMPLES = ''' 137- hosts: fortigates 138 collections: 139 - fortinet.fortios 140 connection: httpapi 141 vars: 142 vdom: "root" 143 ansible_httpapi_use_ssl: yes 144 ansible_httpapi_validate_certs: no 145 ansible_httpapi_port: 443 146 tasks: 147 - name: Configure FortiGuard Web Filter administrative overrides. 148 fortios_webfilter_override: 149 vdom: "{{ vdom }}" 150 state: "present" 151 access_token: "<your_own_value>" 152 webfilter_override: 153 expires: "<your_own_value>" 154 id: "4" 155 initiator: "<your_own_value>" 156 ip: "<your_own_value>" 157 ip6: "<your_own_value>" 158 new_profile: "<your_own_value> (source webfilter.profile.name)" 159 old_profile: "<your_own_value> (source webfilter.profile.name)" 160 scope: "user" 161 status: "enable" 162 user: "<your_own_value>" 163 user_group: "<your_own_value> (source user.group.name)" 164 165''' 166 167RETURN = ''' 168build: 169 description: Build number of the fortigate image 170 returned: always 171 type: str 172 sample: '1547' 173http_method: 174 description: Last method used to provision the content into FortiGate 175 returned: always 176 type: str 177 sample: 'PUT' 178http_status: 179 description: Last result given by FortiGate on last operation applied 180 returned: always 181 type: str 182 sample: "200" 183mkey: 184 description: Master key (id) used in the last call to FortiGate 185 returned: success 186 type: str 187 sample: "id" 188name: 189 description: Name of the table used to fulfill the request 190 returned: always 191 type: str 192 sample: "urlfilter" 193path: 194 description: Path of the table used to fulfill the request 195 returned: always 196 type: str 197 sample: "webfilter" 198revision: 199 description: Internal revision number 200 returned: always 201 type: str 202 sample: "17.0.2.10658" 203serial: 204 description: Serial number of the unit 205 returned: always 206 type: str 207 sample: "FGVMEVYYQT3AB5352" 208status: 209 description: Indication of the operation's result 210 returned: always 211 type: str 212 sample: "success" 213vdom: 214 description: Virtual domain used 215 returned: always 216 type: str 217 sample: "root" 218version: 219 description: Version of the FortiGate 220 returned: always 221 type: str 222 sample: "v5.6.3" 223 224''' 225from ansible.module_utils.basic import AnsibleModule 226from ansible.module_utils.connection import Connection 227from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler 228from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi 229from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec 230from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning 231from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG 232from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison 233from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize 234 235 236def filter_webfilter_override_data(json): 237 option_list = ['expires', 'id', 'initiator', 238 'ip', 'ip6', 'new_profile', 239 'old_profile', 'scope', 'status', 240 'user', 'user_group'] 241 dictionary = {} 242 243 for attribute in option_list: 244 if attribute in json and json[attribute] is not None: 245 dictionary[attribute] = json[attribute] 246 247 return dictionary 248 249 250def underscore_to_hyphen(data): 251 if isinstance(data, list): 252 for i, elem in enumerate(data): 253 data[i] = underscore_to_hyphen(elem) 254 elif isinstance(data, dict): 255 new_data = {} 256 for k, v in data.items(): 257 new_data[k.replace('_', '-')] = underscore_to_hyphen(v) 258 data = new_data 259 260 return data 261 262 263def webfilter_override(data, fos, check_mode=False): 264 265 vdom = data['vdom'] 266 267 state = data['state'] 268 269 webfilter_override_data = data['webfilter_override'] 270 filtered_data = underscore_to_hyphen(filter_webfilter_override_data(webfilter_override_data)) 271 272 # check_mode starts from here 273 if check_mode: 274 mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom) 275 current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey) 276 is_existed = current_data and current_data.get('http_status') == 200 \ 277 and isinstance(current_data.get('results'), list) \ 278 and len(current_data['results']) > 0 279 280 # 2. if it exists and the state is 'present' then compare current settings with desired 281 if state == 'present' or state is True: 282 if mkey is None: 283 return False, True, filtered_data 284 285 # if mkey exists then compare each other 286 # record exits and they're matched or not 287 if is_existed: 288 is_same = is_same_comparison( 289 serialize(current_data['results'][0]), serialize(filtered_data)) 290 return False, not is_same, filtered_data 291 292 # record does not exist 293 return False, True, filtered_data 294 295 if state == 'absent': 296 if mkey is None: 297 return False, False, filtered_data 298 299 if is_existed: 300 return False, True, filtered_data 301 return False, False, filtered_data 302 303 return True, False, {'reason: ': 'Must provide state parameter'} 304 305 if state == "present" or state is True: 306 return fos.set('webfilter', 307 'override', 308 data=filtered_data, 309 vdom=vdom) 310 311 elif state == "absent": 312 return fos.delete('webfilter', 313 'override', 314 mkey=filtered_data['id'], 315 vdom=vdom) 316 else: 317 fos._module.fail_json(msg='state must be present or absent!') 318 319 320def is_successful_status(status): 321 return status['status'] == "success" or \ 322 status['http_method'] == "DELETE" and status['http_status'] == 404 323 324 325def fortios_webfilter(data, fos, check_mode): 326 327 if data['webfilter_override']: 328 resp = webfilter_override(data, fos, check_mode) 329 else: 330 fos._module.fail_json(msg='missing task body: %s' % ('webfilter_override')) 331 if check_mode: 332 return resp 333 return not is_successful_status(resp), \ 334 resp['status'] == "success" and \ 335 (resp['revision_changed'] if 'revision_changed' in resp else True), \ 336 resp 337 338 339versioned_schema = { 340 "type": "list", 341 "children": { 342 "status": { 343 "type": "string", 344 "options": [ 345 { 346 "value": "enable", 347 "revisions": { 348 "v6.0.0": True, 349 "v7.0.0": True, 350 "v6.0.5": True, 351 "v6.4.4": True, 352 "v6.4.0": True, 353 "v6.4.1": True, 354 "v6.2.0": True, 355 "v6.2.3": True, 356 "v6.2.5": True, 357 "v6.2.7": True, 358 "v6.0.11": True 359 } 360 }, 361 { 362 "value": "disable", 363 "revisions": { 364 "v6.0.0": True, 365 "v7.0.0": True, 366 "v6.0.5": True, 367 "v6.4.4": True, 368 "v6.4.0": True, 369 "v6.4.1": True, 370 "v6.2.0": True, 371 "v6.2.3": True, 372 "v6.2.5": True, 373 "v6.2.7": True, 374 "v6.0.11": True 375 } 376 } 377 ], 378 "revisions": { 379 "v6.0.0": True, 380 "v7.0.0": True, 381 "v6.0.5": True, 382 "v6.4.4": True, 383 "v6.4.0": True, 384 "v6.4.1": True, 385 "v6.2.0": True, 386 "v6.2.3": True, 387 "v6.2.5": True, 388 "v6.2.7": True, 389 "v6.0.11": True 390 } 391 }, 392 "initiator": { 393 "type": "string", 394 "revisions": { 395 "v6.0.0": True, 396 "v7.0.0": True, 397 "v6.0.5": True, 398 "v6.4.4": True, 399 "v6.4.0": True, 400 "v6.4.1": True, 401 "v6.2.0": True, 402 "v6.2.3": True, 403 "v6.2.5": True, 404 "v6.2.7": True, 405 "v6.0.11": True 406 } 407 }, 408 "ip": { 409 "type": "string", 410 "revisions": { 411 "v6.0.0": True, 412 "v7.0.0": True, 413 "v6.0.5": True, 414 "v6.4.4": True, 415 "v6.4.0": True, 416 "v6.4.1": True, 417 "v6.2.0": True, 418 "v6.2.3": True, 419 "v6.2.5": True, 420 "v6.2.7": True, 421 "v6.0.11": True 422 } 423 }, 424 "expires": { 425 "type": "string", 426 "revisions": { 427 "v6.0.0": True, 428 "v7.0.0": True, 429 "v6.0.5": True, 430 "v6.4.4": True, 431 "v6.4.0": True, 432 "v6.4.1": True, 433 "v6.2.0": True, 434 "v6.2.3": True, 435 "v6.2.5": True, 436 "v6.2.7": True, 437 "v6.0.11": True 438 } 439 }, 440 "ip6": { 441 "type": "string", 442 "revisions": { 443 "v6.0.0": True, 444 "v7.0.0": True, 445 "v6.0.5": True, 446 "v6.4.4": True, 447 "v6.4.0": True, 448 "v6.4.1": True, 449 "v6.2.0": True, 450 "v6.2.3": True, 451 "v6.2.5": True, 452 "v6.2.7": True, 453 "v6.0.11": True 454 } 455 }, 456 "user": { 457 "type": "string", 458 "revisions": { 459 "v6.0.0": True, 460 "v7.0.0": True, 461 "v6.0.5": True, 462 "v6.4.4": True, 463 "v6.4.0": True, 464 "v6.4.1": True, 465 "v6.2.0": True, 466 "v6.2.3": True, 467 "v6.2.5": True, 468 "v6.2.7": True, 469 "v6.0.11": True 470 } 471 }, 472 "user_group": { 473 "type": "string", 474 "revisions": { 475 "v6.0.0": True, 476 "v7.0.0": True, 477 "v6.0.5": True, 478 "v6.4.4": True, 479 "v6.4.0": True, 480 "v6.4.1": True, 481 "v6.2.0": True, 482 "v6.2.3": True, 483 "v6.2.5": True, 484 "v6.2.7": True, 485 "v6.0.11": True 486 } 487 }, 488 "scope": { 489 "type": "string", 490 "options": [ 491 { 492 "value": "user", 493 "revisions": { 494 "v6.0.0": True, 495 "v7.0.0": True, 496 "v6.0.5": True, 497 "v6.4.4": True, 498 "v6.4.0": True, 499 "v6.4.1": True, 500 "v6.2.0": True, 501 "v6.2.3": True, 502 "v6.2.5": True, 503 "v6.2.7": True, 504 "v6.0.11": True 505 } 506 }, 507 { 508 "value": "user-group", 509 "revisions": { 510 "v6.0.0": True, 511 "v7.0.0": True, 512 "v6.0.5": True, 513 "v6.4.4": True, 514 "v6.4.0": True, 515 "v6.4.1": True, 516 "v6.2.0": True, 517 "v6.2.3": True, 518 "v6.2.5": True, 519 "v6.2.7": True, 520 "v6.0.11": True 521 } 522 }, 523 { 524 "value": "ip", 525 "revisions": { 526 "v6.0.0": True, 527 "v7.0.0": True, 528 "v6.0.5": True, 529 "v6.4.4": True, 530 "v6.4.0": True, 531 "v6.4.1": True, 532 "v6.2.0": True, 533 "v6.2.3": True, 534 "v6.2.5": True, 535 "v6.2.7": True, 536 "v6.0.11": True 537 } 538 }, 539 { 540 "value": "ip6", 541 "revisions": { 542 "v6.0.0": True, 543 "v7.0.0": True, 544 "v6.0.5": True, 545 "v6.4.4": True, 546 "v6.4.0": True, 547 "v6.4.1": True, 548 "v6.2.0": True, 549 "v6.2.3": True, 550 "v6.2.5": True, 551 "v6.2.7": True, 552 "v6.0.11": True 553 } 554 } 555 ], 556 "revisions": { 557 "v6.0.0": True, 558 "v7.0.0": True, 559 "v6.0.5": True, 560 "v6.4.4": True, 561 "v6.4.0": True, 562 "v6.4.1": True, 563 "v6.2.0": True, 564 "v6.2.3": True, 565 "v6.2.5": True, 566 "v6.2.7": True, 567 "v6.0.11": True 568 } 569 }, 570 "old_profile": { 571 "type": "string", 572 "revisions": { 573 "v6.0.0": True, 574 "v7.0.0": True, 575 "v6.0.5": True, 576 "v6.4.4": True, 577 "v6.4.0": True, 578 "v6.4.1": True, 579 "v6.2.0": True, 580 "v6.2.3": True, 581 "v6.2.5": True, 582 "v6.2.7": True, 583 "v6.0.11": True 584 } 585 }, 586 "id": { 587 "type": "integer", 588 "revisions": { 589 "v6.0.0": True, 590 "v7.0.0": True, 591 "v6.0.5": True, 592 "v6.4.4": True, 593 "v6.4.0": True, 594 "v6.4.1": True, 595 "v6.2.0": True, 596 "v6.2.3": True, 597 "v6.2.5": True, 598 "v6.2.7": True, 599 "v6.0.11": True 600 } 601 }, 602 "new_profile": { 603 "type": "string", 604 "revisions": { 605 "v6.0.0": True, 606 "v7.0.0": True, 607 "v6.0.5": True, 608 "v6.4.4": True, 609 "v6.4.0": True, 610 "v6.4.1": True, 611 "v6.2.0": True, 612 "v6.2.3": True, 613 "v6.2.5": True, 614 "v6.2.7": True, 615 "v6.0.11": True 616 } 617 } 618 }, 619 "revisions": { 620 "v6.0.0": True, 621 "v7.0.0": True, 622 "v6.0.5": True, 623 "v6.4.4": True, 624 "v6.4.0": True, 625 "v6.4.1": True, 626 "v6.2.0": True, 627 "v6.2.3": True, 628 "v6.2.5": True, 629 "v6.2.7": True, 630 "v6.0.11": True 631 } 632} 633 634 635def main(): 636 module_spec = schema_to_module_spec(versioned_schema) 637 mkeyname = 'id' 638 fields = { 639 "access_token": {"required": False, "type": "str", "no_log": True}, 640 "enable_log": {"required": False, "type": bool}, 641 "vdom": {"required": False, "type": "str", "default": "root"}, 642 "state": {"required": True, "type": "str", 643 "choices": ["present", "absent"]}, 644 "webfilter_override": { 645 "required": False, "type": "dict", "default": None, 646 "options": { 647 } 648 } 649 } 650 for attribute_name in module_spec['options']: 651 fields["webfilter_override"]['options'][attribute_name] = module_spec['options'][attribute_name] 652 if mkeyname and mkeyname == attribute_name: 653 fields["webfilter_override"]['options'][attribute_name]['required'] = True 654 655 check_legacy_fortiosapi() 656 module = AnsibleModule(argument_spec=fields, 657 supports_check_mode=True) 658 659 versions_check_result = None 660 if module._socket_path: 661 connection = Connection(module._socket_path) 662 if 'access_token' in module.params: 663 connection.set_option('access_token', module.params['access_token']) 664 665 if 'enable_log' in module.params: 666 connection.set_option('enable_log', module.params['enable_log']) 667 else: 668 connection.set_option('enable_log', False) 669 fos = FortiOSHandler(connection, module, mkeyname) 670 versions_check_result = check_schema_versioning(fos, versioned_schema, "webfilter_override") 671 672 is_error, has_changed, result = fortios_webfilter(module.params, fos, module.check_mode) 673 674 else: 675 module.fail_json(**FAIL_SOCKET_MSG) 676 677 if versions_check_result and versions_check_result['matched'] is False: 678 module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv") 679 680 if not is_error: 681 if versions_check_result and versions_check_result['matched'] is False: 682 module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result) 683 else: 684 module.exit_json(changed=has_changed, meta=result) 685 else: 686 if versions_check_result and versions_check_result['matched'] is False: 687 module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result) 688 else: 689 module.fail_json(msg="Error in repo", meta=result) 690 691 692if __name__ == '__main__': 693 main() 694