mirror of
https://github.com/ansible/awx.git
synced 2026-02-22 05:30:18 -03:30
split machine CredentialType into two distinct (ssh and vault) kinds
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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$'
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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.'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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.'),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user