1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 16:51:11 +03:00

Prevent nested encrypted field leak in activity stream.

This commit is contained in:
Aaron Tan 2017-05-19 16:47:06 -04:00
parent 7c2e5df659
commit 7f1f68ee28
6 changed files with 119 additions and 30 deletions

View File

@ -1944,9 +1944,8 @@ class CredentialSerializer(BaseSerializer):
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$'
for k, v in value.get('inputs', {}).items(): if 'inputs' in value:
if force_text(v).startswith('$encrypted$'): value['inputs'] = data.display_inputs()
value['inputs'][k] = '$encrypted$'
return value return value
def get_related(self, obj): def get_related(self, obj):
@ -3203,11 +3202,8 @@ class NotificationTemplateSerializer(BaseSerializer):
def to_representation(self, obj): def to_representation(self, obj):
ret = super(NotificationTemplateSerializer, self).to_representation(obj) ret = super(NotificationTemplateSerializer, self).to_representation(obj)
for field in obj.notification_class.init_parameters: if 'notification_configuration' in ret:
config = obj.notification_configuration ret['notification_configuration'] = obj.display_notification_configuration()
if field in config and force_text(config[field]).startswith('$encrypted$'):
config[field] = '$encrypted$'
ret['notification_configuration'] = config
return ret return ret
def get_related(self, obj): def get_related(self, obj):

View File

@ -15,6 +15,7 @@ from jinja2 import Template
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.utils.encoding import force_text
# AWX # AWX
from awx.api.versioning import reverse from awx.api.versioning import reverse
@ -370,6 +371,13 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
field = 'inputs' field = 'inputs'
super(Credential, self).mark_field_for_save(update_fields, field) super(Credential, self).mark_field_for_save(update_fields, field)
def display_inputs(self):
field_val = self.inputs.copy()
for k, v in field_val.items():
if force_text(v).startswith('$encrypted$'):
field_val[k] = '$encrypted$'
return field_val
class CredentialType(CommonModelNameNotUnique): class CredentialType(CommonModelNameNotUnique):
''' '''

View File

@ -6,7 +6,7 @@ import logging
from django.db import models from django.db import models
from django.core.mail.message import EmailMessage from django.core.mail.message import EmailMessage
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_str from django.utils.encoding import smart_str, force_text
# AWX # AWX
from awx.api.versioning import reverse from awx.api.versioning import reverse
@ -119,6 +119,13 @@ class NotificationTemplate(CommonModelNameNotUnique):
notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients) notification_obj = EmailMessage(subject, backend_obj.format_body(body), sender, recipients)
return backend_obj.send_messages([notification_obj]) return backend_obj.send_messages([notification_obj])
def display_notification_configuration(self):
field_val = self.notification_configuration.copy()
for field in self.notification_class.init_parameters:
if field in field_val and force_text(field_val[field]).startswith('$encrypted$'):
field_val[field] = '$encrypted$'
return field_val
class Notification(CreatedModifiedModel): class Notification(CreatedModifiedModel):
''' '''

View File

