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