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

Replace role system (RBAC) with permissions-based DB roles #14905

Merged
merged 20 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9981750
Replace role system with permissions-based DB roles
AlanCoding Nov 13, 2023
51a526c
Cast ObjectRole object_id to int, very wrong, tmp fix
AlanCoding Feb 21, 2024
ef20666
Cache organization child evaluations and remove hacks
AlanCoding Feb 25, 2024
f8b5c39
Bump number of allowed endpoints (#14956)
AlanCoding Mar 6, 2024
05bd777
[DAB RBAC] Re-implement system auditor as a singleton role in new sys…
AlanCoding Mar 11, 2024
6a22e73
Use AWX base view to make unauth requests 401 (#14981)
AlanCoding Mar 13, 2024
2db2cad
Minor RBAC test fix (#14982)
AlanCoding Mar 13, 2024
fc5280c
Adopt internal DAB RBAC Permission model (#14994)
AlanCoding Mar 14, 2024
c176eef
Bump migration number for RBAC branch
AlanCoding Mar 14, 2024
9638458
[RBAC] Fix migration for created and modified field changes (#14999)
AlanCoding Mar 14, 2024
1fc201a
[RBAC] Fix server error from delete capability of approvals (#15002)
AlanCoding Mar 15, 2024
622fcfa
Generalize can_delete solution, use devel DAB (#15009)
AlanCoding Mar 19, 2024
9abbd8d
[RBAC] Fix known issues with backward compatible access_list (#15052)
AlanCoding Apr 2, 2024
c917b86
AWX Collections for DAB RBAC
fosterseth Apr 2, 2024
0293ef9
Fix missing role membership when giving creator permissions (#15058)
AlanCoding Apr 3, 2024
b5cd7b7
[RBAC] Tweaks to reflect what endpoints are deprecated (#15068)
AlanCoding Apr 5, 2024
770dfef
[RBAC] Fix bug where team could not be given read_role to other team …
AlanCoding Apr 5, 2024
a69eb1f
[RBAC] Rename managed role definitions, and move migration logic here…
AlanCoding Apr 10, 2024
8c2d995
Make custom urls work with RBAC
AlanCoding Apr 10, 2024
0d53381
[RBAC] Update related name to reflect upstream DAB change (#15093)
AlanCoding Apr 11, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions awx/api/generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@
from rest_framework.renderers import StaticHTMLRenderer
from rest_framework.negotiation import DefaultContentNegotiation

# django-ansible-base
from ansible_base.rest_filters.rest_framework.field_lookup_backend import FieldLookupBackend
from ansible_base.lib.utils.models import get_all_field_names
from ansible_base.rbac.models import RoleEvaluation, RoleDefinition
from ansible_base.rbac.permission_registry import permission_registry

# AWX
from awx.main.models import UnifiedJob, UnifiedJobTemplate, User, Role, Credential, WorkflowJobTemplateNode, WorkflowApprovalTemplate
from awx.main.models.rbac import give_creator_permissions
from awx.main.access import optimize_queryset
from awx.main.utils import camelcase_to_underscore, get_search_fields, getattrd, get_object_or_400, decrypt_field, get_awx_version
from awx.main.utils.licensing import server_product_name
Expand Down Expand Up @@ -472,7 +476,11 @@ def skip_related_name(name):

class ListCreateAPIView(ListAPIView, generics.ListCreateAPIView):
# Base class for a list view that allows creating new objects.
pass
def perform_create(self, serializer):
super().perform_create(serializer)
if serializer.Meta.model in permission_registry.all_registered_models:
if self.request and self.request.user:
give_creator_permissions(self.request.user, serializer.instance)
fosterseth marked this conversation as resolved.
Show resolved Hide resolved


class ParentMixin(object):
Expand Down Expand Up @@ -792,13 +800,23 @@ class RetrieveUpdateDestroyAPIView(RetrieveUpdateAPIView, DestroyAPIView):


class ResourceAccessList(ParentMixin, ListAPIView):
deprecated = True
serializer_class = ResourceAccessListElementSerializer
ordering = ('username',)

def get_queryset(self):
obj = self.get_parent_object()

content_type = ContentType.objects.get_for_model(obj)

if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
ancestors = set(RoleEvaluation.objects.filter(content_type_id=content_type.id, object_id=obj.id).values_list('role_id', flat=True))
qs = User.objects.filter(has_roles__in=ancestors) | User.objects.filter(is_superuser=True)
auditor_role = RoleDefinition.objects.filter(name="System Auditor").first()
if auditor_role:
qs |= User.objects.filter(role_assignments__role_definition=auditor_role)
return qs.distinct()

roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))

ancestors = set()
Expand Down Expand Up @@ -958,7 +976,7 @@ def post(self, request, *args, **kwargs):
None, None, self.model, obj, request.user, create_kwargs=create_kwargs, copy_name=serializer.validated_data.get('name', '')
)
if hasattr(new_obj, 'admin_role') and request.user not in new_obj.admin_role.members.all():
new_obj.admin_role.members.add(request.user)
give_creator_permissions(request.user, new_obj)
if sub_objs:
permission_check_func = None
if hasattr(type(self), 'deep_copy_permission_check_func'):
Expand Down
129 changes: 115 additions & 14 deletions awx/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,14 @@
# Django-Polymorphic
from polymorphic.models import PolymorphicModel

# django-ansible-base
from ansible_base.lib.utils.models import get_type_for_model
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
from ansible_base.rbac import permission_registry

# AWX
from awx.main.access import get_user_capabilities
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE
from awx.main.constants import ACTIVE_STATES, CENSOR_VALUE, org_role_to_permission
from awx.main.models import (
ActivityStream,
AdHocCommand,
Expand Down Expand Up @@ -102,7 +105,7 @@
CLOUD_INVENTORY_SOURCES,
)
from awx.main.models.base import VERBOSITY_CHOICES, NEW_JOB_TYPE_CHOICES
from awx.main.models.rbac import role_summary_fields_generator, RoleAncestorEntry
from awx.main.models.rbac import role_summary_fields_generator, give_creator_permissions, get_role_codenames, to_permissions, get_role_from_object_role
from awx.main.fields import ImplicitRoleField
from awx.main.utils import (
get_model_for_type,
Expand Down Expand Up @@ -2763,13 +2766,26 @@ def to_representation(self, user):
team_content_type = ContentType.objects.get_for_model(Team)
content_type = ContentType.objects.get_for_model(obj)

def get_roles_on_resource(parent_role):
"Returns a string list of the roles a parent_role has for current obj."
return list(
RoleAncestorEntry.objects.filter(ancestor=parent_role, content_type_id=content_type.id, object_id=obj.id)
.values_list('role_field', flat=True)
.distinct()
)
reversed_org_map = {}
for k, v in org_role_to_permission.items():
reversed_org_map[v] = k
reversed_role_map = {}
for k, v in to_permissions.items():
reversed_role_map[v] = k

def get_roles_from_perms(perm_list):
"""given a list of permission codenames return a list of role names"""
role_names = set()
for codename in perm_list:
action = codename.split('_', 1)[0]
if action in reversed_role_map:
role_names.add(reversed_role_map[action])
elif codename in reversed_org_map:
if isinstance(obj, Organization):
role_names.add(reversed_org_map[codename])
if 'view_organization' not in role_names:
role_names.add('read_role')
return list(role_names)

def format_role_perm(role):
role_dict = {'id': role.id, 'name': role.name, 'description': role.description}
Expand All @@ -2786,13 +2802,21 @@ def format_role_perm(role):
else:
# Singleton roles should not be managed from this view, as per copy/edit rework spec
role_dict['user_capabilities'] = {'unattach': False}
return {'role': role_dict, 'descendant_roles': get_roles_on_resource(role)}

model_name = content_type.model
if isinstance(obj, Organization):
descendant_perms = [codename for codename in get_role_codenames(role) if codename.endswith(model_name) or codename.startswith('add_')]
else:
descendant_perms = [codename for codename in get_role_codenames(role) if codename.endswith(model_name)]

return {'role': role_dict, 'descendant_roles': get_roles_from_perms(descendant_perms)}

def format_team_role_perm(naive_team_role, permissive_role_ids):
ret = []
team = naive_team_role.content_object
team_role = naive_team_role
if naive_team_role.role_field == 'admin_role':
team_role = naive_team_role.content_object.member_role
team_role = team.member_role
for role in team_role.children.filter(id__in=permissive_role_ids).all():
role_dict = {
'id': role.id,
Expand All @@ -2812,10 +2836,87 @@ def format_team_role_perm(naive_team_role, permissive_role_ids):
else:
# Singleton roles should not be managed from this view, as per copy/edit rework spec
role_dict['user_capabilities'] = {'unattach': False}
ret.append({'role': role_dict, 'descendant_roles': get_roles_on_resource(team_role)})

descendant_perms = list(
RoleEvaluation.objects.filter(role__in=team.has_roles.all(), object_id=obj.id, content_type_id=content_type.id)
.values_list('codename', flat=True)
.distinct()
)

ret.append({'role': role_dict, 'descendant_roles': get_roles_from_perms(descendant_perms)})
return ret

gfk_kwargs = dict(content_type_id=content_type.id, object_id=obj.id)
direct_permissive_role_ids = Role.objects.filter(**gfk_kwargs).values_list('id', flat=True)

if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
ret['summary_fields']['direct_access'] = []
ret['summary_fields']['indirect_access'] = []

new_roles_seen = set()
all_team_roles = set()
all_permissive_role_ids = set()
for evaluation in RoleEvaluation.objects.filter(role__in=user.has_roles.all(), **gfk_kwargs).prefetch_related('role'):
new_role = evaluation.role
if new_role.id in new_roles_seen:
continue
new_roles_seen.add(new_role.id)
old_role = get_role_from_object_role(new_role)
all_permissive_role_ids.add(old_role.id)

if int(new_role.object_id) == obj.id and new_role.content_type_id == content_type.id:
ret['summary_fields']['direct_access'].append(format_role_perm(old_role))
elif new_role.content_type_id == team_content_type.id:
all_team_roles.add(old_role)
else:
ret['summary_fields']['indirect_access'].append(format_role_perm(old_role))

# Lazy role creation gives us a big problem, where some intermediate roles are not easy to find
# like when a team has indirect permission, so here we get all roles the users teams have
# these contribute to all potential permission-granting roles of the object
user_teams_qs = permission_registry.team_model.objects.filter(member_roles__in=ObjectRole.objects.filter(users=user))
team_obj_roles = ObjectRole.objects.filter(teams__in=user_teams_qs)
for evaluation in RoleEvaluation.objects.filter(role__in=team_obj_roles, **gfk_kwargs).prefetch_related('role'):
new_role = evaluation.role
if new_role.id in new_roles_seen:
continue
new_roles_seen.add(new_role.id)
old_role = get_role_from_object_role(new_role)
all_permissive_role_ids.add(old_role.id)

# In DAB RBAC, superuser is strictly a user flag, and global roles are not in the RoleEvaluation table
if user.is_superuser:
ret['summary_fields'].setdefault('indirect_access', [])
all_role_names = [field.name for field in obj._meta.get_fields() if isinstance(field, ImplicitRoleField)]
ret['summary_fields']['indirect_access'].append(
{
"role": {
"id": None,
"name": _("System Administrator"),
"description": _("Can manage all aspects of the system"),
"user_capabilities": {"unattach": False},
},
"descendant_roles": all_role_names,
}
)
elif user.is_system_auditor:
ret['summary_fields'].setdefault('indirect_access', [])
ret['summary_fields']['indirect_access'].append(
{
"role": {
"id": None,
"name": _("System Auditor"),
"description": _("Can view all aspects of the system"),
"user_capabilities": {"unattach": False},
},
"descendant_roles": ["read_role"],
}
)

ret['summary_fields']['direct_access'].extend([y for x in (format_team_role_perm(r, all_permissive_role_ids) for r in all_team_roles) for y in x])

return ret

direct_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('id', flat=True)
all_permissive_role_ids = Role.objects.filter(content_type=content_type, object_id=obj.id).values_list('ancestors__id', flat=True)

direct_access_roles = user.roles.filter(id__in=direct_permissive_role_ids).all()
Expand Down Expand Up @@ -3084,7 +3185,7 @@ def create(self, validated_data):
credential = super(CredentialSerializerCreate, self).create(validated_data)

if user:
credential.admin_role.members.add(user)
give_creator_permissions(user, credential)
if team:
if not credential.organization or team.organization.id != credential.organization.id:
raise serializers.ValidationError({"detail": _("Credential organization must be set and match before assigning to a team")})
Expand Down
Loading
Loading