@ -292,6 +292,15 @@ def notification_template(organization):
headers={"Test": "Header"})) headers={"Test": "Header"}))
@pytest.fixture
def notification_template_with_encrypt(organization):
return NotificationTemplate.objects.create(name='test-notification_template_with_encrypt',
organization=organization,
notification_type="slack",
notification_configuration=dict(channels=["Foo", "Bar"],
token="token"))
@pytest.fixture @pytest.fixture
def notification(notification_template): def notification(notification_template):
return Notification.objects.create(notification_template=notification_template, return Notification.objects.create(notification_template=notification_template,

View File

@ -0,0 +1,60 @@
import pytest
import copy
import json
from awx.main.utils.common import (
model_instance_diff,
model_to_dict,
)
@pytest.mark.django_db
def test_model_to_dict_user(alice):
username = copy.copy(alice.username)
password = copy.copy(alice.password)
output_dict = model_to_dict(alice)
assert output_dict['username'] == username
assert output_dict['password'] == 'hidden'
assert alice.username == password
assert alice.password == password
@pytest.mark.django_db
def test_model_to_dict_credential(credential):
name = copy.copy(credential.name)
inputs = copy.copy(credential.inputs)
output_dict = model_to_dict(credential)
assert output_dict['name'] == name
assert output_dict['inputs'] == 'hidden'
assert credential.name == name
assert credential.inputs == inputs
@pytest.mark.django_db
def test_model_to_dict_notification_template(notification_template_with_encrypt):
old_configuration = copy.deepcopy(notification_template_with_encrypt.notification_configuration)
output_dict = model_to_dict(notification_template_with_encrypt)
new_configuration = json.loads(output_dict['notification_configuration'])
assert notification_template_with_encrypt.notification_configuration == old_configuration
assert new_configuration['token'] == '$encrypted$'
assert new_configuration['channels'] == old_configuration['channels']
@pytest.mark.django_db
def test_model_instance_diff(alice, bob):
alice_name = copy.copy(alice.username)
alice_pass = copy.copy(alice.password)
bob_name = copy.copy(bob.username)
bob_pass = copy.copy(bob.password)
output_dict = model_instance_diff(alice, bob)
assert alice_name == alice.username
assert alice_pass == alice.password
assert bob_name == bob.username
assert bob_pass == bob.password
assert output_dict['username'][0] == alice_name
assert output_dict['username'][1] == bob_name
assert output_dict['password'] == ('hidden', 'hidden')
assert hasattr(alice, 'is_superuser')
assert hasattr(bob, 'is_superuser')
assert 'is_superuser' not in output_dict

View File

@ -372,6 +372,26 @@ def get_allowed_fields(obj, serializer_mapping):
return allowed_fields return allowed_fields
def _convert_model_field_for_display(obj, field_name, password_fields=None):
# NOTE: Careful modifying the value of field_val, as it could modify
# underlying model object field value also.
field_val = getattr(obj, field_name, None)
if password_fields is None:
password_fields = set(getattr(type(obj), 'PASSWORD_FIELDS', [])) | set(['password'])
if field_name in password_fields:
return u'hidden'
if hasattr(obj, 'display_%s' % field_name):
field_val = getattr(obj, 'display_%s' % field_name)()
if isinstance(field_val, (list, dict)):
try:
field_val = json.dumps(field_val, ensure_ascii=False)
except Exception:
pass
if type(field_val) not in (bool, int, type(None)):
field_val = smart_str(field_val)
return field_val
def model_instance_diff(old, new, serializer_mapping=None): def model_instance_diff(old, new, serializer_mapping=None):
""" """
Calculate the differences between two model instances. One of the instances may be None (i.e., a newly Calculate the differences between two model instances. One of the instances may be None (i.e., a newly
@ -380,13 +400,13 @@ def model_instance_diff(old, new, serializer_mapping=None):
When provided, read-only fields will not be included in the resulting dictionary When provided, read-only fields will not be included in the resulting dictionary
""" """
from django.db.models import Model from django.db.models import Model
from awx.main.models.credential import Credential
PASSWORD_FIELDS = ['password'] + Credential.PASSWORD_FIELDS
if not(old is None or isinstance(old, Model)): if not(old is None or isinstance(old, Model)):
raise TypeError('The supplied old instance is not a valid model instance.') raise TypeError('The supplied old instance is not a valid model instance.')
if not(new is None or isinstance(new, Model)): if not(new is None or isinstance(new, Model)):
raise TypeError('The supplied new instance is not a valid model instance.') raise TypeError('The supplied new instance is not a valid model instance.')
old_password_fields = set(getattr(type(old), 'PASSWORD_FIELDS', [])) | set(['password'])
new_password_fields = set(getattr(type(new), 'PASSWORD_FIELDS', [])) | set(['password'])
diff = {} diff = {}
@ -395,15 +415,11 @@ def model_instance_diff(old, new, serializer_mapping=None):
for field in allowed_fields: for field in allowed_fields:
old_value = getattr(old, field, None) old_value = getattr(old, field, None)
new_value = getattr(new, field, None) new_value = getattr(new, field, None)
if old_value != new_value:
if old_value != new_value and field not in PASSWORD_FIELDS: diff[field] = (
if type(old_value) not in (bool, int, type(None)): _convert_model_field_for_display(old, field, password_fields=old_password_fields),
old_value = smart_str(old_value) _convert_model_field_for_display(new, field, password_fields=new_password_fields),
if type(new_value) not in (bool, int, type(None)): )
new_value = smart_str(new_value)
diff[field] = (old_value, new_value)
elif old_value != new_value and field in PASSWORD_FIELDS:
diff[field] = (u"hidden", u"hidden")
if len(diff) == 0: if len(diff) == 0:
diff = None diff = None
@ -417,8 +433,7 @@ def model_to_dict(obj, serializer_mapping=None):
serializer_mapping are used to determine read-only fields. serializer_mapping are used to determine read-only fields.
When provided, read-only fields will not be included in the resulting dictionary When provided, read-only fields will not be included in the resulting dictionary
""" """
from awx.main.models.credential import Credential password_fields = set(getattr(type(obj), 'PASSWORD_FIELDS', [])) | set(['password'])
PASSWORD_FIELDS = ['password'] + Credential.PASSWORD_FIELDS
attr_d = {} attr_d = {}
allowed_fields = get_allowed_fields(obj, serializer_mapping) allowed_fields = get_allowed_fields(obj, serializer_mapping)
@ -426,14 +441,8 @@ def model_to_dict(obj, serializer_mapping=None):
for field in obj._meta.fields: for field in obj._meta.fields:
if field.name not in allowed_fields: if field.name not in allowed_fields:
continue continue
if field.name not in PASSWORD_FIELDS: attr_d[field.name] = _convert_model_field_for_display(obj, field.name, password_fields=password_fields)
field_val = getattr(obj, field.name, None)
if type(field_val) not in (bool, int, type(None)):
attr_d[field.name] = smart_str(field_val)
else:
attr_d[field.name] = field_val
else:
attr_d[field.name] = "hidden"
return attr_d return attr_d