mirror of
https://github.com/ansible/awx.git
synced 2026-05-19 23:07:42 -02:30
cli: add support for granting and revoking roles from users/teams
This commit is contained in:
@@ -20,7 +20,7 @@ from rest_framework.fields import JSONField as DRFJSONField
|
|||||||
from rest_framework.request import clone_request
|
from rest_framework.request import clone_request
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.fields import JSONField
|
from awx.main.fields import JSONField, ImplicitRoleField
|
||||||
from awx.main.models import InventorySource, NotificationTemplate
|
from awx.main.models import InventorySource, NotificationTemplate
|
||||||
|
|
||||||
|
|
||||||
@@ -252,6 +252,16 @@ class Metadata(metadata.SimpleMetadata):
|
|||||||
if getattr(view, 'related_search_fields', None):
|
if getattr(view, 'related_search_fields', None):
|
||||||
metadata['related_search_fields'] = view.related_search_fields
|
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
|
from rest_framework import generics
|
||||||
if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'):
|
if isinstance(view, generics.ListAPIView) and hasattr(view, 'paginator'):
|
||||||
metadata['max_page_size'] = view.paginator.max_page_size
|
metadata['max_page_size'] = view.paginator.max_page_size
|
||||||
|
|||||||
@@ -350,6 +350,141 @@ class SettingsList(CustomAction):
|
|||||||
return self.page.get()
|
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):
|
class SettingsModify(CustomAction):
|
||||||
action = 'modify'
|
action = 'modify'
|
||||||
resource = 'settings'
|
resource = 'settings'
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ def pk_or_name(v2, model_name, value, page=None):
|
|||||||
page = getattr(v2, model_name)
|
page = getattr(v2, model_name)
|
||||||
|
|
||||||
if page:
|
if page:
|
||||||
|
if model_name == 'users':
|
||||||
|
identity = 'username'
|
||||||
|
elif model_name == 'instances':
|
||||||
|
model_name = 'hostname'
|
||||||
results = page.get(**{identity: value})
|
results = page.get(**{identity: value})
|
||||||
if results.count == 1:
|
if results.count == 1:
|
||||||
return int(results.results[0].id)
|
return int(results.results[0].id)
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ class TestOptions(unittest.TestCase):
|
|||||||
page = OptionsPage.from_json({
|
page = OptionsPage.from_json({
|
||||||
'actions': {'GET': {}, 'POST': {}}
|
'actions': {'GET': {}, 'POST': {}}
|
||||||
})
|
})
|
||||||
ResourceOptionsParser(None, page, 'users', self.parser)
|
ResourceOptionsParser(None, page, 'jobs', self.parser)
|
||||||
assert method in self.parser.choices
|
assert method in self.parser.choices
|
||||||
|
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
|
|||||||
Reference in New Issue
Block a user