split machine CredentialType into two distinct (ssh and vault) kinds

This commit is contained in:
Ryan Petrello
2017-05-01 15:42:56 -04:00
parent 3648757efd
commit a1fa9243bc
7 changed files with 81 additions and 43 deletions

View File

@@ -6,7 +6,7 @@ import re
import json import json
# Django # Django
from django.core.exceptions import FieldError, ValidationError, ObjectDoesNotExist from django.core.exceptions import FieldError, ValidationError
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.db.models.fields import FieldDoesNotExist from django.db.models.fields import FieldDoesNotExist
@@ -174,18 +174,6 @@ class FieldLookupBackend(BaseFilterBackend):
except UnicodeEncodeError: except UnicodeEncodeError:
raise ValueError("%r is not an allowed field name. Must be ascii encodable." % lookup) raise ValueError("%r is not an allowed field name. Must be ascii encodable." % lookup)
# Make legacy v1 Credential fields work for backwards compatability
# TODO: remove after API v1 deprecation period
if model._meta.object_name == 'Credential' and lookup == 'kind':
try:
type_ = CredentialType.from_v1_kind(value)
if type_ is None:
raise ParseError(_('cannot filter on kind %s') % value)
value = type_.pk
lookup = 'credential_type'
except ObjectDoesNotExist as e:
raise ParseError(_('cannot filter on kind %s') % value)
field, new_lookup = self.get_field_from_lookup(model, lookup) field, new_lookup = self.get_field_from_lookup(model, lookup)
# Type names are stored without underscores internally, but are presented and # Type names are stored without underscores internally, but are presented and
@@ -277,6 +265,32 @@ class FieldLookupBackend(BaseFilterBackend):
key = key[5:] key = key[5:]
q_not = True q_not = True
# Make legacy v1 Credential fields work for backwards compatability
# TODO: remove after API v1 deprecation period
#
# convert v1 `Credential.kind` queries to `Credential.credential_type__pk`
if queryset.model._meta.object_name == 'Credential' and key == 'kind':
key = key.replace('kind', 'credential_type')
if 'ssh' in values:
# In 3.2, SSH and Vault became separate credential types, but in the v1 API,
# they're both still "kind=ssh"
# under the hood, convert `/api/v1/credentials/?kind=ssh` to
# `/api/v1/credentials/?or__credential_type=<ssh_pk>&or__credential_type=<vault_pk>`
values = set(values)
values.add('vault')
values = list(values)
q_or = True
for i, kind in enumerate(values):
if kind == 'vault':
type_ = CredentialType.objects.get(kind=kind)
else:
type_ = CredentialType.from_v1_kind(kind)
if type_ is None:
raise ParseError(_('cannot filter on kind %s') % kind)
values[i] = type_.pk
# Convert value(s) to python and add to the appropriate list. # Convert value(s) to python and add to the appropriate list.
for value in values: for value in values:
if q_int: if q_int:

View File

@@ -318,15 +318,6 @@ class BaseSerializer(serializers.ModelSerializer):
fval = getattr(fkval, field, None) fval = getattr(fkval, field, None)
# TODO: remove when API v1 is removed
if all([
self.version == 1,
'credential' in fk,
field == 'kind',
fval == 'machine'
]):
fval = 'ssh'
if fval is None and field == 'type': if fval is None and field == 'type':
if isinstance(fkval, PolymorphicModel): if isinstance(fkval, PolymorphicModel):
fkval = fkval.get_real_instance() fkval = fkval.get_real_instance()
@@ -1883,9 +1874,8 @@ class CredentialSerializer(BaseSerializer):
# TODO: remove when API v1 is removed # TODO: remove when API v1 is removed
if self.version == 1: if self.version == 1:
if value.get('kind') == 'machine': if value.get('kind') == 'vault':
value['kind'] = 'ssh' value['kind'] = 'ssh'
for field in V1Credential.PASSWORD_FIELDS: for field in V1Credential.PASSWORD_FIELDS:
if field in value and force_text(value[field]).startswith('$encrypted$'): if field in value and force_text(value[field]).startswith('$encrypted$'):
value[field] = '$encrypted$' value[field] = '$encrypted$'

View File

@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
('modified', models.DateTimeField(default=None, editable=False)), ('modified', models.DateTimeField(default=None, editable=False)),
('description', models.TextField(default=b'', blank=True)), ('description', models.TextField(default=b'', blank=True)),
('name', models.CharField(max_length=512)), ('name', models.CharField(max_length=512)),
('kind', models.CharField(max_length=32, choices=[(b'machine', 'Machine'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'cloud', 'Cloud')])), ('kind', models.CharField(max_length=32, choices=[(b'ssh', 'SSH'), (b'vault', 'Vault'), (b'net', 'Network'), (b'scm', 'Source Control'), (b'cloud', 'Cloud')])),
('managed_by_tower', models.BooleanField(default=False, editable=False)), ('managed_by_tower', models.BooleanField(default=False, editable=False)),
('inputs', awx.main.fields.CredentialTypeInputField(default={}, blank=True)), ('inputs', awx.main.fields.CredentialTypeInputField(default={}, blank=True)),
('injectors', awx.main.fields.CredentialTypeInjectorField(default={}, blank=True)), ('injectors', awx.main.fields.CredentialTypeInjectorField(default={}, blank=True)),

View File

@@ -99,7 +99,7 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
def clean_credential(self): def clean_credential(self):
cred = self.credential cred = self.credential
if cred and cred.kind != 'machine': if cred and cred.kind != 'ssh':
raise ValidationError( raise ValidationError(
_('You must provide a machine / SSH credential.'), _('You must provide a machine / SSH credential.'),
) )

