CheckPoint – Key and user management using Ansible and new GAiA API

As a big fan of automation and Ansible I was pretty unhappy when I found out the default “user” Ansible module was not able to handle user management in a way that GAiA likes – since the users need to be added to the config, I started looking at ways to either bring the functionality to the “user” module, or create a separate module that could handle this task.

Not too long ago CheckPoint has released GAiA API. GAiA API is going to be shipped and enabled by default in R80.40 and newer releases. However, API can be manually installed as an extension to boxes running older versions of CheckPoint. Combined with the fact that CheckPoint also contributed a set of modules to Ansible, I felt like it would be neat to expand the functionalities already provided by a vendor until they give us their “official” way of achieving the same goal.

Okay, enough talking, I will cut to the chase – having the base of the GAiA modules I decided to add two new ones: one to manage users and one to gather facts about users. The modules won’t be long so I thought it would be worth to share them with you 🙂

First lets start with downloading the relevant collection. To do that, we are going to use following command:

ansible-galaxy collection install check_point.gaia

Now let’s create a new directory for our playbook:

mkdir checkpoint_playbooks
cd checkpoint_playbooks

And paste following into a new file called “hosts”, changing relevant bits:

[checkpoint]
192.168.100.10

[checkpoint:vars]
ansible_httpapi_use_ssl=True
ansible_httpapi_validate_certs=False
ansible_user=ansible
ansible_password=mypasswordtowebui
ansible_network_os=checkpoint
ansible_ssh_private_key_file=/home/tecden/.ssh/ansible_ssh_key
ansible_network_os=check_point.gaia.checkpoint

You will need to change the IP address in the [checkpoint] section to include the hosts you want to run the playbook against. In the vars section we can see that I am using both password and private key file. The reason for this is simple – in my playbook, after I create an account I will then put a public key in to allow the new users to authenticate without any passwords.

And now the playbook called api_test.yml (you can call it whatever you like, just swap out the api_test.yml value from examples coming up next):

---
- name: API test
  hosts: checkpoint
  connection: httpapi
  tasks:
   - name: add tecden user
     check_point.gaia.cp_gaia_user:
       name: tecden
       uid: 1010
       roles: adminRole
       homedir: /home/tecden
       shell: cli
   - name: add key
     connection: ssh
     authorized_key:
       user: tecden
       state: present
       key: "{{ lookup('file', '/home/tecden/.ssh/tecden.pub') }}"

Alright. Notice that we are referring to a weird module called “check_point.gaia.cp_gaia_user”. This is a module we are going to create right now. Run following command to open a VIM editor and lets create the new module:

vim ~/.ansible/collections/ansible_collections/check_point/gaia/plugins/modules/cp_gaia.user.py

And paste following contents:

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Ansible module to manage CheckPoint Firewall (c) 2019
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.check_point.gaia.plugins.module_utils.checkpoint import idempotent_api_call

DOCUMENTATION = """
author: Dawid (tecden.co.uk, @killstrelok)
description:
- Set up a user
module: cp_gaia_user
options:
  name:
    description: Login of the user.
    required: true
    type: str
  uid:
    description: UID of the user. Needs to be a number
    required: true
    type: int
  password:
    description: Password of the user.
    required: false
    type: str
  homedir:
    description: Home directory of the new user. If left default, creates home directory using following pattern: /home/{{ name }}
    required: false
    type: str
  shell:
    description: Shell of the new user. Uses Cli(sh) by default.
    required: false
    type: str, choices=['scp-only', 'tcsh', 'csh', 'sh', 'no-login', 'bash' ,'cli']
  roles:
    description: Role of the new user.
    required: false
    type: str, choices=['adminRole','monitorRole']
short_description: Manage user.
version_added: '2.9'

"""

EXAMPLES = """
- name: add user
  check_point.gaia.cp_gaia_user:
    name: new-user
    uid: 1010
"""

RETURN = """
user:
  description: The updated user.
  returned: always
  type: dict
"""


def main():
    # arguments for the module:
    fields = dict(
        name=dict(type='str', required=True),
        uid=dict(type='int'),
        password=dict(type='str'),
        homedir=dict(type='str'),
        shell=dict(type='str', choices=['scp-only', 'tcsh', 'csh', 'sh', 'no-login', 'bash' ,'cli']),
        roles=dict(type='str', choices=['adminRole','monitorRole']),
    )
    module = AnsibleModule(argument_spec=fields, supports_check_mode=True)
    api_call_object = 'user'
    ignore = []
    keys = ["name"]

    res = idempotent_api_call(module, api_call_object, ignore, keys)
    module.exit_json(**res)


if __name__ == "__main__":
    main()

And now the facts module (located in ~/.ansible/collections/ansible_collections/check_point/gaia/plugins/modules/cp_gaia.users_facts.py – optional, not required for the module above to work):

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Ansible module to manage CheckPoint Firewall (c) 2019
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'community'}

DOCUMENTATION = """
module: cp_gaia_users_facts
author: Dawid (tecden.co.uk, @killstrelok)
description:
- Show Users
short_description: Show User/s
version_added: '2.9'
options:
  name:
    description: User to show. If not specified, all users information is returned.
    required: false
    type: str

"""

EXAMPLES = """
- name: Show physical interfaces
  cp_gaia_users_facts:

- name: Show physical interface by specifying it name
  cp_gaia_users_facts:
    name: tecden

"""

RETURN = """
ansible_facts:
  description: The user/s facts.
  returned: always.
  type: list
"""

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.check_point.gaia.plugins.module_utils.checkpoint import facts_api_call


