Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make controller specific team and org roles #15445

Merged
merged 15 commits into from
Aug 22, 2024
33 changes: 32 additions & 1 deletion awx/main/migrations/_dab_rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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_')]
Expand Down
21 changes: 20 additions & 1 deletion awx/main/models/rbac.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down
68 changes: 67 additions & 1 deletion awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
33 changes: 32 additions & 1 deletion awx/main/tests/functional/dab_rbac/test_managed_roles.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
21 changes: 21 additions & 0 deletions awx/main/tests/functional/dab_rbac/test_translation_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions awx/main/tests/functional/test_migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,18 +73,25 @@ def test_receptor_address(self, migrator):
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'),
)

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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 alright, looks good here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did that not require any changes to the data migration? I guess maybe it imported some of the translation methods from the models? I just don't see how this would have gotten fixed.


# Regression testing for bug that comes from current vs past models mismatch
RoleDefinition = new_state.apps.get_model('dab_rbac', 'RoleDefinition')
Expand Down
3 changes: 3 additions & 0 deletions awx/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,9 @@
# 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"
Expand Down
2 changes: 1 addition & 1 deletion requirements/requirements_git.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected].19#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac]
django-ansible-base @ git+https://github.com/ansible/[email protected].22#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac]
Loading