mirror of
https://github.com/ansible/awx.git
synced 2026-01-08 14:32:07 -03:30
Replace role system with permissions-based DB roles
Develop ability to list permissions for existing roles Create a model registry for RBAC-tracked models Write the data migration logic for creating the preloaded role definitions Write migration to migrate old Role into ObjectRole model This loops over the old Role model, knowing it is unique on object and role_field Most of the logic is concerned with identifying the needed permissions, and then corresponding role definition As needed, object roles are created and users then teams are assigned Write re-computation of cache logic for teams and then for object role permissions Migrate new RBAC internals to ansible_base Migrate tests to ansible_base Implement solution for visible_roles Expose URLs for DAB RBAC
This commit is contained in:
parent
1859a6ae69
commit
817c3b36b9
@ -30,11 +30,15 @@ from rest_framework.permissions import IsAuthenticated
|
||||
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
|
||||
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
|
||||
@ -472,7 +476,11 @@ class ListAPIView(generics.ListAPIView, GenericAPIView):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class ParentMixin(object):
|
||||
@ -799,6 +807,11 @@ class ResourceAccessList(ParentMixin, ListAPIView):
|
||||
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))
|
||||
return (User.objects.filter(has_roles__in=ancestors) | User.objects.filter(is_superuser=True)).distinct()
|
||||
|
||||
roles = set(Role.objects.filter(content_type=content_type, object_id=obj.id))
|
||||
|
||||
ancestors = set()
|
||||
@ -959,6 +972,7 @@ class CopyAPIView(GenericAPIView):
|
||||
)
|
||||
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'):
|
||||
|
||||
@ -21,7 +21,7 @@ from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import User, Permission
|
||||
from django.contrib.auth.password_validation import validate_password as django_validate_password
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
|
||||
@ -43,11 +43,13 @@ from rest_framework.utils.serializer_helpers import ReturnList
|
||||
# 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
|
||||
|
||||
# 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,
|
||||
@ -102,7 +104,7 @@ from awx.main.models import (
|
||||
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,
|
||||
@ -2763,13 +2765,23 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
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:
|
||||
role_names.add(codename)
|
||||
return list(role_names)
|
||||
|
||||
def format_role_perm(role):
|
||||
role_dict = {'id': role.id, 'name': role.name, 'description': role.description}
|
||||
@ -2786,13 +2798,21 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
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)}
|
||||
|
||||
if role.singleton_name:
|
||||
descendant_perms = list(Permission.objects.filter(content_type=content_type).values_list('codename', flat=True))
|
||||
else:
|
||||
model_name = content_type.model
|
||||
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,
|
||||
@ -2812,10 +2832,48 @@ class ResourceAccessListElementSerializer(UserSerializer):
|
||||
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__users=user, **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))
|
||||
|
||||
ret['summary_fields']['direct_access'].extend(
|
||||
[y for x in (format_team_role_perm(r, direct_permissive_role_ids) for r in all_team_roles) for y in x]
|
||||
)
|
||||
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()
|
||||
@ -3085,6 +3143,7 @@ class CredentialSerializerCreate(CredentialSerializer):
|
||||
|
||||
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")})
|
||||
|
||||
@ -60,6 +60,9 @@ from oauth2_provider.models import get_access_token_model
|
||||
import pytz
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
# django-ansible-base
|
||||
from ansible_base.rbac.models import RoleEvaluation, ObjectRole
|
||||
|
||||
# AWX
|
||||
from awx.main.tasks.system import send_notifications, update_inventory_computed_fields
|
||||
from awx.main.access import get_user_queryset
|
||||
@ -87,6 +90,7 @@ from awx.api.generics import (
|
||||
from awx.api.views.labels import LabelSubListCreateAttachDetachView
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main import models
|
||||
from awx.main.models.rbac import give_creator_permissions, get_role_definition
|
||||
from awx.main.utils import (
|
||||
camelcase_to_underscore,
|
||||
extract_ansible_vars,
|
||||
@ -536,10 +540,12 @@ class InstanceGroupAccessList(ResourceAccessList):
|
||||
|
||||
|
||||
class InstanceGroupObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.InstanceGroup
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -724,6 +730,7 @@ class TeamUsersList(BaseUsersList):
|
||||
|
||||
|
||||
class TeamRolesList(SubListAttachDetachAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializerWithParentAccess
|
||||
metadata_class = RoleMetadata
|
||||
@ -763,10 +770,12 @@ class TeamRolesList(SubListAttachDetachAPIView):
|
||||
|
||||
|
||||
class TeamObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.Team
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -784,8 +793,15 @@ class TeamProjectsList(SubListAPIView):
|
||||
self.check_parent_access(team)
|
||||
model_ct = ContentType.objects.get_for_model(self.model)
|
||||
parent_ct = ContentType.objects.get_for_model(self.parent_model)
|
||||
proj_roles = models.Role.objects.filter(Q(ancestors__content_type=parent_ct) & Q(ancestors__object_id=team.pk), content_type=model_ct)
|
||||
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in proj_roles])
|
||||
|
||||
rd = get_role_definition(team.member_role)
|
||||
role = ObjectRole.objects.filter(object_id=team.id, content_type=parent_ct, role_definition=rd).first()
|
||||
if role is None:
|
||||
# Team has no permissions, therefore team has no projects
|
||||
return self.model.none()
|
||||
else:
|
||||
project_qs = self.model.accessible_objects(self.request.user, 'read_role')
|
||||
return project_qs.filter(id__in=RoleEvaluation.objects.filter(content_type_id=model_ct.id, role=role).values_list('object_id'))
|
||||
|
||||
|
||||
class TeamActivityStreamList(SubListAPIView):
|
||||
@ -800,10 +816,23 @@ class TeamActivityStreamList(SubListAPIView):
|
||||
self.check_parent_access(parent)
|
||||
|
||||
qs = self.request.user.get_queryset(self.model)
|
||||
|
||||
return qs.filter(
|
||||
Q(team=parent)
|
||||
| Q(project__in=models.Project.accessible_objects(parent.member_role, 'read_role'))
|
||||
| Q(credential__in=models.Credential.accessible_objects(parent.member_role, 'read_role'))
|
||||
| Q(
|
||||
project__in=RoleEvaluation.objects.filter(
|
||||
role__in=parent.has_roles.all(), content_type_id=ContentType.objects.get_for_model(models.Project).id, codename='view_project'
|
||||
)
|
||||
.values_list('object_id')
|
||||
.distinct()
|
||||
)
|
||||
| Q(
|
||||
credential__in=RoleEvaluation.objects.filter(
|
||||
role__in=parent.has_roles.all(), content_type_id=ContentType.objects.get_for_model(models.Credential).id, codename='view_credential'
|
||||
)
|
||||
.values_list('object_id')
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -1055,10 +1084,12 @@ class ProjectAccessList(ResourceAccessList):
|
||||
|
||||
|
||||
class ProjectObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.Project
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -1216,6 +1247,7 @@ class UserTeamsList(SubListAPIView):
|
||||
|
||||
|
||||
class UserRolesList(SubListAttachDetachAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializerWithParentAccess
|
||||
metadata_class = RoleMetadata
|
||||
@ -1490,10 +1522,12 @@ class CredentialAccessList(ResourceAccessList):
|
||||
|
||||
|
||||
class CredentialObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.Credential
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -2285,6 +2319,7 @@ class JobTemplateList(ListCreateAPIView):
|
||||
if ret.status_code == 201:
|
||||
job_template = models.JobTemplate.objects.get(id=ret.data['id'])
|
||||
job_template.admin_role.members.add(request.user)
|
||||
give_creator_permissions(request.user, job_template)
|
||||
return ret
|
||||
|
||||
|
||||
@ -2832,10 +2867,12 @@ class JobTemplateAccessList(ResourceAccessList):
|
||||
|
||||
|
||||
class JobTemplateObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.JobTemplate
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -3218,10 +3255,12 @@ class WorkflowJobTemplateAccessList(ResourceAccessList):
|
||||
|
||||
|
||||
class WorkflowJobTemplateObjectRolesList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.WorkflowJobTemplate
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
@ -4230,6 +4269,7 @@ class ActivityStreamDetail(RetrieveAPIView):
|
||||
|
||||
|
||||
class RoleList(ListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
@ -4237,11 +4277,13 @@ class RoleList(ListAPIView):
|
||||
|
||||
|
||||
class RoleDetail(RetrieveAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
|
||||
|
||||
class RoleUsersList(SubListAttachDetachAPIView):
|
||||
deprecated = True
|
||||
model = models.User
|
||||
serializer_class = serializers.UserSerializer
|
||||
parent_model = models.Role
|
||||
@ -4276,6 +4318,7 @@ class RoleUsersList(SubListAttachDetachAPIView):
|
||||
|
||||
|
||||
class RoleTeamsList(SubListAttachDetachAPIView):
|
||||
deprecated = True
|
||||
model = models.Team
|
||||
serializer_class = serializers.TeamSerializer
|
||||
parent_model = models.Role
|
||||
@ -4320,10 +4363,12 @@ class RoleTeamsList(SubListAttachDetachAPIView):
|
||||
team.member_role.children.remove(role)
|
||||
else:
|
||||
team.member_role.children.add(role)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class RoleParentsList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.Role
|
||||
@ -4337,6 +4382,7 @@ class RoleParentsList(SubListAPIView):
|
||||
|
||||
|
||||
class RoleChildrenList(SubListAPIView):
|
||||
deprecated = True
|
||||
model = models.Role
|
||||
serializer_class = serializers.RoleSerializer
|
||||
parent_model = models.Role
|
||||
|
||||
@ -152,6 +152,7 @@ class InventoryObjectRolesList(SubListAPIView):
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Inventory
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
|
||||
@ -226,6 +226,7 @@ class OrganizationObjectRolesList(SubListAPIView):
|
||||
serializer_class = RoleSerializer
|
||||
parent_model = Organization
|
||||
search_fields = ('role_field', 'content_type__model')
|
||||
deprecated = True
|
||||
|
||||
def get_queryset(self):
|
||||
po = self.get_parent_object()
|
||||
|
||||
@ -132,6 +132,9 @@ class ApiVersionRootView(APIView):
|
||||
data['bulk'] = reverse('api:bulk', request=request)
|
||||
data['analytics'] = reverse('api:analytics_root_view', request=request)
|
||||
data['service_index'] = django_reverse('service-index-root')
|
||||
data['role_definitions'] = django_reverse('roledefinition-list')
|
||||
data['role_user_assignments'] = django_reverse('roleuserassignment-list')
|
||||
data['role_team_assignments'] = django_reverse('roleteamassignment-list')
|
||||
return Response(data)
|
||||
|
||||
|
||||
|
||||
@ -20,7 +20,9 @@ from rest_framework.exceptions import ParseError, PermissionDenied
|
||||
# Django OAuth Toolkit
|
||||
from awx.main.models.oauth import OAuth2Application, OAuth2AccessToken
|
||||
|
||||
# django-ansible-base
|
||||
from ansible_base.lib.utils.validation import to_python_boolean
|
||||
from ansible_base.rbac.models import RoleEvaluation
|
||||
|
||||
# AWX
|
||||
from awx.main.utils import (
|
||||
@ -72,8 +74,6 @@ from awx.main.models import (
|
||||
WorkflowJobTemplateNode,
|
||||
WorkflowApproval,
|
||||
WorkflowApprovalTemplate,
|
||||
ROLE_SINGLETON_SYSTEM_ADMINISTRATOR,
|
||||
ROLE_SINGLETON_SYSTEM_AUDITOR,
|
||||
)
|
||||
from awx.main.models.mixins import ResourceMixin
|
||||
|
||||
@ -264,7 +264,7 @@ class BaseAccess(object):
|
||||
return self.can_change(obj, data)
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.user.is_superuser
|
||||
return self.user.has_obj_perm(obj, 'delete')
|
||||
|
||||
def can_copy(self, obj):
|
||||
return self.can_add({'reference_obj': obj})
|
||||
@ -651,9 +651,8 @@ class UserAccess(BaseAccess):
|
||||
qs = (
|
||||
User.objects.filter(pk__in=Organization.accessible_objects(self.user, 'read_role').values('member_role__members'))
|
||||
| User.objects.filter(pk=self.user.id)
|
||||
| User.objects.filter(
|
||||
pk__in=Role.objects.filter(singleton_name__in=[ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]).values('members')
|
||||
)
|
||||
| User.objects.filter(is_superuser=True)
|
||||
| User.objects.filter(profile__is_system_auditor=True)
|
||||
).distinct()
|
||||
return qs
|
||||
|
||||
@ -711,6 +710,15 @@ class UserAccess(BaseAccess):
|
||||
if not allow_orphans:
|
||||
# in these cases only superusers can modify orphan users
|
||||
return False
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
# Permission granted if the user has all permissions that the target user has
|
||||
target_perms = set(
|
||||
RoleEvaluation.objects.filter(role__in=obj.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct()
|
||||
)
|
||||
user_perms = set(
|
||||
RoleEvaluation.objects.filter(role__in=self.user.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct()
|
||||
)
|
||||
return not (target_perms - user_perms)
|
||||
return not obj.roles.all().exclude(ancestors__in=self.user.roles.all()).exists()
|
||||
else:
|
||||
return self.is_all_org_admin(obj)
|
||||
@ -949,9 +957,6 @@ class InventoryAccess(BaseAccess):
|
||||
def can_update(self, obj):
|
||||
return self.user in obj.update_role
|
||||
|
||||
def can_delete(self, obj):
|
||||
return self.can_admin(obj, None)
|
||||
|
||||
def can_run_ad_hoc_commands(self, obj):
|
||||
return self.user in obj.adhoc_role
|
||||
|
||||
@ -1405,8 +1410,12 @@ class ExecutionEnvironmentAccess(BaseAccess):
|
||||
def can_change(self, obj, data):
|
||||
if obj and obj.organization_id is None:
|
||||
raise PermissionDenied
|
||||
if self.user not in obj.organization.execution_environment_admin_role:
|
||||
raise PermissionDenied
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
if not self.user.has_obj_perm(obj, 'change'):
|
||||
raise PermissionDenied
|
||||
else:
|
||||
if self.user not in obj.organization.execution_environment_admin_role:
|
||||
raise PermissionDenied
|
||||
if data and 'organization' in data:
|
||||
new_org = get_object_from_data('organization', Organization, data, obj=obj)
|
||||
if not new_org or self.user not in new_org.execution_environment_admin_role:
|
||||
@ -1796,7 +1805,15 @@ class JobAccess(BaseAccess):
|
||||
return True
|
||||
|
||||
# Standard permissions model without job template involved
|
||||
if obj.organization and self.user in obj.organization.execute_role:
|
||||
# NOTE: this is the best we can do without caching way more permissions
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
filter_kwargs = dict(
|
||||
content_type_id=ContentType.objects.get_for_model(Organization),
|
||||
object_id=obj.organization_id,
|
||||
role_definition__permissions__codename='execute_jobtemplate',
|
||||
)
|
||||
if self.user.has_roles.filter(**filter_kwargs).exists():
|
||||
return True
|
||||
elif not (obj.job_template or obj.organization):
|
||||
raise PermissionDenied(_('Job has been orphaned from its job template and organization.'))
|
||||
@ -2592,6 +2609,8 @@ class ScheduleAccess(UnifiedCredentialsMixin, BaseAccess):
|
||||
if not JobLaunchConfigAccess(self.user).can_add(data):
|
||||
return False
|
||||
if not data:
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
return self.user.has_roles.filter(permission_partials__codename__in=['execute_jobtemplate', 'update_project', 'update_inventory']).exists()
|
||||
return Role.objects.filter(role_field__in=['update_role', 'execute_role'], ancestors__in=self.user.roles.all()).exists()
|
||||
|
||||
return self.check_related('unified_job_template', UnifiedJobTemplate, data, role_field='execute_role', mandatory=True)
|
||||
@ -2620,6 +2639,8 @@ class NotificationTemplateAccess(BaseAccess):
|
||||
prefetch_related = ('created_by', 'modified_by', 'organization')
|
||||
|
||||
def filtered_queryset(self):
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
return self.model.access_qs(self.user, 'view')
|
||||
return self.model.objects.filter(
|
||||
Q(organization__in=Organization.accessible_objects(self.user, 'notification_admin_role')) | Q(organization__in=self.user.auditor_of_organizations)
|
||||
).distinct()
|
||||
@ -2788,7 +2809,7 @@ class ActivityStreamAccess(BaseAccess):
|
||||
| Q(notification_template__organization__in=auditing_orgs)
|
||||
| Q(notification__notification_template__organization__in=auditing_orgs)
|
||||
| Q(label__organization__in=auditing_orgs)
|
||||
| Q(role__in=Role.objects.filter(ancestors__in=self.user.roles.all()) if auditing_orgs else [])
|
||||
| Q(role__in=Role.visible_roles(self.user) if auditing_orgs else [])
|
||||
)
|
||||
|
||||
project_set = Project.accessible_pk_qs(self.user, 'read_role')
|
||||
@ -2845,13 +2866,10 @@ class RoleAccess(BaseAccess):
|
||||
|
||||
def filtered_queryset(self):
|
||||
result = Role.visible_roles(self.user)
|
||||
# Sanity check: is the requesting user an orphaned non-admin/auditor?
|
||||
# if yes, make system admin/auditor mandatorily visible.
|
||||
if not self.user.is_superuser and not self.user.is_system_auditor and not self.user.organizations.exists():
|
||||
mandatories = ('system_administrator', 'system_auditor')
|
||||
super_qs = Role.objects.filter(singleton_name__in=mandatories)
|
||||
result = result | super_qs
|
||||
return result
|
||||
# Make system admin/auditor mandatorily visible.
|
||||
mandatories = ('system_administrator', 'system_auditor')
|
||||
super_qs = Role.objects.filter(singleton_name__in=mandatories)
|
||||
return result | super_qs
|
||||
|
||||
def can_add(self, obj, data):
|
||||
# Unsupported for now
|
||||
|
||||
@ -114,3 +114,28 @@ SUBSCRIPTION_USAGE_MODEL_UNIQUE_HOSTS = 'unique_managed_hosts'
|
||||
|
||||
# Shared prefetch to use for creating a queryset for the purpose of writing or saving facts
|
||||
HOST_FACTS_FIELDS = ('name', 'ansible_facts', 'ansible_facts_modified', 'modified', 'inventory_id')
|
||||
|
||||
# Data for RBAC compatibility layer
|
||||
role_name_to_perm_mapping = {
|
||||
'adhoc_role': ['adhoc_'],
|
||||
'approval_role': ['approve_'],
|
||||
'auditor_role': ['audit_'],
|
||||
'admin_role': ['change_', 'add_', 'delete_'],
|
||||
'execute_role': ['execute_'],
|
||||
'read_role': ['view_'],
|
||||
'update_role': ['update_'],
|
||||
'member_role': ['member_'],
|
||||
'use_role': ['use_'],
|
||||
}
|
||||
|
||||
org_role_to_permission = {
|
||||
'notification_admin_role': 'add_notificationtemplate',
|
||||
'project_admin_role': 'add_project',
|
||||
'execute_role': 'execute_jobtemplate',
|
||||
'inventory_admin_role': 'add_inventory',
|
||||
'credential_admin_role': 'add_credential',
|
||||
'workflow_admin_role': 'add_workflowjobtemplate',
|
||||
'job_template_admin_role': 'change_jobtemplate', # TODO: this doesnt really work, solution not clear
|
||||
'execution_environment_admin_role': 'add_executionenvironment',
|
||||
'auditor_role': 'view_project', # TODO: also doesnt really work
|
||||
}
|
||||
|
||||
100
awx/main/migrations/0190_add_django_permissions.py
Normal file
100
awx/main/migrations/0190_add_django_permissions.py
Normal file
@ -0,0 +1,100 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-13 20:10
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('main', '0189_inbound_hop_nodes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Add custom permissions for all special actions, like update, use, adhoc, and so on
|
||||
migrations.AlterModelOptions(
|
||||
name='credential',
|
||||
options={'ordering': ('name',), 'permissions': [('use_credential', 'Can use credential in a job or related resource')]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='instancegroup',
|
||||
options={'permissions': [('use_instancegroup', 'Can use instance group in a preference list of a resource')]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='inventory',
|
||||
options={
|
||||
'ordering': ('name',),
|
||||
'permissions': [
|
||||
('use_inventory', 'Can use inventory in a job template'),
|
||||
('adhoc_inventory', 'Can run ad hoc commands'),
|
||||
('update_inventory', 'Can update inventory sources in inventory'),
|
||||
],
|
||||
'verbose_name_plural': 'inventories',
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='jobtemplate',
|
||||
options={'ordering': ('name',), 'permissions': [('execute_jobtemplate', 'Can run this job template')]},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='project',
|
||||
options={
|
||||
'ordering': ('id',),
|
||||
'permissions': [('update_project', 'Can run a project update'), ('use_project', 'Can use project in a job template')],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='workflowjobtemplate',
|
||||
options={
|
||||
'permissions': [
|
||||
('execute_workflowjobtemplate', 'Can run this workflow job template'),
|
||||
('approve_workflowjobtemplate', 'Can approve steps in this workflow job template'),
|
||||
]
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='organization',
|
||||
options={
|
||||
'default_permissions': ('change', 'delete', 'view'),
|
||||
'ordering': ('name',),
|
||||
'permissions': [
|
||||
('member_organization', 'Basic participation permissions for organization'),
|
||||
('audit_organization', 'Audit everything inside the organization'),
|
||||
],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='team',
|
||||
options={'ordering': ('organization__name', 'name'), 'permissions': [('member_team', 'Inherit all roles assigned to this team')]},
|
||||
),
|
||||
# Remove add default permission for a few models
|
||||
migrations.AlterModelOptions(
|
||||
name='jobtemplate',
|
||||
options={
|
||||
'default_permissions': ('change', 'delete', 'view'),
|
||||
'ordering': ('name',),
|
||||
'permissions': [('execute_jobtemplate', 'Can run this job template')],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='instancegroup',
|
||||
options={
|
||||
'default_permissions': ('change', 'delete', 'view'),
|
||||
'permissions': [('use_instancegroup', 'Can use instance group in a preference list of a resource')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DABPermission',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
||||
('codename', models.CharField(max_length=100, verbose_name='codename')),
|
||||
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype', verbose_name='content type')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'permission',
|
||||
'verbose_name_plural': 'permissions',
|
||||
'ordering': ['content_type__model', 'codename'],
|
||||
'unique_together': {('content_type', 'codename')},
|
||||
},
|
||||
),
|
||||
]
|
||||
20
awx/main/migrations/0191_profile_is_system_auditor.py
Normal file
20
awx/main/migrations/0191_profile_is_system_auditor.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-20 16:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('main', '0190_add_django_permissions'),
|
||||
]
|
||||
run_before = [
|
||||
('dab_rbac', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='is_system_auditor',
|
||||
field=models.BooleanField(default=False, help_text='Can view everying in the system, proxies to User model'),
|
||||
),
|
||||
]
|
||||
22
awx/main/migrations/0192_custom_roles.py
Normal file
22
awx/main/migrations/0192_custom_roles.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Generated by Django 4.2.6 on 2023-11-21 02:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
from awx.main.migrations._dab_rbac import migrate_to_new_rbac, create_permissions_as_operation
|
||||
|
||||
from ansible_base.rbac.migrations._managed_definitions import setup_managed_role_definitions
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('main', '0191_profile_is_system_auditor'),
|
||||
('dab_rbac', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# make sure permissions and content types have been created by now
|
||||
# these normally run in a post_migrate signal but we need them for our logic
|
||||
migrations.RunPython(create_permissions_as_operation, migrations.RunPython.noop),
|
||||
migrations.RunPython(setup_managed_role_definitions, migrations.RunPython.noop),
|
||||
migrations.RunPython(migrate_to_new_rbac, migrations.RunPython.noop),
|
||||
]
|
||||
237
awx/main/migrations/_dab_rbac.py
Normal file
237
awx/main/migrations/_dab_rbac.py
Normal file
@ -0,0 +1,237 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from django.apps import apps as global_apps
|
||||
from django.db.models import ForeignKey
|
||||
from django.utils.timezone import now
|
||||
from ansible_base.rbac.migrations._utils import give_permissions, create_custom_permissions
|
||||
|
||||
from awx.main.fields import ImplicitRoleField
|
||||
from awx.main.constants import role_name_to_perm_mapping
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.migrations._dab_rbac')
|
||||
|
||||
|
||||
def create_permissions_as_operation(apps, schema_editor):
|
||||
create_custom_permissions(global_apps.get_app_config("main"))
|
||||
|
||||
|
||||
"""
|
||||
Data structures and methods for the migration of old Role model to ObjectRole
|
||||
"""
|
||||
|
||||
system_admin = ImplicitRoleField(name='system_administrator')
|
||||
system_auditor = ImplicitRoleField(name='system_auditor')
|
||||
system_admin.model = None
|
||||
system_auditor.model = None
|
||||
|
||||
|
||||
def resolve_parent_role(f, role_path):
|
||||
"""
|
||||
Given a field and a path declared in parent_role from the field definition, like
|
||||
execute_role = ImplicitRoleField(parent_role='admin_role')
|
||||
This expects to be passed in (execute_role object, "admin_role")
|
||||
It hould return the admin_role from that object
|
||||
"""
|
||||
if role_path == 'singleton:system_administrator':
|
||||
return system_admin
|
||||
elif role_path == 'singleton:system_auditor':
|
||||
return system_auditor
|
||||
else:
|
||||
related_field = f
|
||||
current_model = f.model
|
||||
for related_field_name in role_path.split('.'):
|
||||
related_field = current_model._meta.get_field(related_field_name)
|
||||
if isinstance(related_field, ForeignKey) and not isinstance(related_field, ImplicitRoleField):
|
||||
current_model = related_field.related_model
|
||||
return related_field
|
||||
|
||||
|
||||
def build_role_map(apps):
|
||||
"""
|
||||
For the old Role model, this builds and returns dictionaries (children, parents)
|
||||
which give a global mapping of the ImplicitRoleField instances according to the graph
|
||||
"""
|
||||
models = set(apps.get_app_config('main').get_models())
|
||||
|
||||
all_fields = set()
|
||||
parents = {}
|
||||
children = {}
|
||||
|
||||
all_fields.add(system_admin)
|
||||
all_fields.add(system_auditor)
|
||||
|
||||
for cls in models:
|
||||
for f in cls._meta.get_fields():
|
||||
if isinstance(f, ImplicitRoleField):
|
||||
all_fields.add(f)
|
||||
|
||||
for f in all_fields:
|
||||
if f.parent_role is not None:
|
||||
if isinstance(f.parent_role, str):
|
||||
parent_roles = [f.parent_role]
|
||||
else:
|
||||
parent_roles = f.parent_role
|
||||
|
||||
# SPECIAL CASE: organization auditor_role is not a child of admin_role
|
||||
# this makes no practical sense and conflicts with expected managed role
|
||||
# so we put it in as a hack here
|
||||
if f.name == 'auditor_role' and f.model._meta.model_name == 'organization':
|
||||
parent_roles.append('admin_role')
|
||||
|
||||
parent_list = []
|
||||
for rel_name in parent_roles:
|
||||
parent_list.append(resolve_parent_role(f, rel_name))
|
||||
|
||||
parents[f] = parent_list
|
||||
|
||||
# build children lookup from parents lookup
|
||||
for child_field, parent_list in parents.items():
|
||||
for parent_field in parent_list:
|
||||
children.setdefault(parent_field, [])
|
||||
children[parent_field].append(child_field)
|
||||
|
||||
return (parents, children)
|
||||
|
||||
|
||||
def get_descendents(f, children_map):
|
||||
"""
|
||||
Given ImplicitRoleField F and the children mapping, returns all descendents
|
||||
of that field, as a set of other fields, including itself
|
||||
"""
|
||||
ret = {f}
|
||||
if f in children_map:
|
||||
for child_field in children_map[f]:
|
||||
ret.update(get_descendents(child_field, children_map))
|
||||
return ret
|
||||
|
||||
|
||||
def get_permissions_for_role(role_field, children_map, apps):
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||
|
||||
perm_list = []
|
||||
for child_field in get_descendents(role_field, children_map):
|
||||
if child_field.name in role_name_to_perm_mapping:
|
||||
for perm_name in role_name_to_perm_mapping[child_field.name]:
|
||||
if perm_name == 'add_' and role_field.model._meta.model_name != 'organization':
|
||||
continue # only organizations can contain add permissions
|
||||
perm = Permission.objects.filter(content_type=ContentType.objects.get_for_model(child_field.model), codename__startswith=perm_name).first()
|
||||
if perm is not None and perm not in perm_list:
|
||||
perm_list.append(perm)
|
||||
|
||||
# special case for two models that have object roles but no organization roles in old system
|
||||
if role_field.name == 'notification_admin_role' or (role_field.name == 'admin_role' and role_field.model._meta.model_name == 'organization'):
|
||||
ct = ContentType.objects.get_for_model(apps.get_model('main', 'NotificationTemplate'))
|
||||
perm_list.extend(list(Permission.objects.filter(content_type=ct)))
|
||||
if role_field.name == 'execution_environment_admin_role' or (role_field.name == 'admin_role' and role_field.model._meta.model_name == 'organization'):
|
||||
ct = ContentType.objects.get_for_model(apps.get_model('main', 'ExecutionEnvironment'))
|
||||
perm_list.extend(list(Permission.objects.filter(content_type=ct)))
|
||||
|
||||
# more special cases for those same above special org-level roles
|
||||
if role_field.name == 'auditor_role':
|
||||
for codename in ('view_notificationtemplate', 'view_executionenvironment'):
|
||||
perm_list.append(Permission.objects.get(codename=codename))
|
||||
|
||||
return perm_list
|
||||
|
||||
|
||||
def migrate_to_new_rbac(apps, schema_editor):
|
||||
"""
|
||||
This method moves the assigned permissions from the old rbac.py models
|
||||
to the new RoleDefinition and ObjectRole models
|
||||
"""
|
||||
Role = apps.get_model('main', 'Role')
|
||||
RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition')
|
||||
Permission = apps.get_model('auth', 'Permission')
|
||||
migration_time = now()
|
||||
|
||||
# remove add premissions that are not valid for migrations from old versions
|
||||
for perm_str in ('add_organization', 'add_jobtemplate'):
|
||||
perm = Permission.objects.filter(codename=perm_str).first()
|
||||
if perm:
|
||||
perm.delete()
|
||||
|
||||
managed_definitions = dict()
|
||||
for role_definition in RoleDefinition.objects.filter(managed=True):
|
||||
permissions = frozenset(role_definition.permissions.values_list('id', flat=True))
|
||||
managed_definitions[permissions] = role_definition
|
||||
|
||||
# Build map of old role model
|
||||
parents, children = build_role_map(apps)
|
||||
|
||||
# NOTE: this import is expected to break at some point, and then just move the data here
|
||||
from awx.main.models.rbac import role_descriptions
|
||||
|
||||
for role in Role.objects.prefetch_related('members', 'parents').iterator():
|
||||
if role.singleton_name:
|
||||
continue # only bothering to migrate object roles
|
||||
|
||||
team_roles = []
|
||||
for parent in role.parents.all():
|
||||
if parent.id not in json.loads(role.implicit_parents):
|
||||
team_roles.append(parent)
|
||||
|
||||
# we will not create any roles that do not have any users or teams
|
||||
if not (role.members.all() or team_roles):
|
||||
logger.debug(f'Skipping role {role.role_field} for {role.content_type.model}-{role.object_id} due to no members')
|
||||
continue
|
||||
|
||||
# get a list of permissions that the old role would grant
|
||||
object_cls = apps.get_model(f'main.{role.content_type.model}')
|
||||
object = object_cls.objects.get(pk=role.object_id) # WORKAROUND, role.content_object does not work in migrations
|
||||
f = object._meta.get_field(role.role_field) # should be ImplicitRoleField
|
||||
perm_list = get_permissions_for_role(f, children, apps)
|
||||
|
||||
permissions = frozenset(perm.id for perm in perm_list)
|
||||
|
||||
# With the needed permissions established, obtain the RoleDefinition this will need, priorities:
|
||||
# 1. If it exists as a managed RoleDefinition then obviously use that
|
||||
# 2. If we already created this for a prior role, use that
|
||||
# 3. Create a new RoleDefinition that lists those permissions
|
||||
if permissions in managed_definitions:
|
||||
role_definition = managed_definitions[permissions]
|
||||
else:
|
||||
action = role.role_field.rsplit('_', 1)[0] # remove the _field ending of the name
|
||||
role_definition_name = f'{role.content_type.model}-{action}'
|
||||
|
||||
description = role_descriptions[role.role_field]
|
||||
if type(description) == dict:
|
||||
if role.content_type.model in description:
|
||||
description = description.get(role.content_type.model)
|
||||
else:
|
||||
description = description.get('default')
|
||||
if '%s' in description:
|
||||
description = description % role.content_type.model
|
||||
|
||||
role_definition, created = RoleDefinition.objects.get_or_create(
|
||||
name=role_definition_name,
|
||||
defaults={'description': description, 'content_type_id': role.content_type_id, 'created_on': migration_time, 'modified_on': migration_time},
|
||||
)
|
||||
|
||||
if created:
|
||||
logger.info(f'Created custom Role Definition {role_definition_name}, pk={role_definition.pk}')
|
||||
role_definition.permissions.set(perm_list)
|
||||
|
||||
# Create the object role and add users to it
|
||||
give_permissions(
|
||||
apps,
|
||||
role_definition,
|
||||
users=role.members.all(),
|
||||
teams=[tr.object_id for tr in team_roles],
|
||||
object_id=role.object_id,
|
||||
content_type_id=role.content_type_id,
|
||||
)
|
||||
|
||||
# migrate is_system_auditor flag, because it is no longer handled by a system role
|
||||
role = Role.objects.filter(singleton_name='system_auditor').first()
|
||||
if role:
|
||||
# if the system auditor role is not present, this is a new install and no users should exist
|
||||
ct = 0
|
||||
for user in role.members.all():
|
||||
user.profile.is_system_auditor = True
|
||||
user.profile.save(update_fields=['is_system_auditor'])
|
||||
ct += 1
|
||||
if ct:
|
||||
logger.info(f'Migrated {ct} users to new system auditor flag')
|
||||
@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved.
|
||||
|
||||
import json
|
||||
|
||||
# Django
|
||||
from django.conf import settings # noqa
|
||||
from django.db import connection
|
||||
@ -8,7 +10,9 @@ from django.db.models.signals import pre_delete # noqa
|
||||
|
||||
# django-ansible-base
|
||||
from ansible_base.resource_registry.fields import AnsibleResourceField
|
||||
from ansible_base.rbac import permission_registry
|
||||
from ansible_base.lib.utils.models import prevent_search
|
||||
from ansible_base.lib.utils.models import user_summary_fields
|
||||
|
||||
# AWX
|
||||
from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa
|
||||
@ -102,6 +106,7 @@ User.add_to_class('get_queryset', get_user_queryset)
|
||||
User.add_to_class('can_access', check_user_access)
|
||||
User.add_to_class('can_access_with_errors', check_user_access_with_errors)
|
||||
User.add_to_class('resource', AnsibleResourceField(primary_key_field="id"))
|
||||
User.add_to_class('summary_fields', user_summary_fields)
|
||||
|
||||
|
||||
def convert_jsonfields():
|
||||
@ -198,7 +203,7 @@ User.add_to_class('created', created)
|
||||
def user_is_system_auditor(user):
|
||||
if not hasattr(user, '_is_system_auditor'):
|
||||
if user.pk:
|
||||
user._is_system_auditor = user.roles.filter(singleton_name='system_auditor', role_field='system_auditor').exists()
|
||||
user._is_system_auditor = user.profile.is_system_auditor
|
||||
else:
|
||||
# Odd case where user is unsaved, this should never be relied on
|
||||
return False
|
||||
@ -212,17 +217,13 @@ def user_is_system_auditor(user, tf):
|
||||
# time they've logged in, and we've just created the new User in this
|
||||
# request), we need one to set up the system auditor role
|
||||
user.save()
|
||||
if tf:
|
||||
role = Role.singleton('system_auditor')
|
||||
# must check if member to not duplicate activity stream
|
||||
if user not in role.members.all():
|
||||
role.members.add(user)
|
||||
user._is_system_auditor = True
|
||||
else:
|
||||
role = Role.singleton('system_auditor')
|
||||
if user in role.members.all():
|
||||
role.members.remove(user)
|
||||
user._is_system_auditor = False
|
||||
if user.profile.is_system_auditor != bool(tf):
|
||||
prior_value = user.profile.is_system_auditor
|
||||
user.profile.is_system_auditor = bool(tf)
|
||||
user.profile.save(update_fields=['is_system_auditor'])
|
||||
user._is_system_auditor = user.profile.is_system_auditor
|
||||
entry = ActivityStream.objects.create(changes=json.dumps({"is_system_auditor": [prior_value, bool(tf)]}), object1='user', operation='update')
|
||||
entry.user.add(user)
|
||||
|
||||
|
||||
User.add_to_class('is_system_auditor', user_is_system_auditor)
|
||||
@ -290,6 +291,10 @@ activity_stream_registrar.connect(WorkflowApprovalTemplate)
|
||||
activity_stream_registrar.connect(OAuth2Application)
|
||||
activity_stream_registrar.connect(OAuth2AccessToken)
|
||||
|
||||
# Register models
|
||||
permission_registry.register(Project, Team, WorkflowJobTemplate, JobTemplate, Inventory, Organization, Credential, NotificationTemplate, ExecutionEnvironment)
|
||||
permission_registry.register(InstanceGroup, parent_field_name=None) # Not part of an organization
|
||||
|
||||
# prevent API filtering on certain Django-supplied sensitive fields
|
||||
prevent_search(User._meta.get_field('password'))
|
||||
prevent_search(OAuth2AccessToken._meta.get_field('token'))
|
||||
|
||||
@ -7,6 +7,9 @@ from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
|
||||
# django-ansible-base
|
||||
from ansible_base.lib.utils.models import get_type_for_model
|
||||
|
||||
# Django-CRUM
|
||||
from crum import get_current_user
|
||||
|
||||
@ -139,6 +142,23 @@ class BaseModel(models.Model):
|
||||
self.save(update_fields=update_fields)
|
||||
return update_fields
|
||||
|
||||
def summary_fields(self):
|
||||
"""
|
||||
This exists for use by django-ansible-base,
|
||||
which has standard patterns that differ from AWX, but we enable views from DAB
|
||||
for those views to list summary_fields for AWX models, those models need to provide this
|
||||
"""
|
||||
from awx.api.serializers import SUMMARIZABLE_FK_FIELDS
|
||||
|
||||
model_name = get_type_for_model(self)
|
||||
related_fields = SUMMARIZABLE_FK_FIELDS.get(model_name, {})
|
||||
summary_data = {}
|
||||
for field_name in related_fields:
|
||||
fval = getattr(self, field_name, None)
|
||||
if fval is not None:
|
||||
summary_data[field_name] = fval
|
||||
return summary_data
|
||||
|
||||
|
||||
class CreatedModifiedModel(BaseModel):
|
||||
"""
|
||||
|
||||
@ -83,6 +83,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique, ResourceMixin):
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
unique_together = ('organization', 'name', 'credential_type')
|
||||
permissions = [('use_credential', 'Can use credential in a job or related resource')]
|
||||
|
||||
PASSWORD_FIELDS = ['inputs']
|
||||
FIELDS_TO_PRESERVE_AT_COPY = ['input_sources']
|
||||
|
||||
@ -485,6 +485,9 @@ class InstanceGroup(HasPolicyEditsMixin, BaseModel, RelatedJobsMixin, ResourceMi
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
permissions = [('use_instancegroup', 'Can use instance group in a preference list of a resource')]
|
||||
# Since this has no direct organization field only superuser can add, so remove add permission
|
||||
default_permissions = ('change', 'delete', 'view')
|
||||
|
||||
def set_default_policy_fields(self):
|
||||
self.policy_instance_list = []
|
||||
|
||||
@ -89,6 +89,11 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin):
|
||||
verbose_name_plural = _('inventories')
|
||||
unique_together = [('name', 'organization')]
|
||||
ordering = ('name',)
|
||||
permissions = [
|
||||
('use_inventory', 'Can use inventory in a job template'),
|
||||
('adhoc_inventory', 'Can run ad hoc commands'),
|
||||
('update_inventory', 'Can update inventory sources in inventory'),
|
||||
]
|
||||
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
@ -1400,7 +1405,7 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin,
|
||||
return selected_groups
|
||||
|
||||
|
||||
class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
|
||||
class CustomInventoryScript(CommonModelNameNotUnique):
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
|
||||
@ -205,6 +205,9 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
permissions = [('execute_jobtemplate', 'Can run this job template')]
|
||||
# Remove add permission, ability to add comes from use permission for inventory, project, credentials
|
||||
default_permissions = ('change', 'delete', 'view')
|
||||
|
||||
job_type = models.CharField(
|
||||
max_length=64,
|
||||
|
||||
@ -19,13 +19,14 @@ from django.utils.translation import gettext_lazy as _
|
||||
from ansible_base.lib.utils.models import prevent_search
|
||||
|
||||
# AWX
|
||||
from awx.main.models.rbac import Role, RoleAncestorEntry
|
||||
|
||||
from awx.main.models.rbac import Role, RoleAncestorEntry, to_permissions
|
||||
from awx.main.utils import parse_yaml_or_json, get_custom_venv_choices, get_licenser, polymorphic
|
||||
from awx.main.utils.execution_environments import get_default_execution_environment
|
||||
from awx.main.utils.encryption import decrypt_value, get_encryption_key, is_encrypted
|
||||
from awx.main.utils.polymorphic import build_polymorphic_ctypes_map
|
||||
from awx.main.fields import AskForField
|
||||
from awx.main.constants import ACTIVE_STATES
|
||||
from awx.main.constants import ACTIVE_STATES, org_role_to_permission
|
||||
|
||||
|
||||
logger = logging.getLogger('awx.main.models.mixins')
|
||||
@ -64,6 +65,22 @@ class ResourceMixin(models.Model):
|
||||
|
||||
@staticmethod
|
||||
def _accessible_pk_qs(cls, accessor, role_field, content_types=None):
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
if role_field not in to_permissions and cls._meta.model_name == 'organization':
|
||||
# superficial alternative for narrow exceptions with org roles
|
||||
# I think this mostly applies to organization members, which is not fully defined yet
|
||||
if accessor.is_superuser:
|
||||
return cls.objects.values_list('id')
|
||||
from ansible_base.rbac.models import ObjectRole
|
||||
|
||||
codename = org_role_to_permission[role_field]
|
||||
|
||||
return (
|
||||
ObjectRole.objects.filter(role_definition__permissions__codename=codename, content_type=ContentType.objects.get_for_model(cls))
|
||||
.values_list('object_id')
|
||||
.distinct()
|
||||
)
|
||||
return cls.access_ids_qs(accessor, to_permissions[role_field], content_types=content_types)
|
||||
if accessor._meta.model_name == 'user':
|
||||
ancestor_roles = accessor.roles.all()
|
||||
elif type(accessor) == Role:
|
||||
|
||||
@ -7,6 +7,7 @@ from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.timezone import now as tz_now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -35,6 +36,12 @@ class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVi
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('name',)
|
||||
permissions = [
|
||||
('member_organization', 'Basic participation permissions for organization'),
|
||||
('audit_organization', 'Audit everything inside the organization'),
|
||||
]
|
||||
# Remove add permission, only superuser can add
|
||||
default_permissions = ('change', 'delete', 'view')
|
||||
|
||||
instance_groups = OrderedManyToManyField('InstanceGroup', blank=True, through='OrganizationInstanceGroupMembership')
|
||||
galaxy_credentials = OrderedManyToManyField(
|
||||
@ -137,6 +144,7 @@ class Team(CommonModelNameNotUnique, ResourceMixin):
|
||||
app_label = 'main'
|
||||
unique_together = [('organization', 'name')]
|
||||
ordering = ('organization__name', 'name')
|
||||
permissions = [('member_team', 'Inherit all roles assigned to this team')]
|
||||
|
||||
organization = models.ForeignKey(
|
||||
'Organization',
|
||||
@ -174,6 +182,7 @@ class Profile(CreatedModifiedModel):
|
||||
max_length=1024,
|
||||
default='',
|
||||
)
|
||||
is_system_auditor = models.BooleanField(default=False, help_text=_('Can view everying in the system, proxies to User model'))
|
||||
|
||||
|
||||
class UserSessionMembership(BaseModel):
|
||||
@ -208,3 +217,23 @@ if not hasattr(User, 'get_absolute_url'):
|
||||
return reverse('api:user_detail', kwargs={'pk': user.pk}, request=request)
|
||||
|
||||
User.add_to_class('get_absolute_url', user_get_absolute_url)
|
||||
|
||||
|
||||
class DABPermission(models.Model):
|
||||
"""
|
||||
This is a partial copy of auth.Permission to be used by DAB RBAC lib
|
||||
and in order to be consistent with other applications
|
||||
"""
|
||||
|
||||
name = models.CharField("name", max_length=255)
|
||||
content_type = models.ForeignKey(ContentType, models.CASCADE, verbose_name="content type")
|
||||
codename = models.CharField("codename", max_length=100)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "permission"
|
||||
verbose_name_plural = "permissions"
|
||||
unique_together = [["content_type", "codename"]]
|
||||
ordering = ["content_type__model", "codename"]
|
||||
|
||||
def __str__(self):
|
||||
return f"<{self.__class__.__name__}: {self.codename}>"
|
||||
|
||||
@ -259,6 +259,7 @@ class Project(UnifiedJobTemplate, ProjectOptions, ResourceMixin, CustomVirtualEn
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
ordering = ('id',)
|
||||
permissions = [('update_project', 'Can run a project update'), ('use_project', 'Can use project in a job template')]
|
||||
|
||||
default_environment = models.ForeignKey(
|
||||
'ExecutionEnvironment',
|
||||
|
||||
@ -9,12 +9,22 @@ import re
|
||||
|
||||
# Django
|
||||
from django.db import models, transaction, connection
|
||||
from django.db.models.signals import m2m_changed
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
|
||||
# Ansible_base app
|
||||
from ansible_base.rbac.models import RoleDefinition
|
||||
from ansible_base.lib.utils.models import get_type_for_model
|
||||
|
||||
# AWX
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.migrations._dab_rbac import build_role_map, get_permissions_for_role
|
||||
from awx.main.constants import role_name_to_perm_mapping, org_role_to_permission
|
||||
|
||||
__all__ = [
|
||||
'Role',
|
||||
@ -75,6 +85,11 @@ role_descriptions = {
|
||||
}
|
||||
|
||||
|
||||
to_permissions = {}
|
||||
for k, v in role_name_to_perm_mapping.items():
|
||||
to_permissions[k] = v[0].strip('_')
|
||||
|
||||
|
||||
tls = threading.local() # thread local storage
|
||||
|
||||
|
||||
@ -86,10 +101,8 @@ def check_singleton(func):
|
||||
"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
sys_admin = Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR)
|
||||
sys_audit = Role.singleton(ROLE_SINGLETON_SYSTEM_AUDITOR)
|
||||
user = args[0]
|
||||
if user in sys_admin or user in sys_audit:
|
||||
if user.is_superuser or user.is_system_auditor:
|
||||
if len(args) == 2:
|
||||
return args[1]
|
||||
return Role.objects.all()
|
||||
@ -169,6 +182,27 @@ class Role(models.Model):
|
||||
|
||||
def __contains__(self, accessor):
|
||||
if accessor._meta.model_name == 'user':
|
||||
if accessor.is_superuser:
|
||||
return True
|
||||
if self.role_field == 'system_administrator':
|
||||
return accessor.is_superuser
|
||||
elif self.role_field == 'system_auditor':
|
||||
return accessor.is_system_auditor
|
||||
elif self.role_field in ('read_role', 'auditor_role') and accessor.is_system_auditor:
|
||||
return True
|
||||
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
if self.role_field not in to_permissions and self.content_object and self.content_object._meta.model_name == 'organization':
|
||||
# valid alternative for narrow exceptions with org roles
|
||||
if self.role_field not in org_role_to_permission:
|
||||
raise Exception(f'org {self.role_field} evaluated but not a translatable permission')
|
||||
codename = org_role_to_permission[self.role_field]
|
||||
|
||||
return accessor.has_obj_perm(self.content_object, codename)
|
||||
|
||||
if self.role_field not in to_permissions:
|
||||
raise Exception(f'{self.role_field} evaluated but not a translatable permission')
|
||||
return accessor.has_obj_perm(self.content_object, to_permissions[self.role_field])
|
||||
return self.ancestors.filter(members=accessor).exists()
|
||||
else:
|
||||
raise RuntimeError(f'Role evaluations only valid for users, received {accessor}')
|
||||
@ -280,6 +314,9 @@ class Role(models.Model):
|
||||
#
|
||||
#
|
||||
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
return
|
||||
|
||||
if len(additions) == 0 and len(removals) == 0:
|
||||
return
|
||||
|
||||
@ -412,6 +449,12 @@ class Role(models.Model):
|
||||
in their organization, but some of those roles descend from
|
||||
organization admin_role, but not auditor_role.
|
||||
"""
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
from ansible_base.rbac.models import RoleEvaluation
|
||||
|
||||
q = RoleEvaluation.objects.filter(role__in=user.has_roles.all()).values_list('object_id', 'content_type_id').query
|
||||
return roles_qs.extra(where=[f'(object_id,content_type_id) in ({q})'])
|
||||
|
||||
return roles_qs.filter(
|
||||
id__in=RoleAncestorEntry.objects.filter(
|
||||
descendent__in=RoleAncestorEntry.objects.filter(ancestor_id__in=list(user.roles.values_list('id', flat=True))).values_list(
|
||||
@ -434,6 +477,13 @@ class Role(models.Model):
|
||||
return self.singleton_name in [ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR]
|
||||
|
||||
|
||||
class AncestorManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
raise RuntimeError('The old RBAC system has been disabled, this should never be called')
|
||||
return super(AncestorManager, self).get_queryset()
|
||||
|
||||
|
||||
class RoleAncestorEntry(models.Model):
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
@ -451,6 +501,8 @@ class RoleAncestorEntry(models.Model):
|
||||
content_type_id = models.PositiveIntegerField(null=False)
|
||||
object_id = models.PositiveIntegerField(null=False)
|
||||
|
||||
objects = AncestorManager()
|
||||
|
||||
|
||||
def role_summary_fields_generator(content_object, role_field):
|
||||
global role_descriptions
|
||||
@ -479,3 +531,133 @@ def role_summary_fields_generator(content_object, role_field):
|
||||
summary['name'] = role_names[role_field]
|
||||
summary['id'] = getattr(content_object, '{}_id'.format(role_field))
|
||||
return summary
|
||||
|
||||
|
||||
# ----------------- Custom Role Compatibility -------------------------
|
||||
# The following are methods to connect this (old) RBAC system to the new
|
||||
# system which allows custom roles
|
||||
# this follows the ORM interface layer documented in docs/rbac.md
|
||||
def get_role_codenames(role):
|
||||
obj = role.content_object
|
||||
if obj is None:
|
||||
return
|
||||
f = obj._meta.get_field(role.role_field)
|
||||
parents, children = build_role_map(apps)
|
||||
return [perm.codename for perm in get_permissions_for_role(f, children, apps)]
|
||||
|
||||
|
||||
def get_role_definition(role):
|
||||
"""Given a old-style role, this gives a role definition in the new RBAC system for it"""
|
||||
obj = role.content_object
|
||||
if obj is None:
|
||||
return
|
||||
f = obj._meta.get_field(role.role_field)
|
||||
action_name = f.name.rsplit("_", 1)[0]
|
||||
rd_name = f'{obj._meta.model_name}-{action_name}-compat'
|
||||
perm_list = get_role_codenames(role)
|
||||
rd, created = RoleDefinition.objects.get_or_create(name=rd_name, permissions=perm_list, defaults={'content_type_id': role.content_type_id})
|
||||
return rd
|
||||
|
||||
|
||||
def get_role_from_object_role(object_role):
|
||||
"""
|
||||
Given an object role from the new system, return the corresponding role from the old system
|
||||
reverses naming from get_role_definition, and the ANSIBLE_BASE_ROLE_PRECREATE setting.
|
||||
"""
|
||||
rd = object_role.role_definition
|
||||
if rd.name.endswith('-compat'):
|
||||
model_name, role_name, _ = rd.name.split('-')
|
||||
role_name += '_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('-')
|
||||
model_cls = apps.get_model('main', target_model_name)
|
||||
target_model_name = get_type_for_model(model_cls)
|
||||
if target_model_name == 'notification_template':
|
||||
target_model_name = 'notification' # total exception
|
||||
role_name = f'{target_model_name}_admin_role'
|
||||
elif rd.name.endswith('-admin'):
|
||||
# cases like "project-admin"
|
||||
model_name, _ = rd.name.rsplit('-', 1)
|
||||
role_name = 'admin_role'
|
||||
else:
|
||||
model_name, role_name = rd.name.split('-')
|
||||
role_name += '_role'
|
||||
return getattr(object_role.content_object, role_name)
|
||||
|
||||
|
||||
def give_or_remove_permission(role, actor, giving=True):
|
||||
obj = role.content_object
|
||||
if obj is None:
|
||||
return
|
||||
rd = get_role_definition(role)
|
||||
rd.give_or_remove_permission(actor, obj, giving=giving)
|
||||
|
||||
|
||||
def give_creator_permissions(user, obj):
|
||||
RoleDefinition.objects.give_creator_permissions(user, obj)
|
||||
|
||||
|
||||
def sync_members_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs):
|
||||
if action.startswith('pre_'):
|
||||
return
|
||||
|
||||
if action == 'post_add':
|
||||
is_giving = True
|
||||
elif action == 'post_remove':
|
||||
is_giving = False
|
||||
elif action == 'post_clear':
|
||||
raise RuntimeError('Clearing of role members not supported')
|
||||
|
||||
if reverse:
|
||||
user = instance
|
||||
else:
|
||||
role = instance
|
||||
|
||||
for user_or_role_id in pk_set:
|
||||
if reverse:
|
||||
role = Role.objects.get(pk=user_or_role_id)
|
||||
else:
|
||||
user = get_user_model().objects.get(pk=user_or_role_id)
|
||||
give_or_remove_permission(role, user, giving=is_giving)
|
||||
|
||||
|
||||
def sync_parents_to_new_rbac(instance, action, model, pk_set, reverse, **kwargs):
|
||||
if action.startswith('pre_'):
|
||||
return
|
||||
|
||||
if action == 'post_add':
|
||||
is_giving = True
|
||||
elif action == 'post_remove':
|
||||
is_giving = False
|
||||
elif action == 'post_clear':
|
||||
raise RuntimeError('Clearing of role members not supported')
|
||||
|
||||
from awx.main.models.organization import Team
|
||||
|
||||
if reverse:
|
||||
parent_role = instance
|
||||
else:
|
||||
child_role = instance
|
||||
|
||||
for role_id in pk_set:
|
||||
if reverse:
|
||||
child_role = Role.objects.get(id=role_id)
|
||||
else:
|
||||
parent_role = Role.objects.get(id=role_id)
|
||||
|
||||
# To a fault, we want to avoid running this if triggered from implicit_parents management
|
||||
# we only want to do anything if we know for sure this is a non-implicit team role
|
||||
if parent_role.role_field not in ('member_role', 'admin_role') or parent_role.content_type.model != 'team':
|
||||
return
|
||||
|
||||
# Team member role is a parent of its read role so we want to avoid this
|
||||
if child_role.role_field == 'read_role' and child_role.content_type.model == 'team':
|
||||
return
|
||||
|
||||
team = Team.objects.get(pk=parent_role.object_id)
|
||||
give_or_remove_permission(child_role, team, giving=is_giving)
|
||||
|
||||
|
||||
m2m_changed.connect(sync_members_to_new_rbac, Role.members.through)
|
||||
m2m_changed.connect(sync_parents_to_new_rbac, Role.parents.through)
|
||||
|
||||
@ -37,7 +37,8 @@ from awx.main.models.base import CommonModelNameNotUnique, PasswordFieldsModel,
|
||||
from awx.main.dispatch import get_task_queuename
|
||||
from awx.main.dispatch.control import Control as ControlDispatcher
|
||||
from awx.main.registrar import activity_stream_registrar
|
||||
from awx.main.models.mixins import ResourceMixin, TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
||||
from awx.main.models.mixins import TaskManagerUnifiedJobMixin, ExecutionEnvironmentMixin
|
||||
from awx.main.models.rbac import to_permissions
|
||||
from awx.main.utils.common import (
|
||||
camelcase_to_underscore,
|
||||
get_model_for_type,
|
||||
@ -210,7 +211,15 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique, ExecutionEn
|
||||
# do not use this if in a subclass
|
||||
if cls != UnifiedJobTemplate:
|
||||
return super(UnifiedJobTemplate, cls).accessible_pk_qs(accessor, role_field)
|
||||
return ResourceMixin._accessible_pk_qs(cls, accessor, role_field, content_types=cls._submodels_with_roles())
|
||||
from ansible_base.rbac.models import RoleEvaluation
|
||||
|
||||
action = to_permissions[role_field]
|
||||
|
||||
return (
|
||||
RoleEvaluation.objects.filter(role__in=accessor.has_roles.all(), codename__startswith=action, content_type_id__in=cls._submodels_with_roles())
|
||||
.values_list('object_id')
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def _perform_unique_checks(self, unique_checks):
|
||||
# Handle the list of unique fields returned above. Replace with an
|
||||
|
||||
@ -467,6 +467,10 @@ class WorkflowJobTemplate(UnifiedJobTemplate, WorkflowJobOptions, SurveyJobTempl
|
||||
|
||||
class Meta:
|
||||
app_label = 'main'
|
||||
permissions = [
|
||||
('execute_workflowjobtemplate', 'Can run this workflow job template'),
|
||||
('approve_workflowjobtemplate', 'Can approve steps in this workflow job template'),
|
||||
]
|
||||
|
||||
notification_templates_approvals = models.ManyToManyField(
|
||||
"NotificationTemplate",
|
||||
|
||||
@ -126,6 +126,8 @@ def rebuild_role_ancestor_list(reverse, model, instance, pk_set, action, **kwarg
|
||||
|
||||
def sync_superuser_status_to_rbac(instance, **kwargs):
|
||||
'When the is_superuser flag is changed on a user, reflect that in the membership of the System Admnistrator role'
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
return
|
||||
update_fields = kwargs.get('update_fields', None)
|
||||
if update_fields and 'is_superuser' not in update_fields:
|
||||
return
|
||||
@ -137,6 +139,8 @@ def sync_superuser_status_to_rbac(instance, **kwargs):
|
||||
|
||||
def sync_rbac_to_superuser_status(instance, sender, **kwargs):
|
||||
'When the is_superuser flag is false but a user has the System Admin role, update the database to reflect that'
|
||||
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
|
||||
return
|
||||
if kwargs['action'] in ['post_add', 'post_remove', 'post_clear']:
|
||||
new_status_value = bool(kwargs['action'] == 'post_add')
|
||||
if hasattr(instance, 'singleton_name'): # duck typing, role.members.add() vs user.roles.add()
|
||||
|
||||
@ -4,7 +4,6 @@ from prometheus_client.parser import text_string_to_metric_families
|
||||
from awx.main import models
|
||||
from awx.main.analytics.metrics import metrics
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models.rbac import Role
|
||||
|
||||
EXPECTED_VALUES = {
|
||||
'awx_system_info': 1.0,
|
||||
@ -66,7 +65,6 @@ def test_metrics_permissions(get, admin, org_admin, alice, bob, organization):
|
||||
organization.auditor_role.members.add(bob)
|
||||
assert get(get_metrics_view_db_only(), user=bob).status_code == 403
|
||||
|
||||
Role.singleton('system_auditor').members.add(bob)
|
||||
bob.is_system_auditor = True
|
||||
assert get(get_metrics_view_db_only(), user=bob).status_code == 200
|
||||
|
||||
|
||||
@ -385,10 +385,9 @@ def test_list_created_org_credentials(post, get, organization, org_admin, org_me
|
||||
@pytest.mark.django_db
|
||||
def test_list_cannot_order_by_encrypted_field(post, get, organization, org_admin, credentialtype_ssh, order_by):
|
||||
for i, password in enumerate(('abc', 'def', 'xyz')):
|
||||
response = post(reverse('api:credential_list'), {'organization': organization.id, 'name': 'C%d' % i, 'password': password}, org_admin)
|
||||
post(reverse('api:credential_list'), {'organization': organization.id, 'name': 'C%d' % i, 'password': password}, org_admin, expect=400)
|
||||
|
||||
response = get(reverse('api:credential_list'), org_admin, QUERY_STRING='order_by=%s' % order_by, status=400)
|
||||
assert response.status_code == 400
|
||||
get(reverse('api:credential_list'), org_admin, QUERY_STRING='order_by=%s' % order_by, expect=400)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -399,8 +398,7 @@ def test_inputs_cannot_contain_extra_fields(get, post, organization, admin, cred
|
||||
'credential_type': credentialtype_ssh.pk,
|
||||
'inputs': {'invalid_field': 'foo'},
|
||||
}
|
||||
response = post(reverse('api:credential_list'), params, admin)
|
||||
assert response.status_code == 400
|
||||
response = post(reverse('api:credential_list'), params, admin, expect=400)
|
||||
assert "'invalid_field' was unexpected" in response.data['inputs'][0]
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models import Role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -39,7 +38,7 @@ def test_indirect_access_list(get, organization, project, team_factory, user, ad
|
||||
assert len(team_admin_res['summary_fields']['direct_access']) == 1
|
||||
assert len(team_admin_res['summary_fields']['indirect_access']) == 0
|
||||
assert len(admin_res['summary_fields']['direct_access']) == 0
|
||||
assert len(admin_res['summary_fields']['indirect_access']) == 1
|
||||
assert len(admin_res['summary_fields']['indirect_access']) == 0 # decreased to 0 because system admin role no longer exists
|
||||
|
||||
project_admin_entry = project_admin_res['summary_fields']['direct_access'][0]['role']
|
||||
assert project_admin_entry['id'] == project.admin_role.id
|
||||
@ -52,6 +51,3 @@ def test_indirect_access_list(get, organization, project, team_factory, user, ad
|
||||
assert project_admin_team_member_entry['id'] == project.admin_role.id
|
||||
assert project_admin_team_member_entry['team_id'] == project_admin_team.id
|
||||
assert project_admin_team_member_entry['team_name'] == project_admin_team.name
|
||||
|
||||
admin_entry = admin_res['summary_fields']['indirect_access'][0]['role']
|
||||
assert admin_entry['name'] == Role.singleton('system_administrator').name
|
||||
|
||||
@ -3,17 +3,6 @@ import pytest
|
||||
from awx.api.versioning import reverse
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_admin_visible_to_orphaned_users(get, alice):
|
||||
names = set()
|
||||
|
||||
response = get(reverse('api:role_list'), user=alice)
|
||||
for item in response.data['results']:
|
||||
names.add(item['name'])
|
||||
assert 'System Auditor' in names
|
||||
assert 'System Administrator' in names
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize('role,code', [('member_role', 400), ('admin_role', 400), ('inventory_admin_role', 204)])
|
||||
@pytest.mark.parametrize('reversed', [True, False])
|
||||
|
||||
@ -32,7 +32,6 @@ from awx.main.models.organization import (
|
||||
Organization,
|
||||
Team,
|
||||
)
|
||||
from awx.main.models.rbac import Role
|
||||
from awx.main.models.notifications import NotificationTemplate, Notification
|
||||
from awx.main.models.events import (
|
||||
JobEvent,
|
||||
@ -434,7 +433,7 @@ def admin(user):
|
||||
@pytest.fixture
|
||||
def system_auditor(user):
|
||||
u = user('an-auditor', False)
|
||||
Role.singleton('system_auditor').members.add(u)
|
||||
u.is_system_auditor = True
|
||||
return u
|
||||
|
||||
|
||||
|
||||
68
awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py
Normal file
68
awx/main/tests/functional/dab_rbac/test_dab_rbac_api.py
Normal file
@ -0,0 +1,68 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse as django_reverse
|
||||
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models import JobTemplate, Inventory, Organization
|
||||
|
||||
from ansible_base.rbac.models import RoleDefinition
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_managed_roles_created():
|
||||
"Managed RoleDefinitions are created in post_migration signal, we expect to see them here"
|
||||
for cls in (JobTemplate, Inventory):
|
||||
ct = ContentType.objects.get_for_model(cls)
|
||||
rds = list(RoleDefinition.objects.filter(content_type=ct))
|
||||
assert len(rds) > 1
|
||||
assert f'{cls._meta.model_name}-admin' in [rd.name for rd in rds]
|
||||
for rd in rds:
|
||||
assert rd.managed is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_custom_read_role(admin_user, post):
|
||||
rd_url = django_reverse('roledefinition-list')
|
||||
resp = post(
|
||||
url=rd_url, data={"name": "read role made for test", "content_type": "awx.inventory", "permissions": ['view_inventory']}, user=admin_user, expect=201
|
||||
)
|
||||
rd_id = resp.data['id']
|
||||
rd = RoleDefinition.objects.get(id=rd_id)
|
||||
assert rd.content_type == ContentType.objects.get_for_model(Inventory)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_assign_managed_role(admin_user, alice, rando, inventory, post):
|
||||
rd = RoleDefinition.objects.get(name='inventory-admin')
|
||||
rd.give_permission(alice, inventory)
|
||||
# Now that alice has full permissions to the inventory, she will give rando permission
|
||||
url = django_reverse('roleuserassignment-list')
|
||||
post(url=url, data={"user": rando.id, "role_definition": rd.id, "object_id": inventory.id}, user=alice, expect=201)
|
||||
assert rando.has_obj_perm(inventory, 'change') is True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_assign_custom_delete_role(admin_user, rando, inventory, delete, patch):
|
||||
rd, _ = RoleDefinition.objects.get_or_create(
|
||||
name='inventory-delete', permissions=['delete_inventory', 'view_inventory'], content_type=ContentType.objects.get_for_model(Inventory)
|
||||
)
|
||||
rd.give_permission(rando, inventory)
|
||||
inv_id = inventory.pk
|
||||
inv_url = reverse('api:inventory_detail', kwargs={'pk': inv_id})
|
||||
patch(url=inv_url, data={"description": "new"}, user=rando, expect=403)
|
||||
delete(url=inv_url, user=rando, expect=202)
|
||||
assert Inventory.objects.get(id=inv_id).pending_deletion
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_assign_custom_add_role(admin_user, rando, organization, post):
|
||||
rd, _ = RoleDefinition.objects.get_or_create(
|
||||
name='inventory-add', permissions=['add_inventory', 'view_organization'], content_type=ContentType.objects.get_for_model(Organization)
|
||||
)
|
||||
rd.give_permission(rando, organization)
|
||||
url = reverse('api:inventory_list')
|
||||
r = post(url=url, data={'name': 'abc', 'organization': organization.id}, user=rando, expect=201)
|
||||
inv_id = r.data['id']
|
||||
inventory = Inventory.objects.get(id=inv_id)
|
||||
assert rando.has_obj_perm(inventory, 'change')
|
||||
23
awx/main/tests/functional/dab_rbac/test_translation_layer.py
Normal file
23
awx/main/tests/functional/dab_rbac/test_translation_layer.py
Normal file
@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models.rbac import get_role_from_object_role
|
||||
|
||||
from ansible_base.rbac.models import RoleUserAssignment
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
'role_name',
|
||||
['execution_environment_admin_role', 'project_admin_role', 'admin_role', 'auditor_role', 'read_role', 'execute_role', 'notification_admin_role'],
|
||||
)
|
||||
def test_round_trip_roles(organization, rando, role_name):
|
||||
"""
|
||||
Make an assignment with the old-style role,
|
||||
get the equivelent new role
|
||||
get the old role again
|
||||
"""
|
||||
getattr(organization, role_name).members.add(rando)
|
||||
assignment = RoleUserAssignment.objects.get(user=rando)
|
||||
print(assignment.role_definition.name)
|
||||
old_role = get_role_from_object_role(assignment.object_role)
|
||||
assert old_role.id == getattr(organization, role_name).id
|
||||
@ -104,11 +104,13 @@ class TestRolesAssociationEntries:
|
||||
else:
|
||||
assert len(entry_qs) == 1
|
||||
# unfortunate, the original creation does _not_ set a real is_auditor field
|
||||
assert 'is_system_auditor' not in json.loads(entry_qs[0].changes)
|
||||
assert 'is_system_auditor' not in json.loads(entry_qs[0].changes) # NOTE: if this fails, see special note
|
||||
# special note - if system auditor flag is moved to user model then we expect this assertion to be changed
|
||||
# make sure that an extra entry is not created, expectation for count would change to 1
|
||||
if value:
|
||||
auditor_changes = json.loads(entry_qs[1].changes)
|
||||
assert auditor_changes['object2'] == 'user'
|
||||
assert auditor_changes['object2_pk'] == u.pk
|
||||
entry = entry_qs[1]
|
||||
assert json.loads(entry.changes) == {'is_system_auditor': [False, True]}
|
||||
assert entry.object1 == 'user'
|
||||
|
||||
def test_user_no_op_api(self, system_auditor):
|
||||
as_ct = ActivityStream.objects.count()
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
|
||||
# AWX context managers for testing
|
||||
from awx.main.models.rbac import batch_role_ancestor_rebuilding
|
||||
from awx.main.signals import disable_activity_stream, disable_computed_fields, update_inventory_computed_fields
|
||||
|
||||
# AWX models
|
||||
@ -10,15 +9,6 @@ from awx.main.models import ActivityStream, Job
|
||||
from awx.main.tests.functional import immediate_on_commit
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rbac_batch_rebuilding(rando, organization):
|
||||
with batch_role_ancestor_rebuilding():
|
||||
organization.admin_role.members.add(rando)
|
||||
inventory = organization.inventories.create(name='test-inventory')
|
||||
assert rando not in inventory.admin_role
|
||||
assert rando in inventory.admin_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_disable_activity_stream():
|
||||
with disable_activity_stream():
|
||||
|
||||
@ -3,7 +3,7 @@ import pytest
|
||||
|
||||
from django.db import transaction
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.models.rbac import Role, ROLE_SINGLETON_SYSTEM_ADMINISTRATOR
|
||||
from awx.main.models.rbac import Role
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -31,8 +31,6 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
||||
'Users can see all roles they have access to, but not all roles'
|
||||
this_user = user('user-test_get_roles_list_user')
|
||||
organization.member_role.members.add(this_user)
|
||||
custom_role = Role.objects.create(role_field='custom_role-test_get_roles_list_user')
|
||||
organization.member_role.children.add(custom_role)
|
||||
|
||||
url = reverse('api:role_list')
|
||||
response = get(url, this_user)
|
||||
@ -46,10 +44,8 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
||||
for r in roles['results']:
|
||||
role_hash[r['id']] = r
|
||||
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id in role_hash
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert organization.member_role.id in role_hash
|
||||
assert custom_role.id in role_hash
|
||||
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash
|
||||
@ -57,7 +53,8 @@ def test_get_roles_list_user(organization, inventory, team, get, user):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_roles_visibility(get, organization, project, admin, alice, bob):
|
||||
Role.singleton('system_auditor').members.add(alice)
|
||||
alice.is_system_auditor = True
|
||||
alice.save()
|
||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=alice).data['count'] == 1
|
||||
assert get(reverse('api:role_list') + '?id=%d' % project.update_role.id, user=bob).data['count'] == 0
|
||||
@ -67,7 +64,8 @@ def test_roles_visibility(get, organization, project, admin, alice, bob):
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_roles_filter_visibility(get, organization, project, admin, alice, bob):
|
||||
Role.singleton('system_auditor').members.add(alice)
|
||||
alice.is_system_auditor = True
|
||||
alice.save()
|
||||
project.update_role.members.add(admin)
|
||||
|
||||
assert get(reverse('api:user_roles_list', kwargs={'pk': admin.id}) + '?id=%d' % project.update_role.id, user=admin).data['count'] == 1
|
||||
@ -105,15 +103,6 @@ def test_cant_delete_role(delete, admin, inventory):
|
||||
#
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_user_roles_list(get, admin):
|
||||
url = reverse('api:user_roles_list', kwargs={'pk': admin.id})
|
||||
response = get(url, admin)
|
||||
assert response.status_code == 200
|
||||
roles = response.data
|
||||
assert roles['count'] > 0 # 'system_administrator' role if nothing else
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_view_other_user_roles(organization, inventory, team, get, alice, bob):
|
||||
'Users can see roles for other users, but only the roles that that user has access to see as well'
|
||||
@ -141,7 +130,6 @@ def test_user_view_other_user_roles(organization, inventory, team, get, alice, b
|
||||
|
||||
assert organization.admin_role.id in role_hash
|
||||
assert custom_role.id not in role_hash # doesn't show up in the user roles list, not an explicit grant
|
||||
assert Role.singleton(ROLE_SINGLETON_SYSTEM_ADMINISTRATOR).id not in role_hash
|
||||
assert inventory.admin_role.id not in role_hash
|
||||
assert team.member_role.id not in role_hash # alice can't see this
|
||||
|
||||
|
||||
@ -1,213 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from awx.main.models import (
|
||||
Role,
|
||||
Organization,
|
||||
Project,
|
||||
)
|
||||
from awx.main.fields import update_role_parentage_for_instance
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_children(organization, alice):
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
|
||||
assert alice not in organization.admin_role
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
||||
A.children.add(B)
|
||||
assert alice not in organization.admin_role
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
||||
A.children.add(organization.admin_role)
|
||||
assert alice in organization.admin_role
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 1
|
||||
A.children.remove(organization.admin_role)
|
||||
assert alice not in organization.admin_role
|
||||
B.children.add(organization.admin_role)
|
||||
assert alice in organization.admin_role
|
||||
B.children.remove(organization.admin_role)
|
||||
assert alice not in organization.admin_role
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
||||
|
||||
# We've had the case where our pre/post save init handlers in our field descriptors
|
||||
# end up creating a ton of role objects because of various not-so-obvious issues
|
||||
assert Role.objects.count() < 50
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_inheritance_by_parents(organization, alice):
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
|
||||
assert alice not in organization.admin_role
|
||||
B.parents.add(A)
|
||||
assert alice not in organization.admin_role
|
||||
organization.admin_role.parents.add(A)
|
||||
assert alice in organization.admin_role
|
||||
organization.admin_role.parents.remove(A)
|
||||
assert alice not in organization.admin_role
|
||||
organization.admin_role.parents.add(B)
|
||||
assert alice in organization.admin_role
|
||||
organization.admin_role.parents.remove(B)
|
||||
assert alice not in organization.admin_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accessible_objects(organization, alice, bob):
|
||||
A = Role.objects.create()
|
||||
A.members.add(alice)
|
||||
B = Role.objects.create()
|
||||
B.members.add(alice)
|
||||
B.members.add(bob)
|
||||
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 0
|
||||
assert Organization.accessible_objects(bob, 'admin_role').count() == 0
|
||||
A.children.add(organization.admin_role)
|
||||
assert Organization.accessible_objects(alice, 'admin_role').count() == 1
|
||||
assert Organization.accessible_objects(bob, 'admin_role').count() == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_team_symantics(organization, team, alice):
|
||||
assert alice not in organization.auditor_role
|
||||
team.member_role.children.add(organization.auditor_role)
|
||||
assert alice not in organization.auditor_role
|
||||
team.member_role.members.add(alice)
|
||||
assert alice in organization.auditor_role
|
||||
team.member_role.members.remove(alice)
|
||||
assert alice not in organization.auditor_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_field_adjustments(organization, inventory, team, alice):
|
||||
'Ensures the auto role reparenting is working correctly through non m2m fields'
|
||||
org2 = Organization.objects.create(name='Org 2', description='org 2')
|
||||
org2.admin_role.members.add(alice)
|
||||
assert alice not in inventory.admin_role
|
||||
inventory.organization = org2
|
||||
inventory.save()
|
||||
assert alice in inventory.admin_role
|
||||
inventory.organization = organization
|
||||
inventory.save()
|
||||
assert alice not in inventory.admin_role
|
||||
# assert False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_implicit_deletes(alice):
|
||||
'Ensures implicit resources and roles delete themselves'
|
||||
delorg = Organization.objects.create(name='test-org')
|
||||
child = Role.objects.create()
|
||||
child.parents.add(delorg.admin_role)
|
||||
delorg.admin_role.members.add(alice)
|
||||
|
||||
admin_role_id = delorg.admin_role.id
|
||||
auditor_role_id = delorg.auditor_role.id
|
||||
|
||||
assert child.ancestors.count() > 1
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 1
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 1
|
||||
n_alice_roles = alice.roles.count()
|
||||
n_system_admin_children = Role.singleton('system_administrator').children.count()
|
||||
|
||||
delorg.delete()
|
||||
|
||||
assert Role.objects.filter(id=admin_role_id).count() == 0
|
||||
assert Role.objects.filter(id=auditor_role_id).count() == 0
|
||||
assert alice.roles.count() == (n_alice_roles - 1)
|
||||
assert Role.singleton('system_administrator').children.count() == (n_system_admin_children - 1)
|
||||
assert child.ancestors.count() == 1
|
||||
assert child.ancestors.all()[0] == child
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_content_object(user):
|
||||
'Ensure our content_object stuf seems to be working'
|
||||
|
||||
org = Organization.objects.create(name='test-org')
|
||||
assert org.admin_role.content_object.id == org.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_hierarchy_rebuilding_multi_path():
|
||||
'Tests a subdtle cases around role hierarchy rebuilding when you have multiple paths to the same role of different length'
|
||||
|
||||
X = Role.objects.create()
|
||||
A = Role.objects.create()
|
||||
B = Role.objects.create()
|
||||
C = Role.objects.create()
|
||||
D = Role.objects.create()
|
||||
|
||||
A.children.add(B)
|
||||
A.children.add(D)
|
||||
B.children.add(C)
|
||||
C.children.add(D)
|
||||
|
||||
assert A.is_ancestor_of(D)
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
X.children.add(A)
|
||||
|
||||
assert X.is_ancestor_of(D) is True
|
||||
|
||||
X.children.remove(A)
|
||||
|
||||
# This can be the stickler, the rebuilder needs to ensure that D's role
|
||||
# hierarchy is built after both A and C are updated.
|
||||
assert X.is_ancestor_of(D) is False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_auto_parenting():
|
||||
org1 = Organization.objects.create(name='org1')
|
||||
org2 = Organization.objects.create(name='org2')
|
||||
|
||||
prj1 = Project.objects.create(name='prj1')
|
||||
prj2 = Project.objects.create(name='prj2')
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj1.organization = org1
|
||||
prj1.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj2.organization = org1
|
||||
prj2.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj1.organization = org2
|
||||
prj1.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
|
||||
prj2.organization = org2
|
||||
prj2.save()
|
||||
|
||||
assert org1.admin_role.is_ancestor_of(prj1.admin_role) is False
|
||||
assert org1.admin_role.is_ancestor_of(prj2.admin_role) is False
|
||||
assert org2.admin_role.is_ancestor_of(prj1.admin_role)
|
||||
assert org2.admin_role.is_ancestor_of(prj2.admin_role)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_parents_keeps_teams(team, project):
|
||||
project.update_role.parents.add(team.member_role)
|
||||
assert list(Project.accessible_objects(team.member_role, 'update_role')) == [project] # test prep sanity check
|
||||
update_role_parentage_for_instance(project)
|
||||
assert list(Project.accessible_objects(team.member_role, 'update_role')) == [project] # actual assertion
|
||||
@ -4,7 +4,7 @@ import pytest
|
||||
from awx.api.versioning import reverse
|
||||
from awx.main.access import BaseAccess, JobTemplateAccess, ScheduleAccess
|
||||
from awx.main.models.jobs import JobTemplate
|
||||
from awx.main.models import Project, Organization, Inventory, Schedule, User
|
||||
from awx.main.models import Project, Organization, Schedule
|
||||
|
||||
|
||||
@mock.patch.object(BaseAccess, 'check_license', return_value=None)
|
||||
@ -283,48 +283,3 @@ class TestProjectOrganization:
|
||||
assert org_admin not in jt.admin_role
|
||||
patch(url=jt.get_absolute_url(), data={'project': project.id}, user=admin_user, expect=200)
|
||||
assert org_admin in jt.admin_role
|
||||
|
||||
def test_inventory_read_transfer_direct(self, patch):
|
||||
orgs = []
|
||||
invs = []
|
||||
admins = []
|
||||
for i in range(2):
|
||||
org = Organization.objects.create(name='org{}'.format(i))
|
||||
org_admin = User.objects.create(username='user{}'.format(i))
|
||||
inv = Inventory.objects.create(organization=org, name='inv{}'.format(i))
|
||||
org.auditor_role.members.add(org_admin)
|
||||
|
||||
orgs.append(org)
|
||||
admins.append(org_admin)
|
||||
invs.append(inv)
|
||||
|
||||
jt = JobTemplate.objects.create(name='foo', inventory=invs[0])
|
||||
assert admins[0] in jt.read_role
|
||||
assert admins[1] not in jt.read_role
|
||||
|
||||
jt.inventory = invs[1]
|
||||
jt.save(update_fields=['inventory'])
|
||||
assert admins[0] not in jt.read_role
|
||||
assert admins[1] in jt.read_role
|
||||
|
||||
def test_inventory_read_transfer_indirect(self, patch):
|
||||
orgs = []
|
||||
admins = []
|
||||
for i in range(2):
|
||||
org = Organization.objects.create(name='org{}'.format(i))
|
||||
org_admin = User.objects.create(username='user{}'.format(i))
|
||||
org.auditor_role.members.add(org_admin)
|
||||
|
||||
orgs.append(org)
|
||||
admins.append(org_admin)
|
||||
|
||||
inv = Inventory.objects.create(organization=orgs[0], name='inv{}'.format(i))
|
||||
|
||||
jt = JobTemplate.objects.create(name='foo', inventory=inv)
|
||||
assert admins[0] in jt.read_role
|
||||
assert admins[1] not in jt.read_role
|
||||
|
||||
inv.organization = orgs[1]
|
||||
inv.save(update_fields=['organization'])
|
||||
assert admins[0] not in jt.read_role
|
||||
assert admins[1] in jt.read_role
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
from awx.main.migrations import _rbac as rbac
|
||||
from awx.main.models import UnifiedJobTemplate, InventorySource, Inventory, JobTemplate, Project, Organization, User
|
||||
from awx.main.models import UnifiedJobTemplate, InventorySource, Inventory, JobTemplate, Project, Organization
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -49,27 +47,3 @@ def test_implied_organization_subquery_job_template():
|
||||
assert jt.test_field is None
|
||||
else:
|
||||
assert jt.test_field == jt.project.organization_id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_give_explicit_inventory_permission():
|
||||
dual_admin = User.objects.create(username='alice')
|
||||
inv_admin = User.objects.create(username='bob')
|
||||
inv_org = Organization.objects.create(name='inv-org')
|
||||
proj_org = Organization.objects.create(name='proj-org')
|
||||
|
||||
inv_org.admin_role.members.add(inv_admin, dual_admin)
|
||||
proj_org.admin_role.members.add(dual_admin)
|
||||
|
||||
proj = Project.objects.create(name="test-proj", organization=proj_org)
|
||||
inv = Inventory.objects.create(name='test-inv', organization=inv_org)
|
||||
|
||||
jt = JobTemplate.objects.create(name='foo', project=proj, inventory=inv)
|
||||
|
||||
assert dual_admin in jt.admin_role
|
||||
|
||||
rbac.restore_inventory_admins(apps, None)
|
||||
|
||||
assert inv_admin in jt.admin_role.members.all()
|
||||
assert dual_admin not in jt.admin_role.members.all()
|
||||
assert dual_admin in jt.admin_role
|
||||
|
||||
@ -92,7 +92,7 @@ def test_team_accessible_by(team, user, project):
|
||||
u = user('team_member', False)
|
||||
|
||||
team.member_role.children.add(project.use_role)
|
||||
assert list(Project.accessible_objects(team.member_role, 'read_role')) == [project]
|
||||
assert list(Project.accessible_objects(team, 'read_role')) == [project]
|
||||
assert u not in project.read_role
|
||||
|
||||
team.member_role.members.add(u)
|
||||
@ -104,7 +104,7 @@ def test_team_accessible_objects(team, user, project):
|
||||
u = user('team_member', False)
|
||||
|
||||
team.member_role.children.add(project.use_role)
|
||||
assert len(Project.accessible_objects(team.member_role, 'read_role')) == 1
|
||||
assert len(Project.accessible_objects(team, 'read_role')) == 1
|
||||
assert not Project.accessible_objects(u, 'read_role')
|
||||
|
||||
team.member_role.members.add(u)
|
||||
|
||||
@ -4,7 +4,7 @@ from unittest import mock
|
||||
from django.test import TransactionTestCase
|
||||
|
||||
from awx.main.access import UserAccess, RoleAccess, TeamAccess
|
||||
from awx.main.models import User, Organization, Inventory, Role
|
||||
from awx.main.models import User, Organization, Inventory
|
||||
|
||||
|
||||
class TestSysAuditorTransactional(TransactionTestCase):
|
||||
@ -18,7 +18,7 @@ class TestSysAuditorTransactional(TransactionTestCase):
|
||||
|
||||
def test_auditor_caching(self):
|
||||
rando = self.rando()
|
||||
with self.assertNumQueries(1):
|
||||
with self.assertNumQueries(2):
|
||||
v = rando.is_system_auditor
|
||||
assert not v
|
||||
with self.assertNumQueries(0):
|
||||
@ -153,34 +153,3 @@ def test_org_admin_cannot_delete_member_attached_to_other_group(org_admin, org_m
|
||||
access = UserAccess(org_admin)
|
||||
other_org.member_role.members.add(org_member)
|
||||
assert not access.can_delete(org_member)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('reverse', (True, False))
|
||||
@pytest.mark.django_db
|
||||
def test_consistency_of_is_superuser_flag(reverse):
|
||||
users = [User.objects.create(username='rando_{}'.format(i)) for i in range(2)]
|
||||
for u in users:
|
||||
assert u.is_superuser is False
|
||||
|
||||
system_admin = Role.singleton('system_administrator')
|
||||
if reverse:
|
||||
for u in users:
|
||||
u.roles.add(system_admin)
|
||||
else:
|
||||
system_admin.members.add(*[u.id for u in users]) # like .add(42, 54)
|
||||
|
||||
for u in users:
|
||||
u.refresh_from_db()
|
||||
assert u.is_superuser is True
|
||||
|
||||
users[0].roles.clear()
|
||||
for u in users:
|
||||
u.refresh_from_db()
|
||||
assert users[0].is_superuser is False
|
||||
assert users[1].is_superuser is True
|
||||
|
||||
system_admin.members.clear()
|
||||
|
||||
for u in users:
|
||||
u.refresh_from_db()
|
||||
assert u.is_superuser is False
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_admin_not_member(team):
|
||||
"""Test to ensure we don't add admin_role as a parent to team.member_role, as
|
||||
this creates a cycle with organization administration, which we've decided
|
||||
to remove support for
|
||||
|
||||
(2016-06-16) I think this might have been resolved. I'm asserting
|
||||
this to be true in the mean time.
|
||||
"""
|
||||
|
||||
assert team.admin_role.is_ancestor_of(team.member_role) is True
|
||||
@ -355,6 +355,7 @@ INSTALLED_APPS = [
|
||||
'ansible_base.rest_filters',
|
||||
'ansible_base.jwt_consumer',
|
||||
'ansible_base.resource_registry',
|
||||
'ansible_base.rbac',
|
||||
]
|
||||
|
||||
|
||||
@ -497,6 +498,12 @@ CACHES = {'default': {'BACKEND': 'awx.main.cache.AWXRedisCache', 'LOCATION': 'un
|
||||
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
|
||||
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
|
||||
SOCIAL_AUTH_USER_MODEL = 'auth.User'
|
||||
ROLE_SINGLETON_USER_RELATIONSHIP = ''
|
||||
ROLE_SINGLETON_TEAM_RELATIONSHIP = ''
|
||||
|
||||
# We want to short-circuit RBAC methods to get permission to system admins and auditors
|
||||
ROLE_BYPASS_SUPERUSER_FLAGS = ['is_superuser']
|
||||
ROLE_BYPASS_ACTION_FLAGS = {'view': 'is_system_auditor'}
|
||||
|
||||
_SOCIAL_AUTH_PIPELINE_BASE = (
|
||||
'social_core.pipeline.social_auth.social_details',
|
||||
@ -1121,11 +1128,11 @@ METRICS_SUBSYSTEM_CONFIG = {
|
||||
ANSIBLE_BASE_TEAM_MODEL = 'main.Team'
|
||||
ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization'
|
||||
ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = 'awx.resource_api'
|
||||
ANSIBLE_BASE_PERMISSION_MODEL = 'main.Permission'
|
||||
|
||||
from ansible_base.lib import dynamic_config # noqa: E402
|
||||
|
||||
settings_file = os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py')
|
||||
include(settings_file)
|
||||
include(os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py'))
|
||||
|
||||
# Add a postfix to the API URL patterns
|
||||
# example if set to '' API pattern will be /api
|
||||
@ -1134,3 +1141,25 @@ OPTIONAL_API_URLPATTERN_PREFIX = ''
|
||||
|
||||
# Use AWX base view, to give 401 on unauthenticated requests
|
||||
ANSIBLE_BASE_CUSTOM_VIEW_PARENT = 'awx.api.generics.APIView'
|
||||
|
||||
# Settings for the ansible_base RBAC system
|
||||
|
||||
# Settings for the RBAC system, override as necessary in app
|
||||
ANSIBLE_BASE_ROLE_PRECREATE = {
|
||||
'object_admin': '{cls._meta.model_name}-admin',
|
||||
'org_admin': 'organization-admin',
|
||||
'org_children': 'organization-{cls._meta.model_name}-admin',
|
||||
'special': '{cls._meta.model_name}-{action}',
|
||||
}
|
||||
|
||||
# Use the new Gateway RBAC system for evaluations? You should. We will remove the old system soon.
|
||||
ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED = True
|
||||
|
||||
# Permissions a user will get when creating a new item
|
||||
ANSIBLE_BASE_CREATOR_DEFAULTS = ['change', 'delete', 'execute', 'use', 'adhoc', 'approve', 'update', 'view']
|
||||
|
||||
# This is a stopgap, will delete after resource registry integration
|
||||
ANSIBLE_BASE_SERVICE_PREFIX = "awx"
|
||||
|
||||
# system username for django-ansible-base
|
||||
SYSTEM_USERNAME = None
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import path, re_path, include
|
||||
from django.urls import re_path, include, path
|
||||
|
||||
from ansible_base.lib.dynamic_config.dynamic_urls import api_urls, api_version_urls, root_urls
|
||||
|
||||
from ansible_base.resource_registry.urls import urlpatterns as resource_api_urls
|
||||
|
||||
@ -22,7 +24,10 @@ def get_urlpatterns(prefix=None):
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
path(f'api{prefix}v2/', include(resource_api_urls)),
|
||||
# path(f'api{prefix}v2/', include(resource_api_urls)),
|
||||
path('api/v2/', include(api_version_urls)),
|
||||
path('api/', include(api_urls)),
|
||||
path('', include(root_urls)),
|
||||
re_path(r'^sso/', include('awx.sso.urls', namespace='sso')),
|
||||
re_path(r'^sso/', include('social_django.urls', namespace='social')),
|
||||
re_path(r'^(?:api/)?400.html$', handle_400),
|
||||
|
||||
@ -18,9 +18,9 @@ def test_grant_organization_permission(run_module, admin_user, organization, sta
|
||||
assert not result.get('failed', False), result.get('msg', result)
|
||||
|
||||
if state == 'present':
|
||||
assert rando in organization.execute_role
|
||||
assert rando in organization.admin_role
|
||||
else:
|
||||
assert rando not in organization.execute_role
|
||||
assert rando not in organization.admin_role
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
161
docs/rbac.md
161
docs/rbac.md
@ -1,166 +1,13 @@
|
||||
# Role-Based Access Control (RBAC)
|
||||
|
||||
This document describes the RBAC implementation of the AWX Software.
|
||||
The intended audience of this document is the AWX developer.
|
||||
The Role-Based Access Control system has been moved to the django-ansible-base library.
|
||||
|
||||
https://github.com/ansible/django-ansible-base
|
||||
|
||||
## Overview
|
||||
|
||||
### RBAC - System Basics
|
||||
|
||||
There are three main concepts to be familiar with: Roles, Resources, and Users.
|
||||
Users can be members of a role, which gives them certain access to any
|
||||
resources associated with that role, or any resources associated with "descendent"
|
||||
roles.
|
||||
|
||||
For example, if I have an organization named "MyCompany" and I want to allow
|
||||
two people, "Alice", and "Bob", access to manage all of the settings associated
|
||||
with that organization, I'd make them both members of the organization's `admin_role`.
|
||||
|
||||
It is often the case that you have many Roles in a system, and you want some
|
||||
roles to include all of the capabilities of other roles. For example, you may
|
||||
want a System Administrator to have access to everything that an Organization
|
||||
Administrator has access to, who has everything that a Project Administrator
|
||||
has access to, and so on. We refer to this concept as the 'Role Hierarchy', and
|
||||
is represented by allowing roles to have "Parent Roles". Any permission that a
|
||||
role has is implicitly granted to any parent roles (or parents of those
|
||||
parents, and so on). Of course roles can have more than one parent, and
|
||||
capabilities are implicitly granted to all parents. (Technically speaking, this
|
||||
forms a directional acyclic graph instead of a strict hierarchy, but the
|
||||
concept should remain intuitive.)
|
||||
Illustrations from the old RBAC system, before the move to django-ansible-base.
|
||||
|
||||

|
||||
|
||||
|
||||
### Implementation Overview
|
||||
|
||||
The RBAC system allows you to create and layer roles for controlling access to resources. Any Django Model can
|
||||
be made into a resource in the RBAC system by using the `ResourceMixin`. Once a model is accessible as a resource, you can
|
||||
extend the model definition to have specific roles using the `ImplicitRoleField`. Within the declaration of
|
||||
this role field you can also specify any parents the role may have, and the RBAC system will take care of
|
||||
all of the appropriate ancestral binding that takes place behind the scenes to ensure that the model you've declared
|
||||
is kept up to date as the relations in your model change.
|
||||
|
||||
### Roles
|
||||
|
||||
Roles are defined for a resource. If a role has any parents, these parents will be considered when determining
|
||||
what roles are checked when accessing a resource.
|
||||
|
||||
ResourceA
|
||||
|-- AdminRole
|
||||
|
||||
ResourceB
|
||||
| -- AdminRole
|
||||
|-- parent = ResourceA.AdminRole
|
||||
|
||||
When a user attempts to access ResourceB, we will check for their access using the set of all unique roles, including the parents.
|
||||
|
||||
ResourceA.AdminRole, ResourceB.AdminRole
|
||||
|
||||
This would provide any members of the above roles with access to ResourceB.
|
||||
|
||||
#### Singleton Role
|
||||
|
||||
There is a special case _Singleton Role_ that you can create. This type of role is for system-wide roles.
|
||||
|
||||
### Models
|
||||
|
||||
The RBAC system defines a few new models. These models represent the underlying RBAC implementation and generally will be abstracted away from your daily development tasks by the implicit fields and mixins.
|
||||
|
||||
#### `Role`
|
||||
|
||||
`Role` defines a single role within the RBAC implementation. It encapsulates the `ancestors`, `parents`, and `members` for a role. This model is intentionally kept dumb and it has no explicit knowledge of a `Resource`. The `Role` model (get it?), defines some methods that aid in the granting and creation of roles.
|
||||
|
||||
##### `visible_roles(cls, user)`
|
||||
|
||||
`visible_roles` is a class method that will look up all of the `Role` instances a user can "see". This includes any roles the user is a direct descendent of as well as any ancestor roles.
|
||||
|
||||
##### `singleton(cls, name)`
|
||||
|
||||
The `singleton` class method is a helper method on the `Role` model that helps in the creation of singleton roles. It will return the role by name if it already exists or create and return the new role in the case it does not.
|
||||
|
||||
##### `get_absolute_url(self)`
|
||||
|
||||
`get_absolute_url` returns the consumable URL endpoint for the `Role`.
|
||||
|
||||
##### `rebuild_role_ancestor_list(self)`
|
||||
|
||||
`rebuild_role_ancestor_list` will rebuild the current role ancestry that is stored in the `ancestors` field of a `Role`. This is called for you by `save` and different Django signals.
|
||||
|
||||
##### `is_ancestor_of(self, role)`
|
||||
|
||||
`is_ancestor_of` returns if the given `role` is an ancestor of the current `Role` instance.
|
||||
|
||||
##### `user in role`
|
||||
|
||||
You may use the `user in some_role` syntax to check and see if the specified
|
||||
user is a member of the given role, **or** a member of any ancestor role.
|
||||
|
||||
### Fields
|
||||
|
||||
#### `ImplicitRoleField`
|
||||
|
||||
`ImplicitRoleField` fields are declared on your model. They provide the definition of grantable roles for accessing your resource. You may (and should) use the `parent_role` parameter to specify any parent roles that should inherit privileges implied by the role.
|
||||
|
||||
`parent_role` is the link to any parent roles you want considered when a user
|
||||
is requesting access to your resource. A `parent_role` can be declared as a
|
||||
single string, `"parent.read_role"`, or a list of many roles,
|
||||
`['parentA.read_role', 'parentB.read_role']` which will make each listed role a parent. You can also use the syntax
|
||||
`[('parentA.read_role', 'parentB.read_role'), 'parentC.read_role']` to make
|
||||
`(parentA.read_role OR parentB.read_role) AND 'parentC.read_role` parents (so `parentB.read_role` will be added only if `parentA.read_role` was `None`).
|
||||
If any listed role can't be evaluated (for example if there are `None` components in the path), then they are simply ignored until the value of the field changes.
|
||||
|
||||
|
||||
### Mixins
|
||||
|
||||
#### `ResourceMixin`
|
||||
|
||||
By mixing in the `ResourceMixin` to your model, you are turning your model in to a resource in the eyes of the RBAC implementation. Your model will gain the helper methods that aid in the checking the access a users roles provides them to your resource.
|
||||
|
||||
##### `accessible_objects(cls, user, role_field)`
|
||||
|
||||
`accessible_objects` is a class method to use instead of `Model.objects`. This method will restrict the query of objects to only those that the user has access to - specifically those objects which the user is a member of the specified role (either directly or indirectly).
|
||||
|
||||
```python
|
||||
objects = MyModel.accessible_objects(user, 'admin_role')
|
||||
objects.filter(name__istartswith='december')
|
||||
```
|
||||
|
||||
##### `accessible_pk_qs(cls, user, role_field)`
|
||||
|
||||
`accessible_pk_qs` returns a queryset of ids that match the same role filter as `accessible_objects`.
|
||||
A key difference is that this is more performant to use in subqueries when filtering related models.
|
||||
|
||||
Say that another model, `YourModel` has a ForeignKey reference to `MyModel` via a field `my_model`,
|
||||
and you want to return all instances of `YourModel` that have a visible related `MyModel`.
|
||||
The best way to do this is:
|
||||
|
||||
```python
|
||||
YourModel.filter(my_model=MyModel.accessible_pk_qs(user, 'admin_role'))
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
After exploring the _Overview_, the usage of the RBAC implementation in your code should feel unobtrusive and natural.
|
||||
|
||||
```python
|
||||
# make your model a Resource
|
||||
class Document(Model, ResourceMixin):
|
||||
...
|
||||
# declare your new role
|
||||
readonly_role = ImplicitRoleField()
|
||||
```
|
||||
|
||||
Now that your model is a resource and has a `Role` defined, you can begin to access the helper methods provided to you by the `ResourceMixin` for checking a user's access to your resource. Here is the output of a Python REPL session:
|
||||
|
||||
```python
|
||||
# we've created some documents and a user
|
||||
>>> document = Document.objects.filter(pk=1)
|
||||
>>> user = User.objects.first()
|
||||
>>> user in document.readonly_role
|
||||
False # not accessible by default
|
||||
>>> document.readonly_role.members.add(user)
|
||||
>>> user in document.readonly_role
|
||||
True # now it is accessible
|
||||
>>> user in document.readonly_role
|
||||
False # my role does not have admin permission
|
||||
```
|
||||
|
||||
@ -5,4 +5,4 @@ git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner
|
||||
# specifically need https://github.com/robgolding/django-radius/pull/27
|
||||
git+https://github.com/ansible/django-radius.git@develop#egg=django-radius
|
||||
git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml
|
||||
django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry]
|
||||
django-ansible-base @ git+https://github.com/alancoding/django-ansible-base@django_permissions#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user