diff --git a/README.rst b/README.rst index aa475a84..f250a5f3 100644 --- a/README.rst +++ b/README.rst @@ -45,7 +45,7 @@ in order to authenticate to your HashiCorp Vault instance: * `VAULT_ADDR`: url for vault * `VAULT_SKIP_VERIFY=true`: if set, do not verify presented TLS certificate before communicating with Vault server. Setting this variable is not recommended except during testing - * `VAULT_AUTHTYPE`: authentication type to use: `token`, `userpass`, `github`, `ldap`, `approle` + * `VAULT_AUTHTYPE`: authentication type to use: `token`, `userpass`, `github`, `ldap`, `radius`, `approle` * `VAULT_LOGIN_MOUNT_POINT`: mount point for login defaults to auth type * `VAULT_TOKEN`: token for vault * `VAULT_ROLE_ID`: (required by `approle`) diff --git a/ansible/module_utils/hashivault.py b/ansible/module_utils/hashivault.py index 344368c8..061476ba 100644 --- a/ansible/module_utils/hashivault.py +++ b/ansible/module_utils/hashivault.py @@ -131,6 +131,8 @@ def hashivault_auth(client, params): client.auth.userpass.login(username, password, mount_point=login_mount_point) elif authtype == 'ldap': client.auth.ldap.login(username, password, mount_point=login_mount_point) + elif authtype == 'radius': + client.auth.radius.login(username, password, mount_point=login_mount_point) elif authtype == 'approle': client = AppRoleClient(client, role_id, secret_id, mount_point=login_mount_point) elif authtype == 'tls': diff --git a/ansible/modules/hashivault/hashivault_radius_config.py b/ansible/modules/hashivault/hashivault_radius_config.py new file mode 100644 index 00000000..1e8e2f0b --- /dev/null +++ b/ansible/modules/hashivault/hashivault_radius_config.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from ansible.module_utils.hashivault import hashivault_argspec +from ansible.module_utils.hashivault import hashivault_auth_client +from ansible.module_utils.hashivault import hashivault_init +from ansible.module_utils.hashivault import hashiwrapper +from hvac.exceptions import InvalidPath + +ANSIBLE_METADATA = {'status': ['stableinterface'], 'supported_by': 'community', 'version': '1.1'} +DOCUMENTATION = ''' +--- +module: hashivault_radius_config +version_added: "4.7.0" +short_description: Hashicorp Vault RADIUS configuration module +description: + - Module to configure the RADIUS authentication method in Hashicorp Vault. +options: + mount_point: + description: + - location where this auth_method is mounted. also known as "path" + default: radius + host: + description: + - The RADIUS server to connect to. Examples: radius.myorg.com, 127.0.0.1 + required: True + secret: + description: + - The RADIUS shared secret + required: True + port: + description: + - The UDP port where the RADIUS server is listening on. Defaults is 1812. + default: 1812 + unregistered_user_policies: + description: + - A comma-separated list of policies to be granted to unregistered users. + default: [] + dial_timeout: + description: + - Number of seconds to wait for a backend connection before timing out. Default is 10. + default: 10 + nas_port: + description: + - The NAS-Port attribute of the RADIUS request. Defaults is 10. + default: 10 +extends_documentation_fragment: hashivault +''' +EXAMPLES = ''' +--- +- hosts: localhost + tasks: + - hashivault_radius_config: + host: "radius.myorg.com" + secret: "my_radius_secret" + port: 1812 + dial_timeout: 10 + nas_port: 10 + unregistered_user_policies: + - default +- hosts: localhost + tasks: + - hashivault_radius_config: + host: "127.0.0.1" + secret: "{{ radius_secret }}" + mount_point: "radius" +''' + + +def main(): + argspec = hashivault_argspec() + argspec['mount_point'] = dict(required=False, type='str', default='radius') + argspec['host'] = dict(required=True, type='str') + argspec['secret'] = dict(required=True, type='str', no_log=True) + argspec['port'] = dict(required=False, type='int', default=1812) + argspec['unregistered_user_policies'] = dict(required=False, type='list', default=[]) + argspec['dial_timeout'] = dict(required=False, type='int', default=10) + argspec['nas_port'] = dict(required=False, type='int', default=10) + + module = hashivault_init(argspec, supports_check_mode=True) + result = hashivault_radius_config(module) + if result.get('failed'): + module.fail_json(**result) + else: + module.exit_json(**result) + + +@hashiwrapper +def hashivault_radius_config(module): + params = module.params + client = hashivault_auth_client(params) + changed = False + desired_state = dict() + desired_state['mount_point'] = params.get('mount_point') + desired_state['host'] = params.get('host') + desired_state['secret'] = params.get('secret') + desired_state['port'] = params.get('port') + desired_state['unregistered_user_policies'] = params.get('unregistered_user_policies') + desired_state['dial_timeout'] = params.get('dial_timeout') + desired_state['nas_port'] = params.get('nas_port') + + # if secret is None, remove it from desired state since we can't compare + if desired_state['secret'] is None: + del desired_state['secret'] + + # check current config + current_state = dict() + try: + result = client.auth.radius.read_configuration( + mount_point=desired_state['mount_point'])['data'] + # map keys from Vault response to desired state keys + current_state['host'] = result.get('host', '') + current_state['port'] = result.get('port', 1812) + current_state['unregistered_user_policies'] = result.get('unregistered_user_policies', []) + current_state['dial_timeout'] = result.get('dial_timeout', 10) + current_state['nas_port'] = result.get('nas_port', 10) + except InvalidPath: + pass + + # check if current config matches desired config values, if they match, set changed to false to prevent action + for k, v in current_state.items(): + if k in desired_state and v != desired_state[k]: + changed = True + + # if configs dont match and checkmode is off, complete the change + if changed and not module.check_mode: + client.auth.radius.configure(**desired_state) + + return {'changed': changed} + + +if __name__ == '__main__': + main() diff --git a/ansible/modules/hashivault/hashivault_radius_user.py b/ansible/modules/hashivault/hashivault_radius_user.py new file mode 100644 index 00000000..3749468f --- /dev/null +++ b/ansible/modules/hashivault/hashivault_radius_user.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +from ansible.module_utils.hashivault import hashivault_argspec +from ansible.module_utils.hashivault import hashivault_auth_client +from ansible.module_utils.hashivault import hashivault_init +from ansible.module_utils.hashivault import hashiwrapper + +ANSIBLE_METADATA = {'status': ['stableinterface'], 'supported_by': 'community', 'version': '1.1'} +DOCUMENTATION = ''' +--- +module: hashivault_radius_user +version_added: "4.7.0" +short_description: Hashicorp Vault RADIUS user management module +description: + - Module to manage RADIUS users in Hashicorp Vault. +options: + name: + description: + - username to create/update/delete + required: True + policies: + description: + - List of policies associated with the user + default: [] + state: + description: + - whether to create/update or delete the user + choices: ['present', 'absent'] + default: present + mount_point: + description: + - The "path" the auth backend is mounted on + default: radius +extends_documentation_fragment: hashivault +''' +EXAMPLES = ''' +--- +- hosts: localhost + tasks: + - hashivault_radius_user: + name: 'bob' + policies: + - 'default' + - 'bob-policy' +- hosts: localhost + tasks: + - hashivault_radius_user: + name: 'alice' + policies: + - 'alice-policy' + state: present +- hosts: localhost + tasks: + - hashivault_radius_user: + name: 'old-user' + state: absent +''' + + +def main(): + argspec = hashivault_argspec() + argspec['name'] = dict(required=True, type='str') + argspec['policies'] = dict(required=False, type='list', default=[]) + argspec['state'] = dict(required=False, choices=['present', 'absent'], default='present') + argspec['mount_point'] = dict(required=False, type='str', default='radius') + module = hashivault_init(argspec) + result = hashivault_radius_user(module.params) + if result.get('failed'): + module.fail_json(**result) + else: + module.exit_json(**result) + + +def hashivault_radius_user_update(user_details, client, user_name, user_policies, mount_point): + changed = False + + # existing policies + if user_details['policies'] is not None: + if set(user_details['policies']) != set(user_policies): + changed = True + # new policies and none existing + elif len(user_policies) > 0: + changed = True + + if changed: + try: + response = client.auth.radius.register_user( + username=user_name, + policies=user_policies, + mount_point=mount_point + ) + except Exception as e: + return {'failed': True, 'msg': str(e)} + if response.status_code == 204: + return {'changed': True} + return {'changed': True, 'data': response} + return {'changed': False} + + +def hashivault_radius_user_create_or_update(params): + client = hashivault_auth_client(params) + user_name = params.get('name') + mount_point = params.get('mount_point') + user_policies = params.get('policies') + try: + user_details = client.auth.radius.read_user(username=user_name, mount_point=mount_point) + except Exception: + client.auth.radius.register_user( + username=user_name, + policies=user_policies, + mount_point=mount_point + ) + return {'changed': True} + return hashivault_radius_user_update(user_details['data'], client, user_name=user_name, + user_policies=user_policies, + mount_point=mount_point) + + +def hashivault_radius_user_delete(params): + client = hashivault_auth_client(params) + user_name = params.get('name') + mount_point = params.get('mount_point') + + try: + client.auth.radius.read_user(username=user_name, mount_point=mount_point) + except Exception: + return {'changed': False} + client.auth.radius.delete_user(username=user_name, mount_point=mount_point) + return {'changed': True} + + +@hashiwrapper +def hashivault_radius_user(params): + state = params.get('state') + if state == 'present': + return hashivault_radius_user_create_or_update(params) + elif state == 'absent': + return hashivault_radius_user_delete(params) + + +if __name__ == '__main__': + main() diff --git a/functional/run.sh b/functional/run.sh index cfb9076c..3d4c4d88 100755 --- a/functional/run.sh +++ b/functional/run.sh @@ -56,6 +56,8 @@ ansible-playbook -v test_unseal.yml ansible-playbook -v test_identity_group.yml ansible-playbook -v test_identity_entity.yml ansible-playbook -v test_ldap_group.yml +ansible-playbook -v test_radius_config.yml +ansible-playbook -v test_radius_user.yml ansible-playbook -v test_pki.yml ansible-playbook -v test_rekey.yml # test_rekey.yml changes unseal keys, need to update VAULT_KEYS diff --git a/functional/test_radius_config.yml b/functional/test_radius_config.yml new file mode 100644 index 00000000..6d268a7f --- /dev/null +++ b/functional/test_radius_config.yml @@ -0,0 +1,52 @@ +--- +- hosts: localhost + gather_facts: no + tasks: + - name: enable radius authentication + hashivault_auth_method: + method_type: radius + + - name: radius configuration + hashivault_radius_config: + host: localhost + secret: test_secret + port: 1812 + dial_timeout: 10 + nas_port: 10 + register: radius_module + - assert: { that: "radius_module is changed" } + - assert: { that: "radius_module.rc == 0" } + + - name: radius configuration is idempotent + hashivault_radius_config: + host: localhost + secret: test_secret + port: 1812 + dial_timeout: 10 + nas_port: 10 + register: radius_module_idempotent + - assert: { that: "radius_module_idempotent is not changed" } + + - name: radius configuration with unregistered_user_policies + hashivault_radius_config: + host: localhost + secret: test_secret + port: 1812 + dial_timeout: 10 + nas_port: 10 + unregistered_user_policies: + - default + register: radius_module_policies + - assert: { that: "radius_module_policies is changed" } + + - name: radius configuration with unregistered_user_policies is idempotent + hashivault_radius_config: + host: localhost + secret: test_secret + port: 1812 + dial_timeout: 10 + nas_port: 10 + unregistered_user_policies: + - default + register: radius_module_policies_idempotent + - assert: { that: "radius_module_policies_idempotent is not changed" } diff --git a/functional/test_radius_user.yml b/functional/test_radius_user.yml new file mode 100644 index 00000000..0b5312e6 --- /dev/null +++ b/functional/test_radius_user.yml @@ -0,0 +1,95 @@ +--- +- hosts: localhost + gather_facts: no + tasks: + - name: arrange - policies - functional_testing_policy_one + hashivault_policy: + name: functional_testing_policy_one + rules: 'path "secret/*" {capabilities = ["read", "list"]}' + + - name: arrange - policies - functional_testing_policy_two + hashivault_policy: + name: functional_testing_policy_two + rules: 'path "secret/*" {capabilities = ["read", "list"]}' + + - name: enable radius authentication + hashivault_auth_method: + method_type: radius + + - name: radius configuration + hashivault_radius_config: + host: localhost + secret: test_secret + register: radius_config + - assert: { that: "radius_config is changed" } + - assert: { that: "radius_config.rc == 0" } + + - name: remove radius user + hashivault_radius_user: + name: test + state: absent + + - name: add radius user + hashivault_radius_user: + name: test + state: present + register: success_config + + - assert: { that: "success_config is changed" } + + - name: chg policies - add functional_testing_policy_one + hashivault_radius_user: + name: test + state: present + policies: + - functional_testing_policy_one + register: chg_policies + + - assert: { that: "chg_policies is changed" } + + - name: duplicate policies + hashivault_radius_user: + name: test + state: present + policies: + - functional_testing_policy_one + register: chg_policies + + - assert: { that: "chg_policies is not changed" } + + - name: chg policies - add functional_testing_policy_two + hashivault_radius_user: + name: test + state: present + policies: + - functional_testing_policy_one + - functional_testing_policy_two + register: chg_policies + + - assert: { that: "chg_policies is changed" } + + - name: chg policies - remove functional_testing_policy_one + hashivault_radius_user: + name: test + state: present + policies: + - functional_testing_policy_two + register: chg_policies + + - assert: { that: "chg_policies is changed" } + + - name: delete radius user + hashivault_radius_user: + name: test + state: absent + register: delete_user + + - assert: { that: "delete_user is changed" } + + - name: delete already deleted radius user + hashivault_radius_user: + name: test + state: absent + register: delete_user_idempotent + + - assert: { that: "delete_user_idempotent is not changed" }