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