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