View File

@@ -317,7 +317,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
# #
@property @property
def needs_ssh_password(self): def needs_ssh_password(self):
return self.kind == 'machine' and self.password == 'ASK' return self.kind == 'ssh' and self.password == 'ASK'
@property @property
def has_encrypted_ssh_key_data(self): def has_encrypted_ssh_key_data(self):
@@ -336,17 +336,17 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
@property @property
def needs_ssh_key_unlock(self): def needs_ssh_key_unlock(self):
if self.kind == 'machine' and self.ssh_key_unlock in ('ASK', ''): if self.kind == 'ssh' and self.ssh_key_unlock in ('ASK', ''):
return self.has_encrypted_ssh_key_data return self.has_encrypted_ssh_key_data
return False return False
@property @property
def needs_become_password(self): def needs_become_password(self):
return self.kind == 'machine' and self.become_password == 'ASK' return self.kind == 'ssh' and self.become_password == 'ASK'
@property @property
def needs_vault_password(self): def needs_vault_password(self):
return self.kind == 'machine' and self.vault_password == 'ASK' return self.kind == 'vault' and self.vault_password == 'ASK'
@property @property
def passwords_needed(self): def passwords_needed(self):
@@ -396,7 +396,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
raise ValidationError(_('Credential cannot be assigned to both a user and team.')) raise ValidationError(_('Credential cannot be assigned to both a user and team.'))
def _password_field_allows_ask(self, field): def _password_field_allows_ask(self, field):
return bool(self.kind == 'machine' and field != 'ssh_key_data') return field in self.credential_type.askable_fields
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
inputs_before = {} inputs_before = {}
@@ -472,7 +472,8 @@ class CredentialType(CommonModelNameNotUnique):
unique_together = (('name', 'kind'),) unique_together = (('name', 'kind'),)
KIND_CHOICES = ( KIND_CHOICES = (
('machine', _('Machine')), ('ssh', _('SSH')),
('vault', _('Vault')),
('net', _('Network')), ('net', _('Network')),
('scm', _('Source Control')), ('scm', _('Source Control')),
('cloud', _('Cloud')) ('cloud', _('Cloud'))
@@ -513,6 +514,13 @@ class CredentialType(CommonModelNameNotUnique):
if field.get('secret', False) is True if field.get('secret', False) is True
] ]
@property
def askable_fields(self):
return [
field['id'] for field in self.inputs.get('fields', [])
if field.get('ask_at_runtime', False) is True
]
@classmethod @classmethod
def default(cls, f): def default(cls, f):
func = functools.partial(f, cls) func = functools.partial(f, cls)
@@ -534,15 +542,9 @@ class CredentialType(CommonModelNameNotUnique):
requirements = {} requirements = {}
if kind == 'ssh': if kind == 'ssh':
if 'vault_password' in data: if 'vault_password' in data:
requirements.update(dict( requirements['kind'] = 'vault'
kind='machine',
name='Vault'
))
else: else:
requirements.update(dict( requirements['kind'] = 'ssh'
kind='machine',
name='SSH'
))
elif kind in ('net', 'scm'): elif kind in ('net', 'scm'):
requirements['kind'] = kind requirements['kind'] = kind
elif kind in kind_choices: elif kind in kind_choices:
@@ -643,7 +645,7 @@ class CredentialType(CommonModelNameNotUnique):
@CredentialType.default @CredentialType.default
def ssh(cls): def ssh(cls):
return cls( return cls(
kind='machine', kind='ssh',
name='SSH', name='SSH',
managed_by_tower=True, managed_by_tower=True,
inputs={ inputs={
@@ -724,7 +726,7 @@ def scm(cls):
@CredentialType.default @CredentialType.default
def vault(cls): def vault(cls):
return cls( return cls(
kind='machine', kind='vault',
name='Vault', name='Vault',
managed_by_tower=True, managed_by_tower=True,
inputs={ inputs={

View File

@@ -168,7 +168,7 @@ class JobOptions(BaseModel):
def clean_credential(self): def clean_credential(self):
cred = self.credential cred = self.credential
if cred and cred.kind != 'machine': if cred and cred.kind != 'ssh':
raise ValidationError( raise ValidationError(
_('You must provide a machine / SSH credential.'), _('You must provide a machine / SSH credential.'),
) )

View File

@@ -33,6 +33,38 @@ def test_filter_by_v1_kind(get, admin, organization, kind, total):
assert response.data['count'] == total assert response.data['count'] == total
@pytest.mark.django_db
def test_filter_by_v1_kind_with_vault(get, admin, organization):
CredentialType.setup_tower_managed_defaults()
cred = Credential(
credential_type=CredentialType.objects.get(kind='ssh'),
name='Best credential ever',
organization=organization,
inputs={
'username': u'jim',
'password': u'secret'
}
)
cred.save()
cred = Credential(
credential_type=CredentialType.objects.get(kind='vault'),
name='Best credential ever',
organization=organization,
inputs={
'vault_password': u'vault!'
}
)
cred.save()
response = get(
reverse('api:credential_list', kwargs={'version': 'v1'}),
admin,
QUERY_STRING='kind=ssh'
)
assert response.status_code == 200
assert response.data['count'] == 2
@pytest.mark.django_db @pytest.mark.django_db
def test_custom_credentials_not_in_v1_api_list(get, admin, organization): def test_custom_credentials_not_in_v1_api_list(get, admin, organization):
""" """