diff --git a/awx/main/credential_plugins/centrify_vault.py b/awx/main/credential_plugins/centrify_vault.py new file mode 100644 index 0000000000..dc4db1fe22 --- /dev/null +++ b/awx/main/credential_plugins/centrify_vault.py @@ -0,0 +1,142 @@ +from .plugin import CredentialPlugin, raise_for_status +from django.utils.translation import ugettext_lazy as _ +from urllib.parse import urljoin +import requests +pas_inputs = { + 'fields': [{ + 'id': 'url', + 'label': _('Centrify Tenant URL'), + 'type': 'string', + 'help_text': _('Centrify Tenant URL'), + 'format': 'url', + }, { + 'id':'client_id', + 'label':_('Centrify API User'), + 'type':'string', + 'help_text': _('Centrify API User, having necessary permissions as mentioned in support doc'), + + }, { + 'id':'client_password', + 'label':_('Centrify API Password'), + 'type':'string', + 'help_text': _('Password of Centrify API User with necessary permissions'), + 'secret':True, + },{ + 'id':'oauth_application_id', + 'label':_('OAuth2 Application ID'), + 'type':'string', + 'help_text': _('Application ID of the configured OAuth2 Client (defaults to \'awx\')'), + 'default': 'awx', + },{ + 'id':'oauth_scope', + 'label':_('OAuth2 Scope'), + 'type':'string', + 'help_text': _('Scope of the configured OAuth2 Client (defaults to \'awx\')'), + 'default': 'awx', + }], + 'metadata': [{ + 'id': 'account-name', + 'label': _('Account Name'), + 'type': 'string', + 'help_text': _('Local system account or Domain account name enrolled in Centrify Vault. eg. (root or DOMAIN/Administrator)'), + },{ + 'id': 'system-name', + 'label': _('System Name'), + 'type': 'string', + 'help_text': _('Machine Name enrolled with in Centrify Portal'), + }], + 'required': ['url', 'account-name', 'system-name','client_id','client_password'], +} + + +# generate bearer token to authenticate with PAS portal, Input : Client ID, Client Secret +def handle_auth(**kwargs): + post_data = { + "grant_type": "client_credentials", + "scope": kwargs['oauth_scope'] + } + response = requests.post( + kwargs['endpoint'], + data = post_data, + auth = (kwargs['client_id'],kwargs['client_password']), + verify = True, + timeout = (5, 30) + ) + raise_for_status(response) + try: + return response.json()['access_token'] + except KeyError: + raise RuntimeError('OAuth request to tenant was unsuccessful') + + +# fetch the ID of system with RedRock query, Input : System Name, Account Name +def get_ID(**kwargs): + endpoint = urljoin(kwargs['url'],'/Redrock/query') + name=" Name='{0}' and User='{1}'".format(kwargs['system_name'],kwargs['acc_name']) + query = 'Select ID from VaultAccount where {0}'.format(name) + post_headers = { + "Authorization": "Bearer " + kwargs['access_token'], + "X-CENTRIFY-NATIVE-CLIENT":"true" + } + response = requests.post( + endpoint, + json = {'Script': query}, + headers = post_headers, + verify = True, + timeout = (5, 30) + ) + raise_for_status(response) + try: + result_str = response.json()["Result"]["Results"] + return result_str[0]["Row"]["ID"] + except (IndexError, KeyError): + raise RuntimeError("Error Detected!! Check the Inputs") + + +# CheckOut Password from Centrify Vault, Input : ID +def get_passwd(**kwargs): + endpoint = urljoin(kwargs['url'],'/ServerManage/CheckoutPassword') + post_headers = { + "Authorization": "Bearer " + kwargs['access_token'], + "X-CENTRIFY-NATIVE-CLIENT":"true" + } + response = requests.post( + endpoint, + json = {'ID': kwargs['acc_id']}, + headers = post_headers, + verify = True, + timeout = (5, 30) + ) + raise_for_status(response) + try: + return response.json()["Result"]["Password"] + except KeyError: + raise RuntimeError("Password Not Found") + + +def centrify_backend(**kwargs): + url = kwargs.get('url') + acc_name = kwargs.get('account-name') + system_name = kwargs.get('system-name') + client_id = kwargs.get('client_id') + client_password = kwargs.get('client_password') + app_id = kwargs.get('oauth_application_id', 'awx') + endpoint = urljoin(url, f'/oauth2/token/{app_id}') + endpoint = { + 'endpoint': endpoint, + 'client_id': client_id, + 'client_password': client_password, + 'oauth_scope': kwargs.get('oauth_scope', 'awx') + } + token = handle_auth(**endpoint) + get_id_args = {'system_name':system_name,'acc_name':acc_name,'url':url,'access_token':token} + acc_id = get_ID(**get_id_args) + get_pwd_args = {'url':url,'acc_id':acc_id,'access_token':token} + return get_passwd(**get_pwd_args) + + +centrify_plugin = CredentialPlugin( + 'Centrify Vault Credential Provider Lookup', + inputs=pas_inputs, + backend=centrify_backend +) diff --git a/awx/main/migrations/0133_centrify_vault_credtype.py b/awx/main/migrations/0133_centrify_vault_credtype.py new file mode 100644 index 0000000000..eee9507691 --- /dev/null +++ b/awx/main/migrations/0133_centrify_vault_credtype.py @@ -0,0 +1,20 @@ +from django.db import migrations + +from awx.main.models import CredentialType +from awx.main.utils.common import set_current_apps + + +def setup_tower_managed_defaults(apps, schema_editor): + set_current_apps(apps) + CredentialType.setup_tower_managed_defaults() + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0132_instancegroup_is_container_group'), + ] + + operations = [ + migrations.RunPython(setup_tower_managed_defaults), + ] diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index 4f87c249be..f2bfa92dac 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -79,6 +79,7 @@ def test_default_cred_types(): 'aws', 'azure_kv', 'azure_rm', + 'centrify_vault_kv', 'conjur', 'galaxy_api_token', 'gce', diff --git a/awx_collection/test/awx/test_credential_input_source.py b/awx_collection/test/awx/test_credential_input_source.py index 703ad4adb3..8984fdfb12 100644 --- a/awx_collection/test/awx/test_credential_input_source.py +++ b/awx_collection/test/awx/test_credential_input_source.py @@ -332,3 +332,52 @@ def test_aim_credential_source(run_module, admin_user, organization, source_cred assert cis.source_credential.name == source_cred_aim_alt.name assert cis.target_credential.name == tgt_cred.name assert cis.input_field_name == 'password' + + + # Test Centrify Vault secret credential source +@pytest.fixture +def source_cred_centrify_secret(organization): + # Make a credential type which will be used by the credential + ct = CredentialType.defaults['centrify_vault_kv']() + ct.save() + return Credential.objects.create( + name='Centrify vault secret Cred', + credential_type=ct, + inputs={ + "url": "https://tenant_id.my.centrify-dev.net", + "client_id": "secretuser@tenant", + "client_password": "secretuserpassword", + } + ) + + +@pytest.mark.django_db +def test_centrify_vault_credential_source(run_module, admin_user, organization, source_cred_centrify_secret, silence_deprecation): + 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=source_cred_centrify_secret.name, + target_credential=tgt_cred.name, + input_field_name='password', + metadata={"system-name": "systemname", "account-name": "accountname"}, + 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['system-name'] == "systemname" + assert cis.metadata['account-name'] == "accountname" + assert cis.source_credential.name == source_cred_centrify_secret.name + assert cis.target_credential.name == tgt_cred.name + assert cis.input_field_name == 'password' + assert result['id'] == cis.pk diff --git a/setup.py b/setup.py index ef696c9deb..55fcff6785 100755 --- a/setup.py +++ b/setup.py @@ -130,7 +130,8 @@ setup( 'hashivault_kv = awx.main.credential_plugins.hashivault:hashivault_kv_plugin', 'hashivault_ssh = awx.main.credential_plugins.hashivault:hashivault_ssh_plugin', 'azure_kv = awx.main.credential_plugins.azure_kv:azure_keyvault_plugin', - 'aim = awx.main.credential_plugins.aim:aim_plugin' + 'aim = awx.main.credential_plugins.aim:aim_plugin', + 'centrify_vault_kv = awx.main.credential_plugins.centrify_vault:centrify_plugin' ] }, data_files = proc_data_files([