blacklist certain sensitive fields and relations as search arguments

see: #5465
see: #5478
This commit is contained in:
Ryan Petrello
2017-02-21 12:18:40 -05:00
parent 0a5b43acae
commit d24fb32358
13 changed files with 99 additions and 32 deletions

View File

@@ -42,7 +42,7 @@ _PythonSerializer.handle_m2m_field = _new_handle_m2m_field
# Add custom methods to User model for permissions checks.
from django.contrib.auth.models import User # noqa
from django.contrib.auth.models import User # noqa
from awx.main.access import * # noqa
@@ -128,3 +128,6 @@ activity_stream_registrar.connect(User)
activity_stream_registrar.connect(WorkflowJobTemplate)
activity_stream_registrar.connect(WorkflowJobTemplateNode)
activity_stream_registrar.connect(WorkflowJob)
# prevent API filtering on certain Django-supplied sensitive fields
prevent_search(User._meta.get_field('password'))

View File

@@ -83,10 +83,10 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin):
editable=False,
through='AdHocCommandEvent',
)
extra_vars = models.TextField(
extra_vars = prevent_search(models.TextField(
blank=True,
default='',
)
))
extra_vars_dict = VarsDictProperty('extra_vars', True)

View File

@@ -23,7 +23,7 @@ from crum import get_current_user
# Ansible Tower
from awx.main.utils import encrypt_field
__all__ = ['VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
__all__ = ['prevent_search', 'VarsDictProperty', 'BaseModel', 'CreatedModifiedModel',
'PasswordFieldsModel', 'PrimordialModel', 'CommonModel',
'CommonModelNameNotUnique', 'NotificationFieldsModel',
'PERM_INVENTORY_ADMIN', 'PERM_INVENTORY_READ',
@@ -343,3 +343,21 @@ class NotificationFieldsModel(BaseModel):
blank=True,
related_name='%(class)s_notification_templates_for_any'
)
def prevent_search(relation):
"""
Used to mark a model field or relation as "restricted from filtering"
e.g.,
class AuthToken(BaseModel):
user = prevent_search(models.ForeignKey(...))
sensitive_data = prevent_search(models.CharField(...))
The flag set by this function is used by
`awx.api.filters.FieldLookupBackend` to blacklist fields and relations that
should not be searchable/filterable via search query params
"""
setattr(relation, '__prevent_search__', True)
return relation

View File

@@ -1284,11 +1284,11 @@ class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
unique_together = [('name', 'organization')]
ordering = ('name',)
script = models.TextField(
script = prevent_search(models.TextField(
blank=True,
default='',
help_text=_('Inventory script contents'),
)
))
organization = models.ForeignKey(
'Organization',
related_name='custom_inventory_scripts',

View File

@@ -117,10 +117,10 @@ class JobOptions(BaseModel):
blank=True,
default=0,
)
extra_vars = models.TextField(
extra_vars = prevent_search(models.TextField(
blank=True,
default='',
)
))
job_tags = models.CharField(
max_length=1024,
blank=True,
@@ -1252,10 +1252,10 @@ class SystemJob(UnifiedJob, SystemJobOptions, JobNotificationMixin):
on_delete=models.SET_NULL,
)
extra_vars = models.TextField(
extra_vars = prevent_search(models.TextField(
blank=True,
default='',
)
))
extra_vars_dict = VarsDictProperty('extra_vars', True)

View File

@@ -7,6 +7,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User # noqa
# AWX
from awx.main.models.base import prevent_search
from awx.main.models.rbac import (
Role, RoleAncestorEntry, get_roles_on_resource
)
@@ -86,10 +87,10 @@ class SurveyJobTemplateMixin(models.Model):
survey_enabled = models.BooleanField(
default=False,
)
survey_spec = JSONField(
survey_spec = prevent_search(JSONField(
blank=True,
default={},
)
))
def survey_password_variables(self):
vars = []
@@ -215,11 +216,11 @@ class SurveyJobMixin(models.Model):
class Meta:
abstract = True
survey_passwords = JSONField(
survey_passwords = prevent_search(JSONField(
blank=True,
default={},
editable=False,
)
))
def display_extra_vars(self):
'''

View File

@@ -220,12 +220,13 @@ class AuthToken(BaseModel):
app_label = 'main'
key = models.CharField(max_length=40, primary_key=True)
user = models.ForeignKey('auth.User', related_name='auth_tokens',
on_delete=models.CASCADE)
user = prevent_search(models.ForeignKey('auth.User',
related_name='auth_tokens', on_delete=models.CASCADE))
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
expires = models.DateTimeField(default=tz_now)
request_hash = models.CharField(max_length=40, blank=True, default='')
request_hash = prevent_search(models.CharField(max_length=40, blank=True,
default=''))
reason = models.CharField(
max_length=1024,
blank=True,

View File

@@ -503,33 +503,33 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
editable=False,
help_text=_("Elapsed time in seconds that the job ran."),
)
job_args = models.TextField(
job_args = prevent_search(models.TextField(
blank=True,
default='',
editable=False,
)
))
job_cwd = models.CharField(
max_length=1024,
blank=True,
default='',
editable=False,
)
job_env = JSONField(
job_env = prevent_search(JSONField(
blank=True,
default={},
editable=False,
)
))
job_explanation = models.TextField(
blank=True,
default='',
editable=False,
help_text=_("A status field to indicate the state of the job if it wasn't able to run and capture stdout"),
)
start_args = models.TextField(
start_args = prevent_search(models.TextField(
blank=True,
default='',
editable=False,
)
))
result_stdout_text = models.TextField(
blank=True,
default='',

View File

@@ -11,7 +11,7 @@ from django.core.urlresolvers import reverse
#from django import settings as tower_settings
# AWX
from awx.main.models import UnifiedJobTemplate, UnifiedJob
from awx.main.models import prevent_search, UnifiedJobTemplate, UnifiedJob
from awx.main.models.notifications import (
NotificationTemplate,
JobNotificationMixin
@@ -280,10 +280,10 @@ class WorkflowJobOptions(BaseModel):
class Meta:
abstract = True
extra_vars = models.TextField(
extra_vars = prevent_search(models.TextField(
blank=True,
default='',
)
))
extra_vars_dict = VarsDictProperty('extra_vars', True)

View File

@@ -2,7 +2,11 @@ import pytest
from rest_framework.exceptions import PermissionDenied
from awx.api.filters import FieldLookupBackend
from awx.main.models import Credential, JobTemplate
from awx.main.models import (AdHocCommand, AuthToken, CustomInventoryScript,
Credential, Job, JobTemplate, SystemJob,
UnifiedJob, User, WorkflowJob,
WorkflowJobTemplate, WorkflowJobOptions)
from awx.main.models.jobs import JobOptions
@pytest.mark.parametrize(u"empty_value", [u'', ''])
@@ -38,3 +42,28 @@ def test_filter_on_related_password_field(password_field, lookup_suffix):
with pytest.raises(PermissionDenied) as excinfo:
field, new_lookup = field_lookup.get_field_from_lookup(JobTemplate, lookup)
assert 'not allowed' in str(excinfo.value)
@pytest.mark.parametrize('model, query', [
(AuthToken, 'request_hash__icontains'),
(User, 'password__icontains'),
(User, 'auth_tokens__key__icontains'),
(User, 'settings__value__icontains'),
(UnifiedJob, 'job_args__icontains'),
(UnifiedJob, 'job_env__icontains'),
(UnifiedJob, 'start_args__icontains'),
(AdHocCommand, 'extra_vars__icontains'),
(JobOptions, 'extra_vars__icontains'),
(SystemJob, 'extra_vars__icontains'),
(WorkflowJobOptions, 'extra_vars__icontains'),
(Job, 'survey_passwords__icontains'),
(WorkflowJob, 'survey_passwords__icontains'),
(JobTemplate, 'survey_spec__icontains'),
(WorkflowJobTemplate, 'survey_spec__icontains'),
(CustomInventoryScript, 'script__icontains')
])
def test_filter_sensitive_fields_and_relations(model, query):
field_lookup = FieldLookupBackend()
with pytest.raises(PermissionDenied) as excinfo:
field, new_lookup = field_lookup.get_field_from_lookup(model, query)
assert 'not allowed' in str(excinfo.value)