mirror of
https://github.com/ansible/awx.git
synced 2026-02-13 15:05:10 -03:30
Fix awx CLI modify command for users with object-level permissions (#16276)
The awx CLI derives available fields for the `modify` command from the list endpoint's POST action schema. Users with object-level admin permissions (e.g., Project Admin) but no list-level POST permission see no field flags, making modify unusable despite having PUT access on the detail endpoint. Fall back to the detail endpoint's action schema when POST is not available on the list endpoint, and prefer PUT over POST when building modify arguments. Signed-off-by: Seth Foster <fosterbseth@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -251,7 +251,13 @@ class CLI(object):
|
||||
if self.resource != 'settings':
|
||||
for method in ('list', 'modify', 'create'):
|
||||
if method in parser.parser.choices:
|
||||
parser.build_query_arguments(method, 'GET' if method == 'list' else 'POST')
|
||||
if method == 'list':
|
||||
http_method = 'GET'
|
||||
elif method == 'modify' and 'PUT' in parser.options:
|
||||
http_method = 'PUT'
|
||||
else:
|
||||
http_method = 'POST'
|
||||
parser.build_query_arguments(method, http_method)
|
||||
if from_sphinx:
|
||||
parsed, extra = self.parser.parse_known_args(self.argv)
|
||||
else:
|
||||
|
||||
@@ -102,6 +102,18 @@ class ResourceOptionsParser(object):
|
||||
if '299' in warning and 'deprecated' in warning:
|
||||
self.deprecated = True
|
||||
self.allowed_options = options.headers.get('Allow', '').split(', ')
|
||||
# If the user can PUT on the detail endpoint but doesn't have
|
||||
# POST on the list endpoint, use the detail endpoint's
|
||||
# action schema so that 'modify' fields are populated.
|
||||
if 'POST' not in self.options and 'PUT' in self.allowed_options:
|
||||
try:
|
||||
detail_actions = options.json().get('actions', {})
|
||||
except Exception:
|
||||
detail_actions = {}
|
||||
if 'PUT' in detail_actions:
|
||||
self.options['PUT'] = detail_actions['PUT']
|
||||
elif 'GET' in detail_actions:
|
||||
self.options['PUT'] = detail_actions['GET']
|
||||
|
||||
def build_list_actions(self):
|
||||
action_map = {
|
||||
@@ -109,6 +121,10 @@ class ResourceOptionsParser(object):
|
||||
'POST': 'create',
|
||||
}
|
||||
for method, action in self.options.items():
|
||||
# Skip 'PUT', which may be added by get_allowed_options
|
||||
# and is handled separately by build_detail_actions
|
||||
if method not in action_map:
|
||||
continue
|
||||
method = action_map[method]
|
||||
parser = self.parser.add_parser(method, help='')
|
||||
if method == 'list':
|
||||
|
||||
@@ -11,6 +11,24 @@ class ResourceOptionsParser(ResourceOptionsParser):
|
||||
self.allowed_options = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
|
||||
|
||||
class NoPostResourceOptionsParser(ResourceOptionsParser):
|
||||
"""Simulates a user with object-level PUT but no list-level POST."""
|
||||
|
||||
detail_put_actions = {}
|
||||
|
||||
def get_allowed_options(self):
|
||||
self.allowed_options = ['GET', 'PUT', 'PATCH', 'DELETE']
|
||||
# Simulate the logic from the real get_allowed_options that
|
||||
# falls back to the detail endpoint's PUT schema when POST
|
||||
# is not available on the list endpoint.
|
||||
if 'POST' not in self.options and 'PUT' in self.allowed_options:
|
||||
if self.detail_put_actions:
|
||||
self.options['PUT'] = self.detail_put_actions
|
||||
|
||||
def handle_custom_actions(self):
|
||||
pass
|
||||
|
||||
|
||||
class OptionsPage(Page):
|
||||
def options(self):
|
||||
return self
|
||||
@@ -185,6 +203,30 @@ class TestOptions(unittest.TestCase):
|
||||
self.parser.choices[method].print_help(out)
|
||||
assert 'positional arguments:\n id' in out.getvalue()
|
||||
|
||||
def test_modify_without_list_post(self):
|
||||
"""User with object-level PUT but no list-level POST can still modify."""
|
||||
page = OptionsPage.from_json(
|
||||
{
|
||||
'actions': {
|
||||
'GET': {},
|
||||
}
|
||||
}
|
||||
)
|
||||
NoPostResourceOptionsParser.detail_put_actions = {
|
||||
'scm_branch': {'type': 'string', 'help_text': 'SCM branch'},
|
||||
'description': {'type': 'string', 'help_text': 'Description'},
|
||||
}
|
||||
options = NoPostResourceOptionsParser(None, page, 'projects', self.parser)
|
||||
|
||||
assert 'modify' in self.parser.choices
|
||||
assert 'create' not in self.parser.choices
|
||||
|
||||
options.build_query_arguments('modify', 'PUT')
|
||||
out = StringIO()
|
||||
self.parser.choices['modify'].print_help(out)
|
||||
assert '--scm_branch TEXT' in out.getvalue()
|
||||
assert '--description TEXT' in out.getvalue()
|
||||
|
||||
|
||||
class TestSettingsOptions(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user