use the m2m field for inventory source creds

This commit is contained in:
AlanCoding
2018-02-14 12:23:53 -05:00
parent 8505783350
commit 9c4d89f512
11 changed files with 409 additions and 169 deletions

View File

@@ -830,7 +830,8 @@ class InventorySourceAccess(BaseAccess):
'''
model = InventorySource
select_related = ('created_by', 'modified_by', 'inventory',)
select_related = ('created_by', 'modified_by', 'inventory')
prefetch_related = ('credentials',)
def filtered_queryset(self):
return self.model.objects.filter(inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))
@@ -878,6 +879,21 @@ class InventorySourceAccess(BaseAccess):
return self.user in obj.inventory.update_role
return False
@check_superuser
def can_attach(self, obj, sub_obj, relationship, data, skip_sub_obj_read_check=False):
if relationship == 'credentials' and isinstance(sub_obj, Credential):
return (
obj and obj.inventory and self.user in obj.inventory.admin_role and
self.user in sub_obj.use_role)
return super(InventorySourceAccess, self).can_attach(
obj, sub_obj, relationship, data, skip_sub_obj_read_check=skip_sub_obj_read_check)
@check_superuser
def can_unattach(self, obj, sub_obj, relationship, *args, **kwargs):
if relationship == 'credentials' and isinstance(sub_obj, Credential):
return obj and obj.inventory and self.user in obj.inventory.admin_role
return super(InventorySourceAccess, self).can_attach(obj, sub_obj, relationship, *args, **kwargs)
class InventoryUpdateAccess(BaseAccess):
'''
@@ -888,7 +904,7 @@ class InventoryUpdateAccess(BaseAccess):
model = InventoryUpdate
select_related = ('created_by', 'modified_by', 'inventory_source__inventory',)
prefetch_related = ('unified_job_template', 'instance_group',)
prefetch_related = ('unified_job_template', 'instance_group', 'credentials',)
def filtered_queryset(self):
return self.model.objects.filter(inventory_source__inventory__in=Inventory.accessible_pk_qs(self.user, 'read_role'))

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2018-02-14 16:14
from __future__ import unicode_literals
from django.db import migrations
from awx.main.migrations import _migration_utils as migration_utils
from awx.main.migrations._multi_cred import (
migrate_inventory_source_cred,
migrate_inventory_source_cred_reverse
)
class Migration(migrations.Migration):
dependencies = [
('main', '0022_v330_create_new_rbac_roles'),
]
operations = [
# Run data migration before removing the old credential field
migrations.RunPython(migration_utils.set_current_apps_for_migrations, migrations.RunPython.noop),
migrations.RunPython(migrate_inventory_source_cred, migrate_inventory_source_cred_reverse),
migrations.RemoveField(
model_name='inventorysource',
name='credential',
),
migrations.RemoveField(
model_name='inventoryupdate',
name='credential',
),
]

View File

@@ -32,3 +32,25 @@ def migrate_workflow_cred_reverse(app, schema_editor):
if cred:
node.credential = cred
node.save()
def migrate_inventory_source_cred(app, schema_editor):
InventoryUpdate = app.get_model('main', 'InventoryUpdate')
InventorySource = app.get_model('main', 'InventorySource')
for cls in (InventoryUpdate, InventorySource):
for obj in cls.objects.iterator():
if obj.credential:
obj.credentials.add(obj.credential)
def migrate_inventory_source_cred_reverse(app, schema_editor):
InventoryUpdate = app.get_model('main', 'InventoryUpdate')
InventorySource = app.get_model('main', 'InventorySource')
for cls in (InventoryUpdate, InventorySource):
for obj in cls.objects.iterator():
cred = obj.credentials.first()
if cred:
obj.credential = cred
obj.save()

View File

