From 676270286854da0460f0f60c197f8ecd41e27347 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 30 Aug 2019 09:07:14 -0400 Subject: [PATCH] cli: add support for granting and revoking roles from users/teams --- awx/api/metadata.py | 12 ++- awxkit/awxkit/cli/custom.py | 135 ++++++++++++++++++++++++++++++++ awxkit/awxkit/cli/options.py | 4 + awxkit/test/cli/test_options.py | 2 +- 4 files changed, 151 insertions(+), 2 deletions(-) diff --git a/awx/api/metadata.py b/awx/api/metadata.py index fa5a4fff1d..dda07502a6 100644 --- a/awx/api/metadata.py +++ b/awx/api/metadata.py @@ -20,7 +20,7 @@ from rest_framework.fields import JSONField as DRFJSONField from rest_framework.request import clone_request # AWX -from awx.main.fields import JSONField +from awx.main.fields import JSONField, ImplicitRoleField from awx.main.models import InventorySource, NotificationTemplate @@ -252,6 +252,16 @@ class Metadata(metadata.SimpleMetadata): if getattr(view, 'related_search_fields', None): metadata['related_search_fields'] = view.related_search_fields + # include role names in metadata + roles = [] + model = getattr(view, 'model', None) + if model: + for field in model._meta.get_fields(): + if type(field) is ImplicitRoleField: + roles.append(field.name) + if len(roles) > 0: + metadata['object_roles'] = roles + from rest_framework import generics if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'): metadata['max_page_size'] = view.paginator.max_page_size diff --git a/awxkit/awxkit/cli/custom.py b/awxkit/awxkit/cli/custom.py index de2031084b..ad2af3b433 100644 --- a/awxkit/awxkit/cli/custom.py +++ b/awxkit/awxkit/cli/custom.py @@ -350,6 +350,141 @@ class SettingsList(CustomAction): return self.page.get() +class RoleMixin(object): + + has_roles = [ + ['organizations', 'organization'], + ['projects', 'project'], + ['inventories', 'inventory'], + ['inventory_scripts', 'inventory_script'], + ['teams', 'team'], + ['credentials', 'credential'], + ['job_templates', 'job_template'], + ['workflow_job_templates', 'workflow_job_template'], + ] + roles = {} # this is calculated once + + def add_arguments(self, parser): + from .options import pk_or_name + + if not RoleMixin.roles: + for resource, flag in self.has_roles: + options = self.page.__class__( + self.page.endpoint.replace(self.resource, resource), + self.page.connection + ).options() + RoleMixin.roles[flag] = [ + role.replace('_role', '') + for role in options.json.get('object_roles', []) + ] + + possible_roles = set() + for v in RoleMixin.roles.values(): + possible_roles.update(v) + + resource_group = parser.choices[self.action].add_mutually_exclusive_group( + required=True + ) + parser.choices[self.action].add_argument( + 'id', + type=functools.partial( + pk_or_name, None, self.resource, page=self.page + ), + help='The ID (or name) of the {} to {} access to/from'.format( + self.resource, self.action + ) + ) + for _type in RoleMixin.roles.keys(): + if _type == 'team' and self.resource == 'team': + # don't add a team to a team + continue + + class related_page(object): + + def __init__(self, connection, resource): + self.conn = connection + if resource == 'inventories': + resource = 'inventory' # d'oh, this is special + self.resource = resource + + def get(self, **kwargs): + v2 = api.Api(connection=self.conn).get().current_version.get() + return getattr(v2, self.resource).get(**kwargs) + + resource_group.add_argument( + '--{}'.format(_type), + type=functools.partial( + pk_or_name, None, _type, + page=related_page( + self.page.connection, + dict((v, k) for k, v in self.has_roles)[_type] + ) + ), + metavar='ID', + help='The ID (or name) of the target {}'.format(_type), + ) + parser.choices[self.action].add_argument( + '--role', type=str, choices=possible_roles, required=True, + help='The name of the role to {}'.format(self.action) + ) + + def perform(self, **kwargs): + for resource, flag in self.has_roles: + if flag in kwargs: + role = kwargs['role'] + if role not in RoleMixin.roles[flag]: + options = ', '.join(RoleMixin.roles[flag]) + raise ValueError( + "invalid choice: '{}' must be one of {}".format( + role, options + ) + ) + value = kwargs[flag] + target = '/api/v2/{}/{}'.format(resource, value) + detail = self.page.__class__( + target, + self.page.connection + ).get() + object_roles = detail['summary_fields']['object_roles'] + actual_role = object_roles[role + '_role'] + params = {'id': actual_role['id']} + if self.action == 'grant': + params['associate'] = True + if self.action == 'revoke': + params['disassociate'] = True + + try: + self.page.get().related.roles.post(params) + except NoContent: + # we expect to enter this block because these endpoints return + # HTTP 204 on success + pass + + +class UserGrant(RoleMixin, CustomAction): + + resource = 'users' + action = 'grant' + + +class UserRevoke(RoleMixin, CustomAction): + + resource = 'users' + action = 'revoke' + + +class TeamGrant(RoleMixin, CustomAction): + + resource = 'teams' + action = 'grant' + + +class TeamRevoke(RoleMixin, CustomAction): + + resource = 'teams' + action = 'revoke' + + class SettingsModify(CustomAction): action = 'modify' resource = 'settings' diff --git a/awxkit/awxkit/cli/options.py b/awxkit/awxkit/cli/options.py index 0c57d44587..d30c86e742 100644 --- a/awxkit/awxkit/cli/options.py +++ b/awxkit/awxkit/cli/options.py @@ -41,6 +41,10 @@ def pk_or_name(v2, model_name, value, page=None): page = getattr(v2, model_name) if page: + if model_name == 'users': + identity = 'username' + elif model_name == 'instances': + model_name = 'hostname' results = page.get(**{identity: value}) if results.count == 1: return int(results.results[0].id) diff --git a/awxkit/test/cli/test_options.py b/awxkit/test/cli/test_options.py index 109fc646c3..ce65e94be3 100644 --- a/awxkit/test/cli/test_options.py +++ b/awxkit/test/cli/test_options.py @@ -181,7 +181,7 @@ class TestOptions(unittest.TestCase): page = OptionsPage.from_json({ 'actions': {'GET': {}, 'POST': {}} }) - ResourceOptionsParser(None, page, 'users', self.parser) + ResourceOptionsParser(None, page, 'jobs', self.parser) assert method in self.parser.choices out = StringIO()