mirror of
https://github.com/ansible/awx.git
synced 2026-03-21 10:57:36 -02:30
AC-637 Credential now requires scm_key_unlock when saving encrypted ssh_key_data.
This commit is contained in:
@@ -93,6 +93,7 @@ class ChoiceField(fields.ChoiceField):
|
|||||||
# ModelSerializer.
|
# ModelSerializer.
|
||||||
serializers.ChoiceField = ChoiceField
|
serializers.ChoiceField = ChoiceField
|
||||||
|
|
||||||
|
|
||||||
class BaseSerializer(serializers.ModelSerializer):
|
class BaseSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
# add the URL and related resources
|
# add the URL and related resources
|
||||||
@@ -184,10 +185,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
else:
|
else:
|
||||||
return obj.active
|
return obj.active
|
||||||
|
|
||||||
def validate_description(self, attrs, source):
|
|
||||||
# Description should always be empty string, never null.
|
|
||||||
attrs[source] = attrs.get(source, None) or ''
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
class UserSerializer(BaseSerializer):
|
class UserSerializer(BaseSerializer):
|
||||||
|
|
||||||
@@ -275,6 +272,7 @@ class UserSerializer(BaseSerializer):
|
|||||||
def validate_is_superuser(self, attrs, source):
|
def validate_is_superuser(self, attrs, source):
|
||||||
return self._validate_ldap_managed_field(attrs, source)
|
return self._validate_ldap_managed_field(attrs, source)
|
||||||
|
|
||||||
|
|
||||||
class OrganizationSerializer(BaseSerializer):
|
class OrganizationSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -296,6 +294,7 @@ class OrganizationSerializer(BaseSerializer):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class ProjectSerializer(BaseSerializer):
|
class ProjectSerializer(BaseSerializer):
|
||||||
|
|
||||||
playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
|
playbooks = serializers.Field(source='playbooks', help_text='Array of playbooks available within this project.')
|
||||||
@@ -415,6 +414,7 @@ class ProjectSerializer(BaseSerializer):
|
|||||||
|
|
||||||
# FIXME: Validate combination of SCM URL and credential!
|
# FIXME: Validate combination of SCM URL and credential!
|
||||||
|
|
||||||
|
|
||||||
class ProjectPlaybooksSerializer(ProjectSerializer):
|
class ProjectPlaybooksSerializer(ProjectSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -425,6 +425,7 @@ class ProjectPlaybooksSerializer(ProjectSerializer):
|
|||||||
ret = super(ProjectPlaybooksSerializer, self).to_native(obj)
|
ret = super(ProjectPlaybooksSerializer, self).to_native(obj)
|
||||||
return ret.get('playbooks', [])
|
return ret.get('playbooks', [])
|
||||||
|
|
||||||
|
|
||||||
class ProjectUpdateSerializer(BaseSerializer):
|
class ProjectUpdateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -443,6 +444,7 @@ class ProjectUpdateSerializer(BaseSerializer):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class BaseSerializerWithVariables(BaseSerializer):
|
class BaseSerializerWithVariables(BaseSerializer):
|
||||||
|
|
||||||
def validate_variables(self, attrs, source):
|
def validate_variables(self, attrs, source):
|
||||||
@@ -455,6 +457,7 @@ class BaseSerializerWithVariables(BaseSerializer):
|
|||||||
raise serializers.ValidationError('Must be valid JSON or YAML')
|
raise serializers.ValidationError('Must be valid JSON or YAML')
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class InventorySerializer(BaseSerializerWithVariables):
|
class InventorySerializer(BaseSerializerWithVariables):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -483,6 +486,7 @@ class InventorySerializer(BaseSerializerWithVariables):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class HostSerializer(BaseSerializerWithVariables):
|
class HostSerializer(BaseSerializerWithVariables):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -613,6 +617,7 @@ class GroupSerializer(BaseSerializerWithVariables):
|
|||||||
raise serializers.ValidationError('Invalid group name')
|
raise serializers.ValidationError('Invalid group name')
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class GroupTreeSerializer(GroupSerializer):
|
class GroupTreeSerializer(GroupSerializer):
|
||||||
|
|
||||||
children = serializers.SerializerMethodField('get_children')
|
children = serializers.SerializerMethodField('get_children')
|
||||||
@@ -630,6 +635,7 @@ class GroupTreeSerializer(GroupSerializer):
|
|||||||
children_qs = obj.children.filter(active=True)
|
children_qs = obj.children.filter(active=True)
|
||||||
return GroupTreeSerializer(children_qs, many=True).data
|
return GroupTreeSerializer(children_qs, many=True).data
|
||||||
|
|
||||||
|
|
||||||
class BaseVariableDataSerializer(BaseSerializer):
|
class BaseVariableDataSerializer(BaseSerializer):
|
||||||
|
|
||||||
def to_native(self, obj):
|
def to_native(self, obj):
|
||||||
@@ -645,24 +651,28 @@ class BaseVariableDataSerializer(BaseSerializer):
|
|||||||
data = {'variables': json.dumps(data)}
|
data = {'variables': json.dumps(data)}
|
||||||
return super(BaseVariableDataSerializer, self).from_native(data, files)
|
return super(BaseVariableDataSerializer, self).from_native(data, files)
|
||||||
|
|
||||||
|
|
||||||
class InventoryVariableDataSerializer(BaseVariableDataSerializer):
|
class InventoryVariableDataSerializer(BaseVariableDataSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Inventory
|
model = Inventory
|
||||||
fields = ('variables',)
|
fields = ('variables',)
|
||||||
|
|
||||||
|
|
||||||
class HostVariableDataSerializer(BaseVariableDataSerializer):
|
class HostVariableDataSerializer(BaseVariableDataSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Host
|
model = Host
|
||||||
fields = ('variables',)
|
fields = ('variables',)
|
||||||
|
|
||||||
|
|
||||||
class GroupVariableDataSerializer(BaseVariableDataSerializer):
|
class GroupVariableDataSerializer(BaseVariableDataSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
fields = ('variables',)
|
fields = ('variables',)
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceSerializer(BaseSerializer):
|
class InventorySourceSerializer(BaseSerializer):
|
||||||
|
|
||||||
#source_password = serializers.WritableField(required=False, default='')
|
#source_password = serializers.WritableField(required=False, default='')
|
||||||
@@ -732,6 +742,7 @@ class InventorySourceSerializer(BaseSerializer):
|
|||||||
# FIXME
|
# FIXME
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class InventoryUpdateSerializer(BaseSerializer):
|
class InventoryUpdateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -751,6 +762,7 @@ class InventoryUpdateSerializer(BaseSerializer):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class TeamSerializer(BaseSerializer):
|
class TeamSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -770,6 +782,7 @@ class TeamSerializer(BaseSerializer):
|
|||||||
))
|
))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(BaseSerializer):
|
class PermissionSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -791,6 +804,7 @@ class PermissionSerializer(BaseSerializer):
|
|||||||
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
res['inventory'] = reverse('api:inventory_detail', args=(obj.inventory.pk,))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
# Can only set either user or team.
|
# Can only set either user or team.
|
||||||
if attrs['user'] and attrs['team']:
|
if attrs['user'] and attrs['team']:
|
||||||
@@ -806,6 +820,7 @@ class PermissionSerializer(BaseSerializer):
|
|||||||
'assigning deployment permissions')
|
'assigning deployment permissions')
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class CredentialSerializer(BaseSerializer):
|
class CredentialSerializer(BaseSerializer):
|
||||||
|
|
||||||
# FIXME: may want to make some of these filtered based on user accessing
|
# FIXME: may want to make some of these filtered based on user accessing
|
||||||
@@ -847,6 +862,7 @@ class CredentialSerializer(BaseSerializer):
|
|||||||
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
|
res['team'] = reverse('api:team_detail', args=(obj.team.pk,))
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
class JobTemplateSerializer(BaseSerializer):
|
class JobTemplateSerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -881,6 +897,7 @@ class JobTemplateSerializer(BaseSerializer):
|
|||||||
raise serializers.ValidationError('Playbook not found for project')
|
raise serializers.ValidationError('Playbook not found for project')
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class JobSerializer(BaseSerializer):
|
class JobSerializer(BaseSerializer):
|
||||||
|
|
||||||
passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start')
|
passwords_needed_to_start = serializers.Field(source='passwords_needed_to_start')
|
||||||
@@ -943,6 +960,7 @@ class JobSerializer(BaseSerializer):
|
|||||||
data.setdefault('job_tags', job_template.job_tags)
|
data.setdefault('job_tags', job_template.job_tags)
|
||||||
return super(JobSerializer, self).from_native(data, files)
|
return super(JobSerializer, self).from_native(data, files)
|
||||||
|
|
||||||
|
|
||||||
class JobHostSummarySerializer(BaseSerializer):
|
class JobHostSummarySerializer(BaseSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -972,6 +990,7 @@ class JobHostSummarySerializer(BaseSerializer):
|
|||||||
pass
|
pass
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class JobEventSerializer(BaseSerializer):
|
class JobEventSerializer(BaseSerializer):
|
||||||
|
|
||||||
event_display = serializers.Field(source='get_event_display2')
|
event_display = serializers.Field(source='get_event_display2')
|
||||||
@@ -1013,6 +1032,7 @@ class JobEventSerializer(BaseSerializer):
|
|||||||
pass
|
pass
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class AuthTokenSerializer(serializers.Serializer):
|
class AuthTokenSerializer(serializers.Serializer):
|
||||||
|
|
||||||
username = serializers.CharField()
|
username = serializers.CharField()
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import yaml
|
|||||||
|
|
||||||
# Django
|
# Django
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ from taggit.managers import TaggableManager
|
|||||||
# Django-Celery
|
# Django-Celery
|
||||||
from djcelery.models import TaskMeta
|
from djcelery.models import TaskMeta
|
||||||
|
|
||||||
__all__ = ['VarsDictProperty', 'PrimordialModel', 'CommonModel',
|
__all__ = ['VarsDictProperty', 'BaseModel', 'PrimordialModel', 'CommonModel',
|
||||||
'CommonModelNameNotUnique', 'CommonTask', 'PERM_INVENTORY_ADMIN',
|
'CommonModelNameNotUnique', 'CommonTask', 'PERM_INVENTORY_ADMIN',
|
||||||
'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE',
|
'PERM_INVENTORY_READ', 'PERM_INVENTORY_WRITE',
|
||||||
'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES',
|
'PERM_INVENTORY_DEPLOY', 'PERM_INVENTORY_CHECK', 'JOB_TYPE_CHOICES',
|
||||||
@@ -97,7 +98,56 @@ class VarsDictProperty(object):
|
|||||||
raise AttributeError('readonly property')
|
raise AttributeError('readonly property')
|
||||||
|
|
||||||
|
|
||||||
class PrimordialModel(models.Model):
|
class BaseModel(models.Model):
|
||||||
|
'''
|
||||||
|
Base model class with common methods for all models.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
if hasattr(self, 'name'):
|
||||||
|
return u'%s-%s' % (self.name, self.id)
|
||||||
|
else:
|
||||||
|
return u'%s-%s' % (self._meta.verbose_name, self.id)
|
||||||
|
|
||||||
|
def clean_fields(self, exclude=None):
|
||||||
|
'''
|
||||||
|
Override default clean_fields to support methods for cleaning
|
||||||
|
individual model fields.
|
||||||
|
'''
|
||||||
|
exclude = exclude or []
|
||||||
|
errors = {}
|
||||||
|
try:
|
||||||
|
super(BaseModel, self).clean_fields(exclude)
|
||||||
|
except ValidationError, e:
|
||||||
|
errors = e.update_error_dict(errors)
|
||||||
|
for f in self._meta.fields:
|
||||||
|
if f.name in exclude:
|
||||||
|
continue
|
||||||
|
if hasattr(self, 'clean_%s' % f.name):
|
||||||
|
try:
|
||||||
|
setattr(self, f.attname,
|
||||||
|
getattr(self, 'clean_%s' % f.name)())
|
||||||
|
except ValidationError, e:
|
||||||
|
errors[f.name] = e.messages
|
||||||
|
if errors:
|
||||||
|
raise ValidationError(errors)
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# For compatibility with Django 1.4.x, attempt to handle any calls to
|
||||||
|
# save that pass update_fields.
|
||||||
|
try:
|
||||||
|
super(BaseModel, self).save(*args, **kwargs)
|
||||||
|
except TypeError:
|
||||||
|
if 'update_fields' not in kwargs:
|
||||||
|
raise
|
||||||
|
kwargs.pop('update_fields')
|
||||||
|
super(BaseModel, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class PrimordialModel(BaseModel):
|
||||||
'''
|
'''
|
||||||
common model for all object types that have these standard fields
|
common model for all object types that have these standard fields
|
||||||
must use a subclass CommonModel or CommonModelNameNotUnique though
|
must use a subclass CommonModel or CommonModelNameNotUnique though
|
||||||
@@ -136,27 +186,11 @@ class PrimordialModel(models.Model):
|
|||||||
)
|
)
|
||||||
active = models.BooleanField(
|
active = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
|
editable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
tags = TaggableManager(blank=True)
|
tags = TaggableManager(blank=True)
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
if hasattr(self, 'name'):
|
|
||||||
return u'%s-%s' % (self.name, self.id)
|
|
||||||
else:
|
|
||||||
return u'%s-%s' % (self._meta.verbose_name, self.id)
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
# For compatibility with Django 1.4.x, attempt to handle any calls to
|
|
||||||
# save that pass update_fields.
|
|
||||||
try:
|
|
||||||
super(PrimordialModel, self).save(*args, **kwargs)
|
|
||||||
except TypeError:
|
|
||||||
if 'update_fields' not in kwargs:
|
|
||||||
raise
|
|
||||||
kwargs.pop('update_fields')
|
|
||||||
super(PrimordialModel, self).save(*args, **kwargs)
|
|
||||||
|
|
||||||
def mark_inactive(self, save=True):
|
def mark_inactive(self, save=True):
|
||||||
'''Use instead of delete to rename and mark inactive.'''
|
'''Use instead of delete to rename and mark inactive.'''
|
||||||
if self.active:
|
if self.active:
|
||||||
@@ -166,6 +200,10 @@ class PrimordialModel(models.Model):
|
|||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def clean_description(self):
|
||||||
|
# Description should always be empty string, never null.
|
||||||
|
return self.description or ''
|
||||||
|
|
||||||
|
|
||||||
class CommonModel(PrimordialModel):
|
class CommonModel(PrimordialModel):
|
||||||
''' a base model where the name is unique '''
|
''' a base model where the name is unique '''
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ class Job(CommonTask):
|
|||||||
return self._get_hosts(job_host_summaries__processed__gt=0)
|
return self._get_hosts(job_host_summaries__processed__gt=0)
|
||||||
|
|
||||||
|
|
||||||
class JobHostSummary(models.Model):
|
class JobHostSummary(BaseModel):
|
||||||
'''
|
'''
|
||||||
Per-host statistics for each job.
|
Per-host statistics for each job.
|
||||||
'''
|
'''
|
||||||
@@ -367,7 +367,7 @@ class JobHostSummary(models.Model):
|
|||||||
self.host.save(update_fields=update_fields)
|
self.host.save(update_fields=update_fields)
|
||||||
self.host.update_computed_fields()
|
self.host.update_computed_fields()
|
||||||
|
|
||||||
class JobEvent(models.Model):
|
class JobEvent(BaseModel):
|
||||||
'''
|
'''
|
||||||
An event/message logged from the callback when running a job.
|
An event/message logged from the callback when running a job.
|
||||||
'''
|
'''
|
||||||
|
|||||||
@@ -254,6 +254,16 @@ class Credential(CommonModelNameNotUnique):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('api:credential_detail', args=(self.pk,))
|
return reverse('api:credential_detail', args=(self.pk,))
|
||||||
|
|
||||||
|
def clean_ssh_key_unlock(self):
|
||||||
|
if self.pk:
|
||||||
|
ssh_key_data = decrypt_field(self, 'ssh_key_data')
|
||||||
|
else:
|
||||||
|
ssh_key_data = self.ssh_key_data
|
||||||
|
if 'ENCRYPTED' in ssh_key_data and not self.ssh_key_unlock:
|
||||||
|
raise ValidationError('SSH key unlock must be set when SSH key '
|
||||||
|
'data is encrypted')
|
||||||
|
return self.ssh_key_unlock
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.user and self.team:
|
if self.user and self.team:
|
||||||
raise ValidationError('Credential cannot be assigned to both a user and team')
|
raise ValidationError('Credential cannot be assigned to both a user and team')
|
||||||
@@ -343,7 +353,7 @@ class Credential(CommonModelNameNotUnique):
|
|||||||
update_fields.append(field)
|
update_fields.append(field)
|
||||||
self.save(update_fields=update_fields)
|
self.save(update_fields=update_fields)
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(BaseModel):
|
||||||
'''
|
'''
|
||||||
Profile model related to User object. Currently stores LDAP DN for users
|
Profile model related to User object. Currently stores LDAP DN for users
|
||||||
loaded from LDAP.
|
loaded from LDAP.
|
||||||
@@ -368,7 +378,7 @@ class Profile(models.Model):
|
|||||||
default='',
|
default='',
|
||||||
)
|
)
|
||||||
|
|
||||||
class AuthToken(models.Model):
|
class AuthToken(BaseModel):
|
||||||
'''
|
'''
|
||||||
Custom authentication tokens per user with expiration and request-specific
|
Custom authentication tokens per user with expiration and request-specific
|
||||||
data.
|
data.
|
||||||
|
|||||||
@@ -509,7 +509,13 @@ class ProjectsTest(BaseTest):
|
|||||||
data = dict(name='zyx', user=self.super_django_user.pk, kind='ssh',
|
data = dict(name='zyx', user=self.super_django_user.pk, kind='ssh',
|
||||||
sudo_username=None)
|
sudo_username=None)
|
||||||
self.post(url, data, expect=400)
|
self.post(url, data, expect=400)
|
||||||
|
|
||||||
|
# Test with encrypted ssh key and no unlock password.
|
||||||
|
with self.current_user(self.super_django_user):
|
||||||
|
data = dict(name='zyx', user=self.super_django_user.pk, kind='ssh',
|
||||||
|
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED)
|
||||||
|
self.post(url, data, expect=400)
|
||||||
|
|
||||||
# FIXME: Check list as other users.
|
# FIXME: Check list as other users.
|
||||||
|
|
||||||
# can edit a credential
|
# can edit a credential
|
||||||
|
|||||||
Reference in New Issue
Block a user