@@ -1084,14 +1084,6 @@ class InventorySourceOptions(BaseModel):
default='',
help_text=_('Inventory source variables in YAML or JSON format.'),
)
credential = models.ForeignKey(
'Credential',
related_name='%(class)ss',
null=True,
default=None,
blank=True,
on_delete=models.SET_NULL,
)
source_regions = models.CharField(
max_length=1024,
blank=True,
@@ -1223,30 +1215,48 @@ class InventorySourceOptions(BaseModel):
"""No region supprt"""
return [('all', 'All')]
def clean_credential(self):
if not self.source:
@staticmethod
def cloud_credential_validation(source, cred):
if not source:
return None
cred = self.credential
if cred and self.source not in ('custom', 'scm'):
if cred and source not in ('custom', 'scm'):
# If a credential was provided, it's important that it matches
# the actual inventory source being used (Amazon requires Amazon
# credentials; Rackspace requires Rackspace credentials; etc...)
if self.source.replace('ec2', 'aws') != cred.kind:
raise ValidationError(
_('Cloud-based inventory sources (such as %s) require '
'credentials for the matching cloud service.') % self.source
)
if source.replace('ec2', 'aws') != cred.kind:
return _('Cloud-based inventory sources (such as %s) require '
'credentials for the matching cloud service.') % source
# Allow an EC2 source to omit the credential. If Tower is running on
# an EC2 instance with an IAM Role assigned, boto will use credentials
# from the instance metadata instead of those explicitly provided.
elif self.source in CLOUD_PROVIDERS and self.source != 'ec2':
raise ValidationError(_('Credential is required for a cloud source.'))
elif self.source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
raise ValidationError(_(
elif source in CLOUD_PROVIDERS and source != 'ec2':
return _('Credential is required for a cloud source.')
elif source == 'custom' and cred and cred.credential_type.kind in ('scm', 'ssh', 'insights', 'vault'):
return _(
'Credentials of type machine, source control, insights and vault are '
'disallowed for custom inventory sources.'
))
return cred
)
return None
def get_deprecated_credential(self, kind):
for cred in self.credentials.all():
if cred.credential_type.kind == kind:
return cred
else:
return None
def get_cloud_credential(self):
credential = None
for cred in self.credentials.all():
if cred.credential_type.kind != 'vault':
credential = cred
return credential
@property
def credential(self):
cred = self.get_cloud_credential()
if cred is not None:
return cred.pk
def clean_source_regions(self):
regions = self.source_regions
@@ -1376,7 +1386,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions):
@classmethod
def _get_unified_job_field_names(cls):
return set(f.name for f in InventorySourceOptions._meta.fields) | set(
['name', 'description', 'schedule']
['name', 'description', 'schedule', 'credentials']
)
def save(self, *args, **kwargs):
@@ -1621,7 +1631,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
return False
if (self.source not in ('custom', 'ec2', 'scm') and
not (self.credential)):
not (self.get_cloud_credential())):
return False
elif self.source == 'scm' and not self.inventory_source.source_project:
return False

View File