def main():
    # arguments for the module:
    fields = dict(
        name=dict(required=False, type="str")
    )
    module = AnsibleModule(argument_spec=fields, supports_check_mode=True)
    api_call_object = "user"
    keys = ["name"]
    res = facts_api_call(module, api_call_object, keys)
    module.exit_json(**res)


if __name__ == "__main__":
    main()

Now, the idempotent_api_call function does not really work when a user does not exist (because API returns error 400 with “object_not_found” message and the function fails as the logic to handle any code that’s not code 200 is missing). Therefore, I decided to modify the checkpoint.py file a little bit, and changed following section:

def idempotent_api_call(module, api_call_object, ignore, keys):
    modules_params_original = module.params
    module_params_show = dict((k, v) for k, v in module.params.items() if k in keys and v is not None)
    module.params = module_params_show
    before = api_call(module=module, api_call_object="show-{0}".format(api_call_object))
    [before.pop(key) for key in ignore]

    # Run the command:
    module.params = modules_params_original
    res = api_call(module=module, api_call_object="set-{0}".format(api_call_object))
    module.params = module_params_show
    after = res.copy()
    [after.pop(key) for key in ignore]

    changed = False if before == after else True

    return {
        api_call_object.replace('-', '_'): res,
        "changed": changed
    }

to this:

def idempotent_api_call(module, api_call_object, ignore, keys):
    modules_params_original = module.params
    module_params_show = dict((k, v) for k, v in module.params.items() if k in keys and v is not None)
    module.params = module_params_show
    before = api_call(module=module, api_call_object="show-{0}".format(api_call_object))
    [before.pop(key) for key in ignore]

    # Run the command:
    module.params = modules_params_original
    if before == "object_not_found":
        res = api_call(module=module, api_call_object="add-{0}".format(api_call_object))
    else:
        res = api_call(module=module, api_call_object="set-{0}".format(api_call_object))
    module.params = module_params_show
    after = res.copy()
    [after.pop(key) for key in ignore]

    changed = False if before == after else True

    return {
        api_call_object.replace('-', '_'): res,
        "changed": changed
    }

And this section:

def api_call(module, api_call_object):
    payload = get_payload_from_parameters(module.params)
    connection = Connection(module._socket_path)
    version = get_version(module)
    code, response = send_request(connection, version, api_call_object, payload)
    if code != 200:
        module.fail_json(msg=parse_fail_message(code, response))

    return response

to this:

def api_call(module, api_call_object):
    payload = get_payload_from_parameters(module.params)
    connection = Connection(module._socket_path)
    version = get_version(module)
    code, response = send_request(connection, version, api_call_object, payload)
    if code != 200:
        if code == 400 and response['code'] == 'object_not_found' and any(nonfatal in api_call_object for nonfatal in ["user"]):
            response = "object_not_found"
        else:
            module.fail_json(msg=parse_fail_message(code, response))
            
           
    return response

I tried to follow a logic where some API calls can fail with “object_not_found” message (e.g. when creating a user) and others cannot (as for example we might attempt to add an IP address to an interface that doesn’t exist etc.). Therefore, any calls that are “fine to fail” should be listed alongside “user” string in following piece of code, like:

any(nonfatal in api_call_object for nonfatal in ["user", "group", "younameit")

Alright, let’s test it. I am going to run following command from the checkpoint_playbooks directory that we created right at the beginning and that contains the playbook and hosts file:

strelok@tecden:~$ ansible-playbook api_test.yml -i hosts     

PLAY [API test] ****************************************************************

TASK [Gathering Facts] *********************************************************
[DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host 192.168.100.10 should 
use /usr/bin/python3, but is using /usr/bin/python for backward compatibility 
with prior Ansible releases. A future Ansible release will default to using the
 discovered platform python for this host. See https://docs.ansible.com/ansible
/2.9/reference_appendices/interpreter_discovery.html for more information. This
 feature will be removed in version 2.12. Deprecation warnings can be disabled 
by setting deprecation_warnings=False in ansible.cfg.
ok: [192.168.100.10]

TASK [add tecden user] *********************************************************
[WARNING]: Module did not set no_log for password
changed: [192.168.100.10]

TASK [add key] *****************************************************************
changed: [192.168.100.10]

PLAY RECAP *********************************************************************
192.168.100.10             : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

One more try to see if the modules will now do nothing as our user already exists and has appropriate key added:

strelok@tecden:~$ ansible-playbook api_test_tecden.yml -i hosts

PLAY [API test] ****************************************************************

TASK [Gathering Facts] *********************************************************
[DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host 192.168.100.10 should 
use /usr/bin/python3, but is using /usr/bin/python for backward compatibility 
with prior Ansible releases. A future Ansible release will default to using the
 discovered platform python for this host. See https://docs.ansible.com/ansible
/2.9/reference_appendices/interpreter_discovery.html for more information. This
 feature will be removed in version 2.12. Deprecation warnings can be disabled 
by setting deprecation_warnings=False in ansible.cfg.
ok: [192.168.100.10]

TASK [add tecden user] *********************************************************
[WARNING]: Module did not set no_log for password
ok: [192.168.100.10]

TASK [add key] *****************************************************************
ok: [192.168.100.10]

PLAY RECAP *********************************************************************
192.168.100.10             : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Woohoo! Nothing happened which means we achieved our goal 🙂

I will be looking to add the ability to define “state” to delete users if needed but I believe this would require more work with the checkpoint.py file which I don’t really want to do right now.

Also sincere apology to @chkp-yuvalfe (CheckPoint employee that maintains the CheckPointAnsibleGAIACollection repo and whose modules I used as templates for above) if my code looks terrible, makes no sense or if I re-invented the wheel.

Leave a Reply

Your email address will not be published. Required fields are marked *

Navigation