mirror of
https://github.com/ansible/awx.git
synced 2026-10-31 06:26:15 -02:30
AAP-66689 - Fix JT admins unable to toggle ask_*_on_launch fields without project_inventory access (#16623)
Fix JT admins unable to toggle ask_*_on_launch fields without project/inventory access
This commit is contained in:
+16
-14
@@ -24,6 +24,7 @@ from ansible_base.rbac.policies import visible_users
|
|||||||
from ansible_base.rbac import permission_registry
|
from ansible_base.rbac import permission_registry
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
|
from awx.main.fields import AskForField
|
||||||
from awx.main.utils import (
|
from awx.main.utils import (
|
||||||
get_object_or_400,
|
get_object_or_400,
|
||||||
get_pk_from_dict,
|
get_pk_from_dict,
|
||||||
@@ -1592,12 +1593,6 @@ class JobTemplateAccess(NotificationAttachMixin, UnifiedCredentialsMixin, BaseAc
|
|||||||
'job_tags',
|
'job_tags',
|
||||||
'force_handlers',
|
'force_handlers',
|
||||||
'skip_tags',
|
'skip_tags',
|
||||||
'ask_variables_on_launch',
|
|
||||||
'ask_tags_on_launch',
|
|
||||||
'ask_job_type_on_launch',
|
|
||||||
'ask_skip_tags_on_launch',
|
|
||||||
'ask_inventory_on_launch',
|
|
||||||
'ask_credential_on_launch',
|
|
||||||
'survey_enabled',
|
'survey_enabled',
|
||||||
'custom_virtualenv',
|
'custom_virtualenv',
|
||||||
'diff_mode',
|
'diff_mode',
|
||||||
@@ -1608,19 +1603,26 @@ class JobTemplateAccess(NotificationAttachMixin, UnifiedCredentialsMixin, BaseAc
|
|||||||
'created',
|
'created',
|
||||||
'modified',
|
'modified',
|
||||||
]
|
]
|
||||||
|
# ask_*_on_launch fields only toggle whether a field can be prompted for at launch
|
||||||
|
# time; they don't grant access to a new resource, so they are always non-sensitive
|
||||||
|
# regardless of which model defines them
|
||||||
|
ask_field_names = {f.name for f in obj._meta.get_fields() if isinstance(f, AskForField)}
|
||||||
|
|
||||||
for k, v in data.items():
|
for field_name, new_value in data.items():
|
||||||
if k not in [x.name for x in obj._meta.concrete_fields]:
|
if field_name not in [x.name for x in obj._meta.concrete_fields]:
|
||||||
continue
|
continue
|
||||||
if hasattr(obj, k) and getattr(obj, k) != v:
|
if hasattr(obj, field_name) and getattr(obj, field_name) != new_value:
|
||||||
if (
|
if field_name not in allowed_fields and field_name not in ask_field_names and not self._fk_value_unchanged(obj, field_name, new_value):
|
||||||
k not in allowed_fields
|
|
||||||
and v != getattr(obj, '%s_id' % k, None)
|
|
||||||
and not (hasattr(obj, '%s_id' % k) and getattr(obj, '%s_id' % k) is None and v == '')
|
|
||||||
): # Equate '' to None in the case of foreign keys
|
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _fk_value_unchanged(self, obj, field_name, new_value):
|
||||||
|
"""True if new_value is just a different representation of the current FK value (id, or '' equated to None)."""
|
||||||
|
fk_id_attr = '%s_id' % field_name
|
||||||
|
if new_value == getattr(obj, fk_id_attr, None):
|
||||||
|
return True
|
||||||
|
return hasattr(obj, fk_id_attr) and getattr(obj, fk_id_attr) is None and new_value == ''
|
||||||
|
|
||||||
def can_delete(self, obj):
|
def can_delete(self, obj):
|
||||||
return self.user.is_superuser or self.user in obj.admin_role
|
return self.user.is_superuser or self.user in obj.admin_role
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ from django.forms.models import model_to_dict
|
|||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
from awx.main.access import BaseAccess, check_superuser, JobTemplateAccess, WorkflowJobTemplateAccess, vars_are_encrypted
|
from awx.main.access import BaseAccess, check_superuser, JobTemplateAccess, WorkflowJobTemplateAccess, vars_are_encrypted
|
||||||
|
from awx.main.fields import AskForField
|
||||||
|
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
Credential,
|
Credential,
|
||||||
CredentialType,
|
CredentialType,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
JobTemplate,
|
||||||
Project,
|
Project,
|
||||||
Role,
|
Role,
|
||||||
Organization,
|
Organization,
|
||||||
@@ -150,6 +152,31 @@ def test_not_superuser(mocker):
|
|||||||
assert can_add(access, None) is False
|
assert can_add(access, None) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_jt_clearing_unset_fk_with_empty_string_is_nonsensitive(job_template_with_ids, user_unit):
|
||||||
|
"""Posting '' for an FK field that is already None (e.g. execution_environment)
|
||||||
|
should be treated as a no-op, not a sensitive change, since the UI sometimes
|
||||||
|
sends '' instead of null.
|
||||||
|
"""
|
||||||
|
access = JobTemplateAccess(user_unit)
|
||||||
|
assert job_template_with_ids.execution_environment_id is None
|
||||||
|
data = {'execution_environment': ''}
|
||||||
|
assert access.changes_are_non_sensitive(job_template_with_ids, data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'field_name, new_value, expected',
|
||||||
|
[
|
||||||
|
('inventory', 11, True), # matches current inventory_id
|
||||||
|
('inventory', 12, False), # different id
|
||||||
|
('execution_environment', '', True), # '' equated to None
|
||||||
|
('execution_environment', 5, False), # not None, and not equal
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_fk_value_unchanged(job_template_with_ids, user_unit, field_name, new_value, expected):
|
||||||
|
access = JobTemplateAccess(user_unit)
|
||||||
|
assert access._fk_value_unchanged(job_template_with_ids, field_name, new_value) is expected
|
||||||
|
|
||||||
|
|
||||||
def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit):
|
def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit):
|
||||||
"""Assure that permission checks are not required if submitted data is
|
"""Assure that permission checks are not required if submitted data is
|
||||||
identical to what the job template already has."""
|
identical to what the job template already has."""
|
||||||
@@ -160,6 +187,19 @@ def test_jt_existing_values_are_nonsensitive(job_template_with_ids, user_unit):
|
|||||||
assert access.changes_are_non_sensitive(job_template_with_ids, data)
|
assert access.changes_are_non_sensitive(job_template_with_ids, data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('field_name', [f.name for f in JobTemplate._meta.get_fields() if isinstance(f, AskForField)])
|
||||||
|
def test_jt_ask_fields_are_nonsensitive(job_template_with_ids, user_unit, field_name):
|
||||||
|
"""Toggling any ask_*_on_launch field should be considered a non-sensitive
|
||||||
|
change so that JT admins without project/inventory use_role can still
|
||||||
|
modify these boolean prompt fields.
|
||||||
|
"""
|
||||||
|
access = JobTemplateAccess(user_unit)
|
||||||
|
# The field defaults to False on the template; flipping it to True should
|
||||||
|
# still be treated as non-sensitive.
|
||||||
|
data = {field_name: True}
|
||||||
|
assert access.changes_are_non_sensitive(job_template_with_ids, data)
|
||||||
|
|
||||||
|
|
||||||
def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
|
def test_change_jt_sensitive_data(job_template_with_ids, mocker, user_unit):
|
||||||
"""Assure that can_add is called with all ForeignKeys."""
|
"""Assure that can_add is called with all ForeignKeys."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user