@@ -924,8 +924,11 @@ class BaseTask(LogErrorsTask):
credentials = []
if isinstance(instance, Job):
credentials = instance.credentials.all()
elif isinstance(instance, InventoryUpdate):
# TODO: allow multiple custom creds for inv updates
credentials = [instance.get_cloud_credential()]
elif hasattr(instance, 'credential'):
# once other UnifiedJobs (project updates, inventory updates)
# once other UnifiedJobs (project updates)
# move from a .credential -> .credentials model, we can
# lose this block
credentials = [instance.credential]
@@ -1719,14 +1722,13 @@ class RunInventoryUpdate(BaseTask):
If no private data is needed, return None.
"""
private_data = {'credentials': {}}
credential = inventory_update.get_cloud_credential()
# If this is GCE, return the RSA key
if inventory_update.source == 'gce':
credential = inventory_update.credential
private_data['credentials'][credential] = decrypt_field(credential, 'ssh_key_data')
return private_data
if inventory_update.source == 'openstack':
credential = inventory_update.credential
openstack_auth = dict(auth_url=credential.host,
username=credential.username,
password=decrypt_field(credential, "password"),
@@ -1803,7 +1805,6 @@ class RunInventoryUpdate(BaseTask):
cp.set(section, k, six.text_type(v))
# Allow custom options to vmware inventory script.
elif inventory_update.source == 'vmware':
credential = inventory_update.credential
section = 'vmware'
cp.add_section(section)
@@ -1838,7 +1839,6 @@ class RunInventoryUpdate(BaseTask):
else:
cp.set(section, k, six.text_type(v))
credential = inventory_update.credential
if credential:
cp.set(section, 'url', credential.host)
cp.set(section, 'user', credential.username)
@@ -1859,7 +1859,6 @@ class RunInventoryUpdate(BaseTask):
section = 'cloudforms'
cp.add_section(section)
credential = inventory_update.credential
if credential:
cp.set(section, 'url', credential.host)
cp.set(section, 'username', credential.username)
@@ -1897,7 +1896,7 @@ class RunInventoryUpdate(BaseTask):
if cp.sections():
f = cStringIO.StringIO()
cp.write(f)
private_data['credentials'][inventory_update.credential] = f.getvalue()
private_data['credentials'][credential] = f.getvalue()
return private_data
def build_passwords(self, inventory_update, **kwargs):
@@ -1912,7 +1911,7 @@ class RunInventoryUpdate(BaseTask):
# Take key fields from the credential in use and add them to the
# passwords dictionary.
credential = inventory_update.credential
credential = inventory_update.get_cloud_credential()
if credential:
for subkey in ('username', 'host', 'project', 'client', 'tenant', 'subscription'):
passwords['source_%s' % subkey] = getattr(credential, subkey)
@@ -1957,7 +1956,9 @@ class RunInventoryUpdate(BaseTask):
}
if inventory_update.source in ini_mapping:
cred_data = kwargs.get('private_data_files', {}).get('credentials', '')
env[ini_mapping[inventory_update.source]] = cred_data.get(inventory_update.credential, '')
env[ini_mapping[inventory_update.source]] = cred_data.get(
inventory_update.get_cloud_credential(), ''
)
if inventory_update.source == 'gce':
env['GCE_ZONE'] = inventory_update.source_regions if inventory_update.source_regions != 'all' else '' # noqa

View File

@@ -1426,9 +1426,9 @@ def test_field_removal(put, organization, admin, credentialtype_ssh, version, pa
@pytest.mark.parametrize('relation, related_obj', [
['ad_hoc_commands', AdHocCommand()],
['insights_inventories', Inventory()],
['inventorysources', InventorySource()],
['unifiedjobs', Job()],
['unifiedjobtemplates', JobTemplate()],
['unifiedjobtemplates', InventorySource()],
['projects', Project()],
['workflowjobnodes', WorkflowJobNode()],
])

View File

@@ -103,18 +103,22 @@ def test_safe_env_returns_new_copy():
def test_openstack_client_config_generation(mocker):
update = tasks.RunInventoryUpdate()
credential = mocker.Mock(**{
'host': 'https://keystone.openstack.example.org',
'username': 'demo',
'password': 'secrete',
'project': 'demo-project',
'domain': 'my-demo-domain',
})
cred_method = mocker.Mock(return_value=credential)
inventory_update = mocker.Mock(**{
'source': 'openstack',
'credential.host': 'https://keystone.openstack.example.org',
'credential.username': 'demo',
'credential.password': 'secrete',
'credential.project': 'demo-project',
'credential.domain': 'my-demo-domain',
'source_vars_dict': {}
'source_vars_dict': {},
'get_cloud_credential': cred_method
})
cloud_config = update.build_private_data(inventory_update)
cloud_credential = yaml.load(
cloud_config.get('credentials')[inventory_update.credential]
cloud_config.get('credentials')[credential]
)
assert cloud_credential['clouds'] == {
'devstack': {
@@ -135,18 +139,22 @@ def test_openstack_client_config_generation(mocker):
])
def test_openstack_client_config_generation_with_private_source_vars(mocker, source, expected):
update = tasks.RunInventoryUpdate()
credential = mocker.Mock(**{
'host': 'https://keystone.openstack.example.org',
'username': 'demo',
'password': 'secrete',
'project': 'demo-project',
'domain': None,
})
cred_method = mocker.Mock(return_value=credential)
inventory_update = mocker.Mock(**{
'source': 'openstack',
'credential.host': 'https://keystone.openstack.example.org',
'credential.username': 'demo',
'credential.password': 'secrete',
'credential.project': 'demo-project',
'credential.domain': None,
'source_vars_dict': {'private': source}
'source_vars_dict': {'private': source},
'get_cloud_credential': cred_method
})
cloud_config = update.build_private_data(inventory_update)
cloud_credential = yaml.load(
cloud_config.get('credentials')[inventory_update.credential]
cloud_config.get('credentials')[credential]
)
assert cloud_credential['clouds'] == {
'devstack': {
@@ -1519,8 +1527,9 @@ class TestInventoryUpdateCredentials(TestJobExecution):
)
)
def test_source_without_credential(self):
def test_source_without_credential(self, mocker):
self.instance.source = 'ec2'
self.instance.get_cloud_credential = mocker.Mock(return_value=None)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1538,7 +1547,7 @@ class TestInventoryUpdateCredentials(TestJobExecution):
self.task.run(self.pk)
@pytest.mark.parametrize('with_credential', [True, False])
def test_custom_source(self, with_credential):
def test_custom_source(self, with_credential, mocker):
self.instance.source = 'custom'
self.instance.source_vars = '{"FOO": "BAR"}'
patch = mock.patch.object(InventoryUpdate, 'source_script', mock.Mock(
@@ -1549,16 +1558,22 @@ class TestInventoryUpdateCredentials(TestJobExecution):
if with_credential:
azure_rm = CredentialType.defaults['azure_rm']()
self.instance.credential = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'client': 'some-client',
'secret': 'some-secret',
'tenant': 'some-tenant',
'subscription': 'some-subscription',
}
)
def get_cred():
cred = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'client': 'some-client',
'secret': 'some-secret',
'tenant': 'some-tenant',
'subscription': 'some-subscription',
}
)
return cred
self.instance.get_cloud_credential = get_cred
else:
self.instance.get_cloud_credential = mocker.Mock(return_value=None)
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1580,14 +1595,16 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_ec2_source(self):
aws = CredentialType.defaults['aws']()
self.instance.source = 'ec2'
self.instance.credential = Credential(
pk=1,
credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'}
)
self.instance.credential.inputs['password'] = encrypt_field(
self.instance.credential, 'password'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=aws,
inputs = {'username': 'bob', 'password': 'secret'}
)
cred.inputs['password'] = encrypt_field(cred, 'password')
return cred
self.instance.get_cloud_credential = get_cred
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1608,14 +1625,16 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_vmware_source(self):
vmware = CredentialType.defaults['vmware']()
self.instance.source = 'vmware'
self.instance.credential = Credential(
pk=1,
credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
)
self.instance.credential.inputs['password'] = encrypt_field(
self.instance.credential, 'password'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=vmware,
inputs = {'username': 'bob', 'password': 'secret', 'host': 'https://example.org'}
)
cred.inputs['password'] = encrypt_field(cred, 'password')
return cred
self.instance.get_cloud_credential = get_cred
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1634,17 +1653,21 @@ class TestInventoryUpdateCredentials(TestJobExecution):
azure_rm = CredentialType.defaults['azure_rm']()
self.instance.source = 'azure_rm'
self.instance.source_regions = 'north, south, east, west'
self.instance.credential = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'client': 'some-client',
'secret': 'some-secret',
'tenant': 'some-tenant',
'subscription': 'some-subscription',
'cloud_environment': 'foobar'
}
)
def get_cred():
cred = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'client': 'some-client',
'secret': 'some-secret',
'tenant': 'some-tenant',
'subscription': 'some-subscription',
'cloud_environment': 'foobar'
}
)
return cred
self.instance.get_cloud_credential = get_cred
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1671,16 +1694,20 @@ class TestInventoryUpdateCredentials(TestJobExecution):
azure_rm = CredentialType.defaults['azure_rm']()
self.instance.source = 'azure_rm'
self.instance.source_regions = 'all'
self.instance.credential = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'subscription': 'some-subscription',
'username': 'bob',
'password': 'secret',
'cloud_environment': 'foobar'
}
)
def get_cred():
cred = Credential(
pk=1,
credential_type=azure_rm,
inputs = {
'subscription': 'some-subscription',
'username': 'bob',
'password': 'secret',
'cloud_environment': 'foobar'
}
)
return cred
self.instance.get_cloud_credential = get_cred
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1706,18 +1733,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
gce = CredentialType.defaults['gce']()
self.instance.source = 'gce'
self.instance.source_regions = 'all'
self.instance.credential = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
'project': 'some-project',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
self.instance.credential.inputs['ssh_key_data'] = encrypt_field(
self.instance.credential, 'ssh_key_data'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
'project': 'some-project',
'ssh_key_data': self.EXAMPLE_PRIVATE_KEY
}
)
cred.inputs['ssh_key_data'] = encrypt_field(
cred, 'ssh_key_data'
)
return cred
self.instance.get_cloud_credential = get_cred
expected_gce_zone = ''
def run_pexpect_side_effect(*args, **kwargs):
@@ -1745,19 +1777,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_openstack_source(self):
openstack = CredentialType.defaults['openstack']()
self.instance.source = 'openstack'
self.instance.credential = Credential(
pk=1,
credential_type=openstack,
inputs = {
'username': 'bob',
'password': 'secret',
'project': 'tenant-name',
'host': 'https://keystone.example.org'
}
)
self.instance.credential.inputs['ssh_key_data'] = encrypt_field(
self.instance.credential, 'ssh_key_data'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=openstack,
inputs = {
'username': 'bob',
'password': 'secret',
'project': 'tenant-name',
'host': 'https://keystone.example.org'
}
)
cred.inputs['ssh_key_data'] = encrypt_field(
cred, 'ssh_key_data'
)
return cred
self.instance.get_cloud_credential = get_cred
def run_pexpect_side_effect(*args, **kwargs):
args, cwd, env, stdout = args
@@ -1780,18 +1816,23 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_satellite6_source(self):
satellite6 = CredentialType.defaults['satellite6']()
self.instance.source = 'satellite6'
self.instance.credential = Credential(
pk=1,
credential_type=satellite6,
inputs = {
'username': 'bob',
'password': 'secret',
'host': 'https://example.org'
}
)
self.instance.credential.inputs['password'] = encrypt_field(
self.instance.credential, 'password'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=satellite6,
inputs = {
'username': 'bob',
'password': 'secret',
'host': 'https://example.org'
}
)
cred.inputs['password'] = encrypt_field(
cred, 'password'
)
return cred
self.instance.get_cloud_credential = get_cred
self.instance.source_vars = '{"satellite6_group_patterns": "[a,b,c]", "satellite6_group_prefix": "hey_"}'
def run_pexpect_side_effect(*args, **kwargs):
@@ -1811,18 +1852,22 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_cloudforms_source(self):
cloudforms = CredentialType.defaults['cloudforms']()
self.instance.source = 'cloudforms'
self.instance.credential = Credential(
pk=1,
credential_type=cloudforms,
inputs = {
'username': 'bob',
'password': 'secret',
'host': 'https://example.org'
}
)
self.instance.credential.inputs['password'] = encrypt_field(
self.instance.credential, 'password'
)
def get_cred():
cred = Credential(
pk=1,
credential_type=cloudforms,
inputs = {
'username': 'bob',
'password': 'secret',
'host': 'https://example.org'
}
)
cred.inputs['password'] = encrypt_field(
cred, 'password'
)
return cred
self.instance.get_cloud_credential = get_cred
self.instance.source_vars = '{"prefer_ipv4": True}'
@@ -1847,14 +1892,19 @@ class TestInventoryUpdateCredentials(TestJobExecution):
def test_awx_task_env(self):
gce = CredentialType.defaults['gce']()
self.instance.source = 'gce'
self.instance.credential = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
'project': 'some-project',
}
)
def get_cred():
cred = Credential(
pk=1,
credential_type=gce,
inputs = {
'username': 'bob',
'project': 'some-project',
}
)
return cred
self.instance.get_cloud_credential = get_cred
patch = mock.patch('awx.main.tasks.settings.AWX_TASK_ENV', {'FOO': 'BAR'})
patch.start()