From 4c55685656342818f46287f7bf025e6a743b4eae Mon Sep 17 00:00:00 2001 From: Tom Page Date: Thu, 11 Jun 2020 17:52:30 +0100 Subject: [PATCH] Add tower_credential_input_source to awx_collection Signed-off-by: Tom Page --- .../modules/tower_credential_input_source.py | 134 +++++++++ .../test/awx/test_credential_input_source.py | 268 ++++++++++++++++++ .../tasks/main.yml | 74 +++++ 3 files changed, 476 insertions(+) create mode 100644 awx_collection/plugins/modules/tower_credential_input_source.py create mode 100644 awx_collection/test/awx/test_credential_input_source.py create mode 100644 awx_collection/tests/integration/targets/tower_credential_input_source/tasks/main.yml diff --git a/awx_collection/plugins/modules/tower_credential_input_source.py b/awx_collection/plugins/modules/tower_credential_input_source.py new file mode 100644 index 0000000000..d6245911ec --- /dev/null +++ b/awx_collection/plugins/modules/tower_credential_input_source.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright: (c) 2017, Wayne Witzel III +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = ''' +--- +module: tower_credential_input_source +author: "Tom Page (@Tompage1994)" +version_added: "2.3" +short_description: create, update, or destroy Ansible Tower credential input sources. +description: + - Create, update, or destroy Ansible Tower credential input sources. See + U(https://www.ansible.com/tower) for an overview. +options: + description: + description: + - The description to use for the credential input source. + type: str + input_field_name: + description: + - The input field the credential source will be used for + required: True + type: str + metadata: + description: + - A JSON or YAML string + required: False + type: str + target_credential: + description: + - The credential which will have its input defined by this source + required: true + type: str + source_credential: + description: + - The credential which is the source of the credential lookup + required: true + type: str + state: + description: + - Desired state of the resource. + choices: ["present", "absent"] + default: "present" + type: str + +extends_documentation_fragment: awx.awx.auth +''' + + +EXAMPLES = ''' +- name: Use CyberArk Lookup credential as password source + tower_credential_input_source: + input_field_name: password + target_credential: new_cred + source_credential: cyberark_lookup + metadata: + object_query: "Safe=MY_SAFE;Object=awxuser" + object_query_format: "Exact" + state: present + +''' + +from ..module_utils.tower_api import TowerModule + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + description=dict(default=''), + input_field_name=dict(required=True), + target_credential=dict(required=True), + source_credential=dict(required=True), + metadata=dict(type=dict), + state=dict(choices=['present', 'absent'], default='present'), + ) + + # Create a module for ourselves + module = TowerModule(argument_spec=argument_spec) + + # Extract our parameters + description = module.params.get('description') + input_field_name = module.params.get('input_field_name') + target_credential = module.params.get('target_credential') + source_credential = module.params.get('source_credential') + metadata = module.params.get('metadata') + state = module.params.get('state') + + target_credential_id = module.resolve_name_to_id('credentials', target_credential) + source_credential_id = module.resolve_name_to_id('credentials', source_credential) + + # Attempt to look up the object based on the provided name, credential type and optional organization + lookup_data = { + 'target_credential': target_credential_id, + 'source_credential': source_credential_id, + 'input_field_name': input_field_name, + } + + credential_input_source = module.get_one('credential_input_sources', **{'data': lookup_data}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + if credential_input_source: + credential_input_source['name'] = '' + module.delete_if_needed(credential_input_source) + + # Create the data that gets sent for create and update + credential_input_source_fields = { + 'target_credential': target_credential_id, + 'source_credential': source_credential_id, + 'input_field_name': input_field_name, + } + if metadata: + credential_input_source_fields['metadata'] = metadata + if description: + credential_input_source_fields['description'] = description + + # If the state was present we can let the module build or update the existing group, this will return on its own + module.create_or_update_if_needed( + credential_input_source, credential_input_source_fields, endpoint='credential_input_sources', item_type='credential_input_source' + ) + + +if __name__ == '__main__': + main() diff --git a/awx_collection/test/awx/test_credential_input_source.py b/awx_collection/test/awx/test_credential_input_source.py new file mode 100644 index 0000000000..4c5cf84e8f --- /dev/null +++ b/awx_collection/test/awx/test_credential_input_source.py @@ -0,0 +1,268 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from awx.main.models import CredentialInputSource, Credential, CredentialType, Organization + + +# Test CyberArk AIM credential source +@pytest.fixture +def source_cred_aim(organization): + # Make a credential type which will be used by the credential + ct=CredentialType.defaults['aim']() + ct.save() + return Credential.objects.create( + name='CyberArk AIM Cred', + credential_type=ct, + inputs={ + "url": "https://cyberark.example.com", + "app_id": "myAppID", + "verify": "false" + } + ) + + +@pytest.mark.django_db +def test_aim_credential_source(run_module, admin_user, organization, silence_deprecation): + src_cred = source_cred_aim(organization) + ct=CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=src_cred.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"object_query": "Safe=SUPERSAFE;Object=MyAccount"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['object_query'] == "Safe=SUPERSAFE;Object=MyAccount" + assert cis.source_credential.name == src_cred.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test CyberArk Conjur credential source +@pytest.fixture +def source_cred_conjur(organization): + # Make a credential type which will be used by the credential + ct=CredentialType.defaults['conjur']() + ct.save() + return Credential.objects.create( + name='CyberArk CONJUR Cred', + credential_type=ct, + inputs={ + "url": "https://cyberark.example.com", + "api_key": "myApiKey", + "account": "account", + "username": "username" + } + ) + + +@pytest.mark.django_db +def test_conjur_credential_source(run_module, admin_user, organization, silence_deprecation): + src_cred = source_cred_conjur(organization) + ct=CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=src_cred.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.source_credential.name == src_cred.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Hashicorp Vault secret credential source +@pytest.fixture +def source_cred_hashi_secret(organization): + # Make a credential type which will be used by the credential + ct=CredentialType.defaults['hashivault_kv']() + ct.save() + return Credential.objects.create( + name='HashiCorp secret Cred', + credential_type=ct, + inputs={ + "url": "https://secret.hash.example.com", + "token": "myApiKey", + "role_id": "role", + "secret_id": "secret" + } + ) + + +@pytest.mark.django_db +def test_hashi_secret_credential_source(run_module, admin_user, organization, silence_deprecation): + src_cred = source_cred_hashi_secret(organization) + ct=CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=src_cred.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret", "auth_path": "/path/to/auth", "secret_backend": "backend", "secret_key": "a_key"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.metadata['auth_path'] == "/path/to/auth" + assert cis.metadata['secret_backend'] == "backend" + assert cis.metadata['secret_key'] == "a_key" + assert cis.source_credential.name == src_cred.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Hashicorp Vault signed ssh credential source +@pytest.fixture +def source_cred_hashi_ssh(organization): + # Make a credential type which will be used by the credential + ct=CredentialType.defaults['hashivault_ssh']() + ct.save() + return Credential.objects.create( + name='HashiCorp ssh Cred', + credential_type=ct, + inputs={ + "url": "https://ssh.hash.example.com", + "token": "myApiKey", + "role_id": "role", + "secret_id": "secret" + } + ) + + +@pytest.mark.django_db +def test_hashi_ssh_credential_source(run_module, admin_user, organization, silence_deprecation): + src_cred = source_cred_hashi_ssh(organization) + ct=CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=src_cred.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_path": "/path/to/secret", "auth_path": "/path/to/auth", "role": "role", "public_key": "a_key", "valid_principals": "some_value"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_path'] == "/path/to/secret" + assert cis.metadata['auth_path'] == "/path/to/auth" + assert cis.metadata['role'] == "role" + assert cis.metadata['public_key'] == "a_key" + assert cis.metadata['valid_principals'] == "some_value" + assert cis.source_credential.name == src_cred.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk + + +# Test Azure Key Vault credential source +@pytest.fixture +def source_cred_azure_kv(organization): + # Make a credential type which will be used by the credential + ct=CredentialType.defaults['azure_kv']() + ct.save() + return Credential.objects.create( + name='Azure KV Cred', + credential_type=ct, + inputs={ + "url": "https://key.azure.example.com", + "client": "client", + "secret": "secret", + "tenant": "tenant", + "cloud_name": "the_cloud", + } + ) + + +@pytest.mark.django_db +def test_azure_kv_credential_source(run_module, admin_user, organization, silence_deprecation): + src_cred = source_cred_azure_kv(organization) + ct=CredentialType.defaults['ssh']() + ct.save() + tgt_cred = Credential.objects.create( + name='Test Machine Credential', + organization=organization, + credential_type=ct, + inputs={'username': 'bob'} + ) + + result = run_module('tower_credential_input_source', dict( + source_credential=src_cred.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"secret_field": "my_pass"}, + state='present' + ), admin_user) + + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + + assert CredentialInputSource.objects.count() == 1 + cis = CredentialInputSource.objects.first() + + assert cis.metadata['secret_field'] == "my_pass" + assert cis.source_credential.name == src_cred.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk diff --git a/awx_collection/tests/integration/targets/tower_credential_input_source/tasks/main.yml b/awx_collection/tests/integration/targets/tower_credential_input_source/tasks/main.yml new file mode 100644 index 0000000000..9e17847818 --- /dev/null +++ b/awx_collection/tests/integration/targets/tower_credential_input_source/tasks/main.yml @@ -0,0 +1,74 @@ +--- +- name: Generate names + set_fact: + src_cred_name: src_cred + target_cred_name: target_cred + +- name: Add Tower credential Lookup + tower_credential: + description: Credential for Testing Source + name: "{{ src_cred_name }}" + credential_type: CyberArk AIM Central Credential Provider Lookup + inputs: + url: "https://cyberark.example.com" + app_id: "My-App-ID" + organization: Default + register: result + +- assert: + that: + - "result is changed" + +- name: Add Tower credential Target + tower_credential: + description: Credential for Testing Target + name: "{{ target_cred_name }}" + credential_type: Machine + inputs: + username: user + organization: Default + register: result + +- assert: + that: + - "result is changed" + +- name: Add credential Input Source + tower_credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_name }}" + source_credential: "{{ src_cred_name }}" + metadata: + object_query: "Safe=MY_SAFE;Object=AWX-user" + object_query_format: "Exact" + state: present + +- assert: + that: + - "result is changed" + +- name: Remove a Tower credential type + tower_credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_name }}" + source_credential: "{{ src_cred_name }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Remove Tower credential Lookup + tower_credential: + name: "{{ src_cred_name }}" + organization: Default + state: absent + register: result + +- name: Remove Tower credential Lookup + tower_credential: + name: "{{ target_cred_name }}" + organization: Default + state: absent + register: result