mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
Make controller specific team and org roles (#15445)
Adds the following managed Role Definitions Controller Team Admin Controller Team Member Controller Organization Admin Controller Organization Member These have the same permission set as the platform roles (without the Controller prefix) Adding members to teams and orgs via the legacy RBAC system will use these role definitions. Other changes: - Bump DAB to 2024.08.22 - Set ALLOW_LOCAL_ASSIGNING_JWT_ROLES to False in defaults.py. This setting prevents assignments to the platform roles (e.g. Team Member). Signed-off-by: Seth Foster <fosterbseth@gmail.com>
This commit is contained in:
parent
78f345c486
commit
7ed0eee60c
@ -167,7 +167,7 @@ def migrate_to_new_rbac(apps, schema_editor):
|
||||
perm.delete()
|
||||
|
||||
managed_definitions = dict()
|
||||
for role_definition in RoleDefinition.objects.filter(managed=True):
|
||||
for role_definition in RoleDefinition.objects.filter(managed=True).exclude(name__in=(settings.ANSIBLE_BASE_JWT_MANAGED_ROLES)):
|
||||
permissions = frozenset(role_definition.permissions.values_list('id', flat=True))
|
||||
managed_definitions[permissions] = role_definition
|
||||
|
||||
@ -309,6 +309,16 @@ def setup_managed_role_definitions(apps, schema_editor):
|
||||
to_create['object_admin'].format(cls=cls), f'Has all permissions to a single {cls._meta.verbose_name}', ct, indiv_perms, RoleDefinition
|
||||
)
|
||||
)
|
||||
if cls_name == 'team':
|
||||
managed_role_definitions.append(
|
||||
get_or_create_managed(
|
||||
'Controller Team Admin',
|
||||
f'Has all permissions to a single {cls._meta.verbose_name}',
|
||||
ct,
|
||||
indiv_perms,
|
||||
RoleDefinition,
|
||||
)
|
||||
)
|
||||
|
||||
if 'org_children' in to_create and (cls_name not in ('organization', 'instancegroup', 'team')):
|
||||
org_child_perms = object_perms.copy()
|
||||
@ -349,6 +359,18 @@ def setup_managed_role_definitions(apps, schema_editor):
|
||||
RoleDefinition,
|
||||
)
|
||||
)
|
||||
if action == 'member' and cls_name in ('organization', 'team'):
|
||||
suffix = to_create['special'].format(cls=cls, action=action.title())
|
||||
rd_name = f'Controller {suffix}'
|
||||
managed_role_definitions.append(
|
||||
get_or_create_managed(
|
||||
rd_name,
|
||||
f'Has {action} permissions to a single {cls._meta.verbose_name}',
|
||||
ct,
|
||||
perm_list,
|
||||
RoleDefinition,
|
||||
)
|
||||
)
|
||||
|
||||
if 'org_admin' in to_create:
|
||||
managed_role_definitions.append(
|
||||
@ -360,6 +382,15 @@ def setup_managed_role_definitions(apps, schema_editor):
|
||||
RoleDefinition,
|
||||
)
|
||||
)
|
||||
managed_role_definitions.append(
|
||||
get_or_create_managed(
|
||||
'Controller Organization Admin',
|
||||
'Has all permissions to a single organization and all objects inside of it',
|
||||
org_ct,
|
||||
org_perms,
|
||||
RoleDefinition,
|
||||
)
|
||||
)
|
||||
|
||||
# Special "organization action" roles
|
||||
audit_permissions = [perm for perm in org_perms if perm.codename.startswith('view_')]
|
||||
|
||||
@ -557,12 +557,25 @@ def get_role_definition(role):
|
||||
f = obj._meta.get_field(role.role_field)
|
||||
action_name = f.name.rsplit("_", 1)[0]
|
||||
model_print = type(obj).__name__
|
||||
rd_name = f'{model_print} {action_name.title()} Compat'
|
||||
perm_list = get_role_codenames(role)
|
||||
defaults = {
|
||||
'content_type_id': role.content_type_id,
|
||||
'description': f'Has {action_name.title()} permission to {model_print} for backwards API compatibility',
|
||||
}
|
||||
# use Controller-specific role definitions for Team/Organization and member/admin
|
||||
# instead of platform role definitions
|
||||
# these should exist in the system already, so just do a lookup by role definition name
|
||||
if model_print in ['Team', 'Organization'] and action_name in ['member', 'admin']:
|
||||
rd_name = f'Controller {model_print} {action_name.title()}'
|
||||
rd = RoleDefinition.objects.filter(name=rd_name).first()
|
||||
if rd:
|
||||
return rd
|
||||
else:
|
||||
return RoleDefinition.objects.create_from_permissions(permissions=perm_list, name=rd_name, managed=True, **defaults)
|
||||
|
||||
else:
|
||||
rd_name = f'{model_print} {action_name.title()} Compat'
|
||||
|
||||
with impersonate(None):
|
||||
try:
|
||||
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults=defaults)
|
||||
@ -585,6 +598,12 @@ def get_role_from_object_role(object_role):
|
||||
model_name, role_name, _ = rd.name.split()
|
||||
role_name = role_name.lower()
|
||||
role_name += '_role'
|
||||
elif rd.name.startswith('Controller') and rd.name.endswith(' Admin'):
|
||||
# Controller Organization Admin and Controller Team Admin
|
||||
role_name = 'admin_role'
|
||||
elif rd.name.startswith('Controller') and rd.name.endswith(' Member'):
|
||||
# Controller Organization Member and Controller Team Member
|
||||
role_name = 'member_role'
|
||||
elif rd.name.endswith(' Admin') and rd.name.count(' ') == 2:
|
||||
# cases like "Organization Project Admin"
|
||||
model_name, target_model_name, role_name = rd.name.split()
|
||||
|
||||
@ -148,9 +148,75 @@ def test_assign_credential_to_user_of_another_org(setup_managed_roles, credentia
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@override_settings(ALLOW_LOCAL_RESOURCE_MANAGEMENT=False)
|
||||
@override_settings(ALLOW_LOCAL_ASSIGNING_JWT_ROLES=False)
|
||||
def test_team_member_role_not_assignable(team, rando, post, admin_user, setup_managed_roles):
|
||||
member_rd = RoleDefinition.objects.get(name='Organization Member')
|
||||
url = django_reverse('roleuserassignment-list')
|
||||
r = post(url, data={'object_id': team.id, 'role_definition': member_rd.id, 'user': rando.id}, user=admin_user, expect=400)
|
||||
assert 'Not managed locally' in str(r.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_adding_user_to_org_member_role(setup_managed_roles, organization, admin, bob, post, get):
|
||||
'''
|
||||
Adding user to organization member role via the legacy RBAC endpoints
|
||||
should give them access to the organization detail
|
||||
'''
|
||||
url_detail = reverse('api:organization_detail', kwargs={'pk': organization.id})
|
||||
get(url_detail, user=bob, expect=403)
|
||||
|
||||
role = organization.member_role
|
||||
url = reverse('api:role_users_list', kwargs={'pk': role.id})
|
||||
post(url, data={'id': bob.id}, user=admin, expect=204)
|
||||
|
||||
get(url_detail, user=bob, expect=200)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('actor', ['user', 'team'])
|
||||
@pytest.mark.parametrize('role_name', ['Organization Admin', 'Organization Member', 'Team Admin', 'Team Member'])
|
||||
def test_prevent_adding_actor_to_platform_roles(setup_managed_roles, role_name, actor, organization, team, admin, bob, post):
|
||||
'''
|
||||
Prevent user or team from being added to platform-level roles
|
||||
'''
|
||||
rd = RoleDefinition.objects.get(name=role_name)
|
||||
endpoint = 'roleuserassignment-list' if actor == 'user' else 'roleteamassignment-list'
|
||||
url = django_reverse(endpoint)
|
||||
object_id = team.id if 'Team' in role_name else organization.id
|
||||
data = {'object_id': object_id, 'role_definition': rd.id}
|
||||
actor_id = bob.id if actor == 'user' else team.id
|
||||
data[actor] = actor_id
|
||||
r = post(url, data=data, user=admin, expect=400)
|
||||
assert 'Not managed locally' in str(r.data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('role_name', ['Controller Team Admin', 'Controller Team Member'])
|
||||
def test_adding_user_to_controller_team_roles(setup_managed_roles, role_name, team, admin, bob, post, get):
|
||||
'''
|
||||
Allow user to be added to Controller Team Admin or Controller Team Member
|
||||
'''
|
||||
url_detail = reverse('api:team_detail', kwargs={'pk': team.id})
|
||||
get(url_detail, user=bob, expect=403)
|
||||
|
||||
rd = RoleDefinition.objects.get(name=role_name)
|
||||
url = django_reverse('roleuserassignment-list')
|
||||
post(url, data={'object_id': team.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201)
|
||||
|
||||
get(url_detail, user=bob, expect=200)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('role_name', ['Controller Organization Admin', 'Controller Organization Member'])
|
||||
def test_adding_user_to_controller_organization_roles(setup_managed_roles, role_name, organization, admin, bob, post, get):
|
||||
'''
|
||||
Allow user to be added to Controller Organization Admin or Controller Organization Member
|
||||
'''
|
||||
url_detail = reverse('api:organization_detail', kwargs={'pk': organization.id})
|
||||
get(url_detail, user=bob, expect=403)
|
||||
|
||||
rd = RoleDefinition.objects.get(name=role_name)
|
||||
url = django_reverse('roleuserassignment-list')
|
||||
post(url, data={'object_id': organization.id, 'role_definition': rd.id, 'user': bob.id}, user=admin, expect=201)
|
||||
|
||||
get(url, user=bob, expect=200)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from ansible_base.rbac.models import RoleDefinition, DABPermission
|
||||
from ansible_base.rbac.models import RoleDefinition, DABPermission, RoleUserAssignment
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -29,3 +29,34 @@ def test_org_child_add_permission(setup_managed_roles):
|
||||
|
||||
# special case for JobTemplate, anyone can create one with use permission to project/inventory
|
||||
assert not DABPermission.objects.filter(codename='add_jobtemplate').exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_controller_specific_roles_have_correct_permissions(setup_managed_roles):
|
||||
'''
|
||||
Controller specific roles should have the same permissions as the platform roles
|
||||
e.g. Controller Team Admin should have same permission set as Team Admin
|
||||
'''
|
||||
for rd_name in ['Controller Team Admin', 'Controller Team Member', 'Controller Organization Member', 'Controller Organization Admin']:
|
||||
rd = RoleDefinition.objects.get(name=rd_name)
|
||||
rd_platform = RoleDefinition.objects.get(name=rd_name.split('Controller ')[1])
|
||||
assert set(rd.permissions.all()) == set(rd_platform.permissions.all())
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('resource_name', ['Team', 'Organization'])
|
||||
@pytest.mark.parametrize('action', ['Member', 'Admin'])
|
||||
def test_legacy_RBAC_uses_controller_specific_roles(setup_managed_roles, resource_name, action, team, bob, organization):
|
||||
'''
|
||||
Assignment to legacy RBAC roles should use controller specific role definitions
|
||||
e.g. Controller Team Admin, Controller Team Member, Controller Organization Member, Controller Organization Admin
|
||||
'''
|
||||
resource = team if resource_name == 'Team' else organization
|
||||
if action == 'Member':
|
||||
resource.member_role.members.add(bob)
|
||||
else:
|
||||
resource.admin_role.members.add(bob)
|
||||
rd = RoleDefinition.objects.get(name=f'Controller {resource_name} {action}')
|
||||
rd_platform = RoleDefinition.objects.get(name=f'{resource_name} {action}')
|
||||
assert RoleUserAssignment.objects.filter(role_definition=rd, user=bob, object_id=resource.id).exists()
|
||||
assert not RoleUserAssignment.objects.filter(role_definition=rd_platform, user=bob, object_id=resource.id).exists()
|
||||
|
||||
@ -192,3 +192,24 @@ def test_user_auditor_rel(organization, rando, setup_managed_roles):
|
||||
audit_rd = RoleDefinition.objects.get(name='Organization Audit')
|
||||
audit_rd.give_permission(rando, organization)
|
||||
assert list(rando.auditor_of_organizations) == [organization]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('resource_name', ['Organization', 'Team'])
|
||||
@pytest.mark.parametrize('role_name', ['Member', 'Admin'])
|
||||
def test_mapping_from_controller_role_definitions_to_roles(organization, team, rando, role_name, resource_name, setup_managed_roles):
|
||||
"""
|
||||
ensure mappings for controller roles are correct
|
||||
e.g.
|
||||
Controller Organization Member > organization.member_role
|
||||
Controller Organization Admin > organization.admin_role
|
||||
Controller Team Member > team.member_role
|
||||
Controller Team Admin > team.admin_role
|
||||
"""
|
||||
resource = organization if resource_name == 'Organization' else team
|
||||
old_role_name = f"{role_name.lower()}_role"
|
||||
getattr(resource, old_role_name).members.add(rando)
|
||||
assignment = RoleUserAssignment.objects.get(user=rando)
|
||||
assert assignment.role_definition.name == f'Controller {resource_name} {role_name}'
|
||||
old_role = get_role_from_object_role(assignment.object_role)
|
||||
assert old_role.id == getattr(resource, old_role_name).id
|
||||
|
||||
@ -73,11 +73,16 @@ class TestMigrationSmoke:
|
||||
def test_migrate_DAB_RBAC(self, migrator):
|
||||
old_state = migrator.apply_initial_migration(('main', '0190_alter_inventorysource_source_and_more'))
|
||||
Organization = old_state.apps.get_model('main', 'Organization')
|
||||
Team = old_state.apps.get_model('main', 'Team')
|
||||
User = old_state.apps.get_model('auth', 'User')
|
||||
|
||||
org = Organization.objects.create(name='arbitrary-org', created=now(), modified=now())
|
||||
user = User.objects.create(username='random-user')
|
||||
org.read_role.members.add(user)
|
||||
org.member_role.members.add(user)
|
||||
|
||||
team = Team.objects.create(name='arbitrary-team', organization=org, created=now(), modified=now())
|
||||
team.member_role.members.add(user)
|
||||
|
||||
new_state = migrator.apply_tested_migration(
|
||||
('main', '0192_custom_roles'),
|
||||
@ -85,6 +90,8 @@ class TestMigrationSmoke:
|
||||
|
||||
RoleUserAssignment = new_state.apps.get_model('dab_rbac', 'RoleUserAssignment')
|
||||
assert RoleUserAssignment.objects.filter(user=user.id, object_id=org.id).exists()
|
||||
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Organization Member', object_id=org.id).exists()
|
||||
assert RoleUserAssignment.objects.filter(user=user.id, role_definition__name='Controller Team Member', object_id=team.id).exists()
|
||||
|
||||
# Regression testing for bug that comes from current vs past models mismatch
|
||||
RoleDefinition = new_state.apps.get_model('dab_rbac', 'RoleDefinition')
|
||||
|
||||
@ -660,6 +660,9 @@ AWX_AUTO_DEPROVISION_INSTANCES = False
|
||||
# e.g. organizations, teams, and users
|
||||
ALLOW_LOCAL_RESOURCE_MANAGEMENT = True
|
||||
|
||||
# If True, allow users to be assigned to roles that were created via JWT
|
||||
ALLOW_LOCAL_ASSIGNING_JWT_ROLES = False
|
||||
|
||||
# Enable Pendo on the UI, possible values are 'off', 'anonymous', and 'detailed'
|
||||
# Note: This setting may be overridden by database settings.
|
||||
PENDO_TRACKING_STATE = "off"
|
||||
|
||||
@ -2,4 +2,4 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi
|
||||
# Remove pbr from requirements.in when moving ansible-runner to requirements.in
|
||||
git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner
|
||||
git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml
|
||||
django-ansible-base @ git+https://github.com/ansible/django-ansible-base@2024.8.19#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac]
|
||||
django-ansible-base @ git+https://github.com/ansible/django-ansible-base@2024.8.22#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user