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_user_domain_controller 27short_description: Configure domain controller entries 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 user feature and domain_controller 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 user_domain_controller: 76 description: 77 - Configure domain controller entries. 78 default: null 79 type: dict 80 suboptions: 81 ad_mode: 82 description: 83 - Set Active Directory mode. 84 type: str 85 choices: 86 - none 87 - ds 88 - lds 89 adlds_dn: 90 description: 91 - AD LDS distinguished name. 92 type: str 93 adlds_ip_address: 94 description: 95 - AD LDS IPv4 address. 96 type: str 97 adlds_ip6: 98 description: 99 - AD LDS IPv6 address. 100 type: str 101 adlds_port: 102 description: 103 - Port number of AD LDS service . 104 type: int 105 dns_srv_lookup: 106 description: 107 - Enable/disable DNS service lookup. 108 type: str 109 choices: 110 - enable 111 - disable 112 domain_name: 113 description: 114 - Domain DNS name. 115 type: str 116 extra_server: 117 description: 118 - extra servers. 119 type: list 120 suboptions: 121 id: 122 description: 123 - Server ID. 124 required: true 125 type: int 126 ip_address: 127 description: 128 - Domain controller IP address. 129 type: str 130 port: 131 description: 132 - Port to be used for communication with the domain controller . 133 type: int 134 source_ip_address: 135 description: 136 - FortiGate IPv4 address to be used for communication with the domain controller. 137 type: str 138 source_port: 139 description: 140 - Source port to be used for communication with the domain controller. 141 type: int 142 hostname: 143 description: 144 - Hostname of the server to connect to. 145 type: str 146 interface: 147 description: 148 - Specify outgoing interface to reach server. Source system.interface.name. 149 type: str 150 interface_select_method: 151 description: 152 - Specify how to select outgoing interface to reach server. 153 type: str 154 choices: 155 - auto 156 - sdwan 157 - specify 158 ip_address: 159 description: 160 - Domain controller IP address. 161 type: str 162 ip6: 163 description: 164 - Domain controller IPv6 address. 165 type: str 166 ldap_server: 167 description: 168 - LDAP server name. Source user.ldap.name. 169 type: str 170 name: 171 description: 172 - Domain controller entry name. 173 required: true 174 type: str 175 password: 176 description: 177 - Password for specified username. 178 type: str 179 port: 180 description: 181 - Port to be used for communication with the domain controller . 182 type: int 183 replication_port: 184 description: 185 - Port to be used for communication with the domain controller for replication service. Port number 0 indicates automatic discovery. 186 type: int 187 source_ip_address: 188 description: 189 - FortiGate IPv4 address to be used for communication with the domain controller. 190 type: str 191 source_ip6: 192 description: 193 - FortiGate IPv6 address to be used for communication with the domain controller. 194 type: str 195 source_port: 196 description: 197 - Source port to be used for communication with the domain controller. 198 type: int 199 username: 200 description: 201 - User name to sign in with. Must have proper permissions for service. 202 type: str 203''' 204 205EXAMPLES = ''' 206- hosts: fortigates 207 collections: 208 - fortinet.fortios 209 connection: httpapi 210 vars: 211 vdom: "root" 212 ansible_httpapi_use_ssl: yes 213 ansible_httpapi_validate_certs: no 214 ansible_httpapi_port: 443 215 tasks: 216 - name: Configure domain controller entries. 217 fortios_user_domain_controller: 218 vdom: "{{ vdom }}" 219 state: "present" 220 access_token: "<your_own_value>" 221 user_domain_controller: 222 ad_mode: "none" 223 adlds_dn: "<your_own_value>" 224 adlds_ip_address: "<your_own_value>" 225 adlds_ip6: "<your_own_value>" 226 adlds_port: "7" 227 dns_srv_lookup: "enable" 228 domain_name: "<your_own_value>" 229 extra_server: 230 - 231 id: "11" 232 ip_address: "<your_own_value>" 233 port: "13" 234 source_ip_address: "<your_own_value>" 235 source_port: "15" 236 hostname: "myhostname" 237 interface: "<your_own_value> (source system.interface.name)" 238 interface_select_method: "auto" 239 ip_address: "<your_own_value>" 240 ip6: "<your_own_value>" 241 ldap_server: "<your_own_value> (source user.ldap.name)" 242 name: "default_name_22" 243 password: "<your_own_value>" 244 port: "24" 245 replication_port: "25" 246 source_ip_address: "<your_own_value>" 247 source_ip6: "<your_own_value>" 248 source_port: "28" 249 username: "<your_own_value>" 250 251''' 252 253RETURN = ''' 254build: 255 description: Build number of the fortigate image 256 returned: always 257 type: str 258 sample: '1547' 259http_method: 260 description: Last method used to provision the content into FortiGate 261 returned: always 262 type: str 263 sample: 'PUT' 264http_status: 265 description: Last result given by FortiGate on last operation applied 266 returned: always 267 type: str 268 sample: "200" 269mkey: 270 description: Master key (id) used in the last call to FortiGate 271 returned: success 272 type: str 273 sample: "id" 274name: 275 description: Name of the table used to fulfill the request 276 returned: always 277 type: str 278 sample: "urlfilter" 279path: 280 description: Path of the table used to fulfill the request 281 returned: always 282 type: str 283 sample: "webfilter" 284revision: 285 description: Internal revision number 286 returned: always 287 type: str 288 sample: "17.0.2.10658" 289serial: 290 description: Serial number of the unit 291 returned: always 292 type: str 293 sample: "FGVMEVYYQT3AB5352" 294status: 295 description: Indication of the operation's result 296 returned: always 297 type: str 298 sample: "success" 299vdom: 300 description: Virtual domain used 301 returned: always 302 type: str 303 sample: "root" 304version: 305 description: Version of the FortiGate 306 returned: always 307 type: str 308 sample: "v5.6.3" 309 310''' 311from ansible.module_utils.basic import AnsibleModule 312from ansible.module_utils.connection import Connection 313from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler 314from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi 315from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec 316from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning 317from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG 318from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison 319from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize 320 321 322def filter_user_domain_controller_data(json): 323 option_list = ['ad_mode', 'adlds_dn', 'adlds_ip_address', 324 'adlds_ip6', 'adlds_port', 'dns_srv_lookup', 325 'domain_name', 'extra_server', 'hostname', 326 'interface', 'interface_select_method', 'ip_address', 327 'ip6', 'ldap_server', 'name', 328 'password', 'port', 'replication_port', 329 'source_ip_address', 'source_ip6', 'source_port', 330 'username'] 331 dictionary = {} 332 333 for attribute in option_list: 334 if attribute in json and json[attribute] is not None: 335 dictionary[attribute] = json[attribute] 336 337 return dictionary 338 339 340def underscore_to_hyphen(data): 341 if isinstance(data, list): 342 for i, elem in enumerate(data): 343 data[i] = underscore_to_hyphen(elem) 344 elif isinstance(data, dict): 345 new_data = {} 346 for k, v in data.items(): 347 new_data[k.replace('_', '-')] = underscore_to_hyphen(v) 348 data = new_data 349 350 return data 351 352 353def user_domain_controller(data, fos, check_mode=False): 354 355 vdom = data['vdom'] 356 357 state = data['state'] 358 359 user_domain_controller_data = data['user_domain_controller'] 360 filtered_data = underscore_to_hyphen(filter_user_domain_controller_data(user_domain_controller_data)) 361 362 # check_mode starts from here 363 if check_mode: 364 mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom) 365 current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey) 366 is_existed = current_data and current_data.get('http_status') == 200 \ 367 and isinstance(current_data.get('results'), list) \ 368 and len(current_data['results']) > 0 369 370 # 2. if it exists and the state is 'present' then compare current settings with desired 371 if state == 'present' or state is True: 372 if mkey is None: 373 return False, True, filtered_data 374 375 # if mkey exists then compare each other 376 # record exits and they're matched or not 377 if is_existed: 378 is_same = is_same_comparison( 379 serialize(current_data['results'][0]), serialize(filtered_data)) 380 return False, not is_same, filtered_data 381 382 # record does not exist 383 return False, True, filtered_data 384 385 if state == 'absent': 386 if mkey is None: 387 return False, False, filtered_data 388 389 if is_existed: 390 return False, True, filtered_data 391 return False, False, filtered_data 392 393 return True, False, {'reason: ': 'Must provide state parameter'} 394 395 if state == "present" or state is True: 396 return fos.set('user', 397 'domain-controller', 398 data=filtered_data, 399 vdom=vdom) 400 401 elif state == "absent": 402 return fos.delete('user', 403 'domain-controller', 404 mkey=filtered_data['name'], 405 vdom=vdom) 406 else: 407 fos._module.fail_json(msg='state must be present or absent!') 408 409 410def is_successful_status(status): 411 return status['status'] == "success" or \ 412 status['http_method'] == "DELETE" and status['http_status'] == 404 413 414 415def fortios_user(data, fos, check_mode): 416 417 if data['user_domain_controller']: 418 resp = user_domain_controller(data, fos, check_mode) 419 else: 420 fos._module.fail_json(msg='missing task body: %s' % ('user_domain_controller')) 421 if check_mode: 422 return resp 423 return not is_successful_status(resp), \ 424 resp['status'] == "success" and \ 425 (resp['revision_changed'] if 'revision_changed' in resp else True), \ 426 resp 427 428 429versioned_schema = { 430 "type": "list", 431 "children": { 432 "extra_server": { 433 "type": "list", 434 "children": { 435 "source_port": { 436 "type": "integer", 437 "revisions": { 438 "v7.0.0": True 439 } 440 }, 441 "source_ip_address": { 442 "type": "string", 443 "revisions": { 444 "v7.0.0": True 445 } 446 }, 447 "ip_address": { 448 "type": "string", 449 "revisions": { 450 "v7.0.0": True, 451 "v6.4.4": True, 452 "v6.4.0": True, 453 "v6.4.1": True, 454 "v6.2.0": True, 455 "v6.2.3": True, 456 "v6.2.5": True, 457 "v6.2.7": True 458 } 459 }, 460 "port": { 461 "type": "integer", 462 "revisions": { 463 "v7.0.0": True, 464 "v6.4.4": True, 465 "v6.4.0": True, 466 "v6.4.1": True, 467 "v6.2.0": True, 468 "v6.2.3": True, 469 "v6.2.5": True, 470 "v6.2.7": True 471 } 472 }, 473 "id": { 474 "type": "integer", 475 "revisions": { 476 "v7.0.0": True, 477 "v6.4.4": True, 478 "v6.4.0": True, 479 "v6.4.1": True, 480 "v6.2.0": True, 481 "v6.2.3": True, 482 "v6.2.5": True, 483 "v6.2.7": True 484 } 485 } 486 }, 487 "revisions": { 488 "v7.0.0": True, 489 "v6.4.4": True, 490 "v6.4.0": True, 491 "v6.4.1": True, 492 "v6.2.0": True, 493 "v6.2.3": True, 494 "v6.2.5": True, 495 "v6.2.7": True 496 } 497 }, 498 "ldap_server": { 499 "type": "string", 500 "revisions": { 501 "v6.0.0": True, 502 "v7.0.0": True, 503 "v6.0.5": True, 504 "v6.4.4": True, 505 "v6.4.0": True, 506 "v6.4.1": True, 507 "v6.2.0": True, 508 "v6.2.3": True, 509 "v6.2.5": True, 510 "v6.2.7": True, 511 "v6.0.11": True 512 } 513 }, 514 "source_ip_address": { 515 "type": "string", 516 "revisions": { 517 "v7.0.0": True 518 } 519 }, 520 "port": { 521 "type": "integer", 522 "revisions": { 523 "v6.0.0": True, 524 "v7.0.0": True, 525 "v6.0.5": True, 526 "v6.4.4": True, 527 "v6.4.0": True, 528 "v6.4.1": True, 529 "v6.2.0": True, 530 "v6.2.3": True, 531 "v6.2.5": True, 532 "v6.2.7": True, 533 "v6.0.11": True 534 } 535 }, 536 "replication_port": { 537 "type": "integer", 538 "revisions": { 539 "v7.0.0": True 540 } 541 }, 542 "adlds_dn": { 543 "type": "string", 544 "revisions": { 545 "v7.0.0": True 546 } 547 }, 548 "hostname": { 549 "type": "string", 550 "revisions": { 551 "v7.0.0": True 552 } 553 }, 554 "domain_name": { 555 "type": "string", 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 "adlds_port": { 571 "type": "integer", 572 "revisions": { 573 "v7.0.0": True 574 } 575 }, 576 "adlds_ip_address": { 577 "type": "string", 578 "revisions": { 579 "v7.0.0": True 580 } 581 }, 582 "adlds_ip6": { 583 "type": "string", 584 "revisions": { 585 "v7.0.0": True 586 } 587 }, 588 "username": { 589 "type": "string", 590 "revisions": { 591 "v7.0.0": True 592 } 593 }, 594 "source_ip6": { 595 "type": "string", 596 "revisions": { 597 "v7.0.0": True 598 } 599 }, 600 "ip6": { 601 "type": "string", 602 "revisions": { 603 "v7.0.0": True 604 } 605 }, 606 "interface": { 607 "type": "string", 608 "revisions": { 609 "v7.0.0": True 610 } 611 }, 612 "password": { 613 "type": "string", 614 "revisions": { 615 "v7.0.0": True 616 } 617 }, 618 "ip_address": { 619 "type": "string", 620 "revisions": { 621 "v6.0.0": True, 622 "v7.0.0": True, 623 "v6.0.5": True, 624 "v6.4.4": True, 625 "v6.4.0": True, 626 "v6.4.1": True, 627 "v6.2.0": True, 628 "v6.2.3": True, 629 "v6.2.5": True, 630 "v6.2.7": True, 631 "v6.0.11": True 632 } 633 }, 634 "name": { 635 "type": "string", 636 "revisions": { 637 "v6.0.0": True, 638 "v7.0.0": True, 639 "v6.0.5": True, 640 "v6.4.4": True, 641 "v6.4.0": True, 642 "v6.4.1": True, 643 "v6.2.0": True, 644 "v6.2.3": True, 645 "v6.2.5": True, 646 "v6.2.7": True, 647 "v6.0.11": True 648 } 649 }, 650 "dns_srv_lookup": { 651 "type": "string", 652 "options": [ 653 { 654 "value": "enable", 655 "revisions": { 656 "v7.0.0": True 657 } 658 }, 659 { 660 "value": "disable", 661 "revisions": { 662 "v7.0.0": True 663 } 664 } 665 ], 666 "revisions": { 667 "v7.0.0": True 668 } 669 }, 670 "ad_mode": { 671 "type": "string", 672 "options": [ 673 { 674 "value": "none", 675 "revisions": { 676 "v7.0.0": True 677 } 678 }, 679 { 680 "value": "ds", 681 "revisions": { 682 "v7.0.0": True 683 } 684 }, 685 { 686 "value": "lds", 687 "revisions": { 688 "v7.0.0": True 689 } 690 } 691 ], 692 "revisions": { 693 "v7.0.0": True 694 } 695 }, 696 "interface_select_method": { 697 "type": "string", 698 "options": [ 699 { 700 "value": "auto", 701 "revisions": { 702 "v7.0.0": True 703 } 704 }, 705 { 706 "value": "sdwan", 707 "revisions": { 708 "v7.0.0": True 709 } 710 }, 711 { 712 "value": "specify", 713 "revisions": { 714 "v7.0.0": True 715 } 716 } 717 ], 718 "revisions": { 719 "v7.0.0": True 720 } 721 }, 722 "source_port": { 723 "type": "integer", 724 "revisions": { 725 "v7.0.0": True 726 } 727 } 728 }, 729 "revisions": { 730 "v6.0.0": True, 731 "v7.0.0": True, 732 "v6.0.5": True, 733 "v6.4.4": True, 734 "v6.4.0": True, 735 "v6.4.1": True, 736 "v6.2.0": True, 737 "v6.2.3": True, 738 "v6.2.5": True, 739 "v6.2.7": True, 740 "v6.0.11": True 741 } 742} 743 744 745def main(): 746 module_spec = schema_to_module_spec(versioned_schema) 747 mkeyname = 'name' 748 fields = { 749 "access_token": {"required": False, "type": "str", "no_log": True}, 750 "enable_log": {"required": False, "type": bool}, 751 "vdom": {"required": False, "type": "str", "default": "root"}, 752 "state": {"required": True, "type": "str", 753 "choices": ["present", "absent"]}, 754 "user_domain_controller": { 755 "required": False, "type": "dict", "default": None, 756 "options": { 757 } 758 } 759 } 760 for attribute_name in module_spec['options']: 761 fields["user_domain_controller"]['options'][attribute_name] = module_spec['options'][attribute_name] 762 if mkeyname and mkeyname == attribute_name: 763 fields["user_domain_controller"]['options'][attribute_name]['required'] = True 764 765 check_legacy_fortiosapi() 766 module = AnsibleModule(argument_spec=fields, 767 supports_check_mode=True) 768 769 versions_check_result = None 770 if module._socket_path: 771 connection = Connection(module._socket_path) 772 if 'access_token' in module.params: 773 connection.set_option('access_token', module.params['access_token']) 774 775 if 'enable_log' in module.params: 776 connection.set_option('enable_log', module.params['enable_log']) 777 else: 778 connection.set_option('enable_log', False) 779 fos = FortiOSHandler(connection, module, mkeyname) 780 versions_check_result = check_schema_versioning(fos, versioned_schema, "user_domain_controller") 781 782 is_error, has_changed, result = fortios_user(module.params, fos, module.check_mode) 783 784 else: 785 module.fail_json(**FAIL_SOCKET_MSG) 786 787 if versions_check_result and versions_check_result['matched'] is False: 788 module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv") 789 790 if not is_error: 791 if versions_check_result and versions_check_result['matched'] is False: 792 module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result) 793 else: 794 module.exit_json(changed=has_changed, meta=result) 795 else: 796 if versions_check_result and versions_check_result['matched'] is False: 797 module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result) 798 else: 799 module.fail_json(msg="Error in repo", meta=result) 800 801 802if __name__ == '__main__': 803 main() 804