diff --git a/ansible_base/lib/dynamic_config/dynamic_settings.py b/ansible_base/lib/dynamic_config/dynamic_settings.py index d256e156a..2c18315d1 100644 --- a/ansible_base/lib/dynamic_config/dynamic_settings.py +++ b/ansible_base/lib/dynamic_config/dynamic_settings.py @@ -145,13 +145,8 @@ if 'ansible_base.rbac' in INSTALLED_APPS: - # 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}', - } + # The settings-based specification of managed roles from DAB RBAC vendored ones + ANSIBLE_BASE_MANAGED_ROLE_REGISTRY = {} # Permissions a user will get when creating a new item ANSIBLE_BASE_CREATOR_DEFAULTS = ['add', 'change', 'delete', 'view'] diff --git a/ansible_base/rbac/managed.py b/ansible_base/rbac/managed.py new file mode 100644 index 000000000..1e7e6539c --- /dev/null +++ b/ansible_base/rbac/managed.py @@ -0,0 +1,167 @@ +import logging +from typing import Optional, Type + +from django.conf import settings +from django.db.models import Model +from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_noop + +logger = logging.getLogger('ansible_base.rbac.managed') + + +class ManagedRoleConstructor: + """Subclasses must define attributes, or override methods that use attribues + - name + - description + - model_name + - permission_list + """ + + def __init__(self, overrides=None): + if overrides: + for key, value in overrides.items(): + setattr(self, key, value) + + def get_model(self, apps): + "It is intended that this will error if subclass did not set model_name" + if self.model_name is None: + return None + return apps.get_model(self.model_name) + + def get_permissions(self, apps) -> set[str]: + "It is intended that this will error if subclass did not set permission_list" + return self.permission_list + + def get_translated_name(self) -> str: + return _(self.name) + + def get_content_type(self, apps): + model = self.get_model(apps) + if model is None: + return None + content_type_cls = apps.get_model('contenttypes', 'ContentType') + return content_type_cls.objects.get_for_model(model) + + def get_or_create(self, apps): + "Create from a list of text-type permissions and do validation" + role_definition_cls = apps.get_model('dab_rbac', 'RoleDefinition') + defaults = { + 'description': self.description, + 'content_type': self.get_content_type(apps), + 'managed': True, + } + rd, created = role_definition_cls.objects.get_or_create(name=self.name, defaults=defaults) + + if created: + permissions = self.get_permissions(apps) + permission_cls = apps.get_model('dab_rbac', 'DABPermission') + perm_list = [permission_cls.objects.get(codename=str_perm) for str_perm in permissions] + rd.permissions.add(*perm_list) + logger.info(f'Created {self.shortname} managed role definition, name={self.name}') + logger.debug(f'Data of {self.name} role definition: {defaults}') + logger.debug(f'Permissions of {self.name} role definition: {permissions}') + return rd, created + + def allowed_permissions(self, model: Optional[Type[Model]]) -> set[str]: + from ansible_base.rbac.validators import combine_values, permissions_allowed_for_role + + return combine_values(permissions_allowed_for_role(model)) + + +class ManagedAdminBase(ManagedRoleConstructor): + description = gettext_noop("Has all permissions to a single {model_name_verbose}") + + def get_permissions(self, apps) -> set[str]: + """All permissions possible for the associated model""" + return self.allowed_permissions(self.get_model(apps)) + + +class ManagedActionBase(ManagedRoleConstructor): + description = gettext_noop("Can take specified action for a single {model_name_verbose}") + action = None + + def get_permissions(self, apps) -> set[str]: + """Gives permission for one special action and includes view permission as well""" + model_name = self.get_model(apps)._meta.model_name + return {f'view_{model_name}', self.action} + + +class ManagedReadOnlyBase(ManagedRoleConstructor): + """Given a certain type this managed role includes all possible view permissions for that type + + The type is defined in the subclass, so this is an abstract class + """ + + description = gettext_noop("Has all viewing related permissions that can be delegated via {model_name_verbose}") + + def get_permissions(self, apps) -> set[str]: + return {codename for codename in self.allowed_permissions(self.get_model(apps)) if codename.startswith('view')} + + +class OrganizationMixin: + model_name = settings.ANSIBLE_BASE_ORGANIZATION_MODEL + + +class TeamMixin: + model_name = settings.ANSIBLE_BASE_TEAM_MODEL + + +# Start concrete shared role definitions + + +class SystemAuditor(ManagedReadOnlyBase): + name = gettext_noop("System Auditor") + description = gettext_noop("Has view permissions to all objects") + model_name = None + + +class OrganizationAdmin(OrganizationMixin, ManagedAdminBase): + name = gettext_noop("Organization Admin") + description = gettext_noop("Has all permissions to a single organization and all objects inside of it") + + +class OrganizationMember(OrganizationMixin, ManagedActionBase): + name = gettext_noop("Organization Member") + description = gettext_noop("Has member permission to a single organization") + action = 'member_organization' + + +class TeamAdmin(TeamMixin, ManagedAdminBase): + name = gettext_noop("Team Admin") + description = gettext_noop("Can manage a single team and inherits all role assignments to the team") + + +class TeamMember(TeamMixin, ManagedActionBase): + name = gettext_noop("Team Member") + description = gettext_noop("Inherits all role assignments to a single team") + action = 'member_team' + + +# Setup for registry, ultimately exists inside of permission_registry + + +managed_role_templates = { + 'sys_auditor': SystemAuditor, + 'org_admin': OrganizationAdmin, + 'org_member': OrganizationMember, + 'team_admin': TeamAdmin, + 'team_member': TeamMember, + # These are not fully functional on their own, but can be easily subclassed + 'admin_base': ManagedAdminBase, + 'action_base': ManagedActionBase, +} + + +def get_managed_role_constructors(apps, setting_value: dict[str, dict]) -> dict[str, ManagedRoleConstructor]: + """Constructs managed role definition (instructions for creating a managed role definition) + + from the entries in setting_value, expected to be from settings.ANSIBLE_BASE_MANAGED_ROLE_REGISTRY""" + ret = {} + for shortname, role_data in setting_value.items(): + lookup_shortname = role_data.get('shortname', shortname) + cls = managed_role_templates[lookup_shortname] + overrides = role_data.copy() + overrides['template_shortname'] = lookup_shortname + overrides['shortname'] = shortname + ret[shortname] = cls(overrides=overrides) + return ret diff --git a/ansible_base/rbac/migrations/_managed_definitions.py b/ansible_base/rbac/migrations/_managed_definitions.py deleted file mode 100644 index d1a3d738b..000000000 --- a/ansible_base/rbac/migrations/_managed_definitions.py +++ /dev/null @@ -1,111 +0,0 @@ -import logging - -from django.conf import settings - -from ansible_base.rbac.permission_registry import permission_registry - -logger = logging.getLogger('ansible_base.rbac.migrations._managed_definitions') - - -def get_or_create_managed(name, description, ct, permissions, RoleDefinition): - role_definition, created = RoleDefinition.objects.get_or_create( - name=name, - defaults={'managed': True, 'description': description, 'content_type': ct} - ) - role_definition.permissions.set(list(permissions)) - - if not role_definition.managed: - role_definition.managed = True - role_definition.save(update_fields=['managed']) - - if created: - logger.info(f'Created RoleDefinition {role_definition.name} pk={role_definition} with {len(permissions)} permissions') - - return role_definition - - -def setup_managed_role_definitions(apps, schema_editor): - """ - Idepotent method to create or sync the managed role definitions - """ - to_create = settings.ANSIBLE_BASE_ROLE_PRECREATE - - ContentType = apps.get_model('contenttypes', 'ContentType') - Permission = apps.get_model('dab_rbac', 'DABPermission') - RoleDefinition = apps.get_model('dab_rbac', 'RoleDefinition') - Organization = apps.get_model(settings.ANSIBLE_BASE_ORGANIZATION_MODEL) - org_ct = ContentType.objects.get_for_model(Organization) - managed_role_definitions = [] - - org_perms = set() - for cls in permission_registry._registry: - ct = ContentType.objects.get_for_model(cls) - object_perms = set(Permission.objects.filter(content_type=ct)) - # Special case for InstanceGroup which has an organiation field, but is not an organization child object - if cls._meta.model_name != 'instancegroup': - org_perms.update(object_perms) - - if 'object_admin' in to_create and cls != Organization: - indiv_perms = object_perms.copy() - add_perms = [perm for perm in indiv_perms if perm.codename.startswith('add_')] - if add_perms: - for perm in add_perms: - indiv_perms.remove(perm) - - managed_role_definitions.append( - get_or_create_managed( - to_create['object_admin'].format(cls=cls), - f'Has all permissions to a single {cls._meta.verbose_name}', - ct, - indiv_perms, - RoleDefinition - ) - ) - - if 'org_children' in to_create and cls != Organization: - org_child_perms = object_perms.copy() - org_child_perms.add(Permission.objects.get(codename='view_organization')) - - managed_role_definitions.append( - get_or_create_managed( - to_create['org_children'].format(cls=cls), - f'Has all permissions to {cls._meta.verbose_name_plural} within an organization', - org_ct, - org_child_perms, - RoleDefinition, - ) - ) - - if 'special' in to_create: - special_perms = [] - for perm in object_perms: - if perm.codename.split('_')[0] not in ('add', 'change', 'update', 'delete', 'view'): - special_perms.append(perm) - for perm in special_perms: - action = perm.codename.split('_')[0] - view_perm = Permission.objects.get(content_type=ct, codename__startswith='view_') - managed_role_definitions.append( - get_or_create_managed( - to_create['special'].format(cls=cls, action=action), - f'Has {action} permissions to a single {cls._meta.verbose_name}', - ct, - [perm, view_perm], - RoleDefinition, - ) - ) - - if 'org_admin' in to_create: - managed_role_definitions.append( - get_or_create_managed( - to_create['org_admin'].format(cls=Organization), - 'Has all permissions to a single organization and all objects inside of it', - org_ct, - org_perms, - RoleDefinition, - ) - ) - - unexpected_role_definitions = RoleDefinition.objects.filter(managed=True).exclude(pk__in=[rd.pk for rd in managed_role_definitions]) - for role_definition in unexpected_role_definitions: - logger.info(f'Deleting old managed role definition {role_definition.name}, pk={role_definition.pk}') - role_definition.delete() diff --git a/ansible_base/rbac/models.py b/ansible_base/rbac/models.py index fd04bbf80..debcbf191 100644 --- a/ansible_base/rbac/models.py +++ b/ansible_base/rbac/models.py @@ -1,6 +1,6 @@ import logging from collections.abc import Iterable -from typing import Optional +from typing import Optional, Type # Django from django.conf import settings @@ -58,23 +58,28 @@ def __get__(self, obj, objtype=None): class ManagedRoleManager: - def __init__(self): + def __init__(self, apps): self._cache = {} + self.apps = apps def clear(self) -> None: "Clear any managed roles already loaded into the cache" self._cache = {} - org_admin = ManagedRoleFromSetting('Organization Admin') - org_member = ManagedRoleFromSetting('Organization Member') - team_admin = ManagedRoleFromSetting('Team Admin') - team_member = ManagedRoleFromSetting('Team Member') + def __getattr__(self, attr): + if attr in self._cache: + return self._cache[attr] + code_definition = permission_registry.get_managed_role_constructor(attr) + if code_definition: + rd, _ = code_definition.get_or_create(self.apps) + return rd class RoleDefinitionManager(models.Manager): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.managed = ManagedRoleManager() + def contribute_to_class(self, cls: Type[models.Model], name: str) -> None: + """After Django populates the model for the manager, attach the manager role manager""" + super().contribute_to_class(cls, name) + self.managed = ManagedRoleManager(self.model._meta.apps) def give_creator_permissions(self, user, obj) -> Optional['RoleUserAssignment']: # If the user is a superuser, no need to bother giving the creator permissions diff --git a/ansible_base/rbac/permission_registry.py b/ansible_base/rbac/permission_registry.py index 367bc5df6..fc817014a 100644 --- a/ansible_base/rbac/permission_registry.py +++ b/ansible_base/rbac/permission_registry.py @@ -8,6 +8,8 @@ from django.db.models.signals import post_delete, post_migrate from django.utils.functional import cached_property +from ansible_base.rbac.managed import ManagedRoleConstructor, get_managed_role_constructors + """ This will record the models that the RBAC system in this app will follow Other apps should register models with this pattern @@ -22,9 +24,10 @@ class PermissionRegistry: def __init__(self): - self._registry = set() + self._registry = set() # model registry self._name_to_model = dict() self._parent_fields = dict() + self._managed_roles = dict() # code-defined role definitions, managed=True self.apps_ready = False self._tracked_relationships = set() self._trackers = dict() @@ -108,6 +111,32 @@ def get_resource_registry(self): return get_registry() + def get_managed_role_constructor(self, shortname): + return self._managed_roles.get(shortname) + + def register_managed_role_constructor(self, shortname: str, managed_role: ManagedRoleConstructor) -> None: + """Add the given managed role to the managed role registry""" + self._managed_roles[shortname] = managed_role + + def register_managed_role_constructors(self) -> None: + """Adds the data in setting ANSIBLE_BASE_MANAGED_ROLE_REGISTRY to the managed role registry""" + managed_defs = get_managed_role_constructors(self.apps, settings.ANSIBLE_BASE_MANAGED_ROLE_REGISTRY) + for shortname, constructor in managed_defs.items(): + self.register_managed_role_constructor(shortname, constructor) + + def create_managed_roles(self, apps) -> list[tuple[Model, bool]]: + """Safe-ish method to create managed roles inside of a migration + + Returns a list with all the managed RoleDefinition objects and whether they were created + in case you have to make decisions based on that""" + if not self.apps_ready: + raise RuntimeError('Cannot create managed roles before apps are ready') + ret = [] + for managed_role in self._managed_roles.values(): + rd, created = managed_role.get_or_create(apps) + ret.append((rd, created)) + return ret + def call_when_apps_ready(self, apps, app_config): from ansible_base.rbac import triggers from ansible_base.rbac.evaluations import bound_has_obj_perm, bound_singleton_permissions, connect_rbac_methods @@ -147,6 +176,8 @@ def call_when_apps_ready(self, apps, app_config): self._trackers[role_name] = tracker tracker.initialize(relationship) + self.register_managed_role_constructors() + @property def team_model(self): return self.apps.get_model(settings.ANSIBLE_BASE_TEAM_MODEL) diff --git a/ansible_base/rbac/triggers.py b/ansible_base/rbac/triggers.py index 17f24d71e..ca99ca03b 100644 --- a/ansible_base/rbac/triggers.py +++ b/ansible_base/rbac/triggers.py @@ -2,14 +2,11 @@ from contextlib import contextmanager from typing import Optional -from django.apps import apps -from django.conf import settings from django.db.models import Model, Q from django.db.models.signals import m2m_changed, post_delete, post_init, post_save, pre_delete, pre_save from django.db.utils import ProgrammingError from ansible_base.rbac.caching import compute_object_role_permissions, compute_team_member_roles -from ansible_base.rbac.migrations._managed_definitions import setup_managed_role_definitions from ansible_base.rbac.models import ObjectRole, RoleDefinition, RoleEvaluation, get_evaluation_model from ansible_base.rbac.permission_registry import permission_registry from ansible_base.rbac.validators import validate_team_assignment_enabled @@ -278,9 +275,6 @@ def post_migration_rbac_setup(*args, **kwargs): except ProgrammingError: return # this happens when migrating backwards, tables do not exist at prior states - if settings.ANSIBLE_BASE_ROLE_PRECREATE: - setup_managed_role_definitions(apps, None) - compute_team_member_roles() compute_object_role_permissions() diff --git a/ansible_base/rbac/validators.py b/ansible_base/rbac/validators.py index d7cbcbce1..c85d3f992 100644 --- a/ansible_base/rbac/validators.py +++ b/ansible_base/rbac/validators.py @@ -26,9 +26,10 @@ def permissions_allowed_for_system_role() -> dict[type, list[str]]: "Permission codenames useable in system-wide roles, which have content_type set to None" permissions_by_model = defaultdict(list) for cls in sorted(permission_registry.all_registered_models, key=lambda cls: cls._meta.model_name): - if cls._meta.model_name == 'team': - continue # special exclusion of team object permissions from system-wide roles + is_team = bool(cls._meta.model_name == 'team') for codename in codenames_for_cls(cls): + if is_team and (not codename.startswith('view')): + continue # special exclusion of team object permissions from system-wide roles permissions_by_model[cls].append(codename) return permissions_by_model diff --git a/docs/apps/rbac.md b/docs/apps/rbac.md index 5614c2404..3c0ef049d 100644 --- a/docs/apps/rbac.md +++ b/docs/apps/rbac.md @@ -317,23 +317,6 @@ ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization' The organization model is only used for pre-created role definitions. -### Managed Pre-Created Role Definitions - -In a post_migrate signal, certain RoleDefinitions are pre-created. -You can customize that with the following setting. - -``` -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}', -} -``` - -Set this to `{}` if you will create role definitions in your own data migration, -or if you want all roles to be user-defined. - ### RBAC vs User Flag Responsibilities With some user flags, like the standard `is_superuser` flag, the RBAC system does not diff --git a/test_app/management/commands/create_demo_data.py b/test_app/management/commands/create_demo_data.py index 507872949..d64233001 100644 --- a/test_app/management/commands/create_demo_data.py +++ b/test_app/management/commands/create_demo_data.py @@ -9,7 +9,6 @@ from ansible_base.authentication.models import Authenticator, AuthenticatorUser from ansible_base.oauth2_provider.models import OAuth2Application from ansible_base.rbac.models import RoleDefinition -from ansible_base.rbac.validators import combine_values, permissions_allowed_for_role from test_app.models import EncryptionModel, InstanceGroup, Inventory, Organization, Team, User @@ -84,45 +83,21 @@ def handle(self, *args, **kwargs): with impersonate(bull_bot): Team.objects.get_or_create(name='community.general maintainers', defaults={'organization': galaxy}) - # NOTE: managed role definitions are turned off, you could turn them on and get rid of these - org_perms = combine_values(permissions_allowed_for_role(Organization)) - role_manager = type(RoleDefinition.objects.managed) - org_admin, _ = RoleDefinition.objects.get_or_create( - name=role_manager.org_admin.role_name, - permissions=org_perms, - defaults={'content_type': ContentType.objects.get_for_model(Organization), 'managed': True}, - ) - RoleDefinition.objects.get_or_create( - name=role_manager.org_member.role_name, - permissions=['member_organization', 'view_organization'], - defaults={'content_type': ContentType.objects.get_for_model(Organization), 'managed': True}, - ) ig_admin, _ = RoleDefinition.objects.get_or_create( name='AWX InstanceGroup admin', permissions=['change_instancegroup', 'delete_instancegroup', 'view_instancegroup'], defaults={'content_type': ContentType.objects.get_for_model(InstanceGroup)}, ) - team_perms = combine_values(permissions_allowed_for_role(Team)) - RoleDefinition.objects.get_or_create( - name=role_manager.team_admin.role_name, - permissions=team_perms, - defaults={'content_type': ContentType.objects.get_for_model(Team), 'managed': True}, - ) - team_member, _ = RoleDefinition.objects.get_or_create( - name=role_manager.team_member.role_name, - permissions=['view_team', 'member_team'], - defaults={'content_type': ContentType.objects.get_for_model(Team), 'managed': True}, - ) org_admin_user, _ = User.objects.get_or_create(username='org_admin') ig_admin_user, _ = User.objects.get_or_create(username='instance_group_admin') - org_admin.give_permission(org_admin_user, awx) + RoleDefinition.objects.managed.org_admin.give_permission(org_admin_user, awx) ig_admin.give_permission(ig_admin_user, isolated_group) for user in (org_admin_user, ig_admin_user, spud): user.set_password('password') user.save() - team_member.give_permission(spud, awx_devs) + RoleDefinition.objects.managed.team_member.give_permission(spud, awx_devs) OAuth2Application.objects.get_or_create( name="Demo OAuth2 Application", diff --git a/test_app/settings.py b/test_app/settings.py index 72595b065..dc8f46925 100644 --- a/test_app/settings.py +++ b/test_app/settings.py @@ -154,7 +154,14 @@ SYSTEM_USERNAME = '_system' -ANSIBLE_BASE_ROLE_PRECREATE = {} # tested in individual tests +ANSIBLE_BASE_MANAGED_ROLE_REGISTRY = { + 'team_member': {}, + 'team_admin': {}, + 'org_admin': {}, + 'org_member': {}, + 'cow_admin': {'shortname': 'admin_base', 'model_name': 'test_app.cow', 'name': 'Cow Admin'}, + 'cow_moo': {'shortname': 'action_base', 'model_name': 'test_app.cow', 'name': 'Cow Mooer', 'action': 'say_cow'}, +} ANSIBLE_BASE_ALLOW_SINGLETON_USER_ROLES = True ANSIBLE_BASE_ALLOW_SINGLETON_TEAM_ROLES = True diff --git a/test_app/tests/conftest.py b/test_app/tests/conftest.py index c7ef4f087..c258f4ac7 100644 --- a/test_app/tests/conftest.py +++ b/test_app/tests/conftest.py @@ -21,9 +21,7 @@ from ansible_base.lib.testing.fixtures import * # noqa: F403, F401 from ansible_base.lib.testing.util import copy_fixture, delete_authenticator from ansible_base.oauth2_provider.fixtures import * # noqa: F403, F401 -from ansible_base.rbac import permission_registry from ansible_base.rbac.models import RoleDefinition -from ansible_base.rbac.validators import combine_values, permissions_allowed_for_role from test_app import models @@ -610,56 +608,32 @@ def disable_activity_stream(): yield -role_manager = type(RoleDefinition.objects.managed) - - @pytest.fixture def org_admin_rd(): """Give all permissions possible for an organization""" - perm_list = combine_values(permissions_allowed_for_role(models.Organization)) RoleDefinition.objects.managed.clear() - return RoleDefinition.objects.create_from_permissions( - permissions=perm_list, - name=role_manager.org_admin.role_name, - content_type=permission_registry.content_type_model.objects.get_for_model(models.Organization), - managed=True, - ) + yield RoleDefinition.objects.managed.org_admin + RoleDefinition.objects.managed.clear() @pytest.fixture def org_member_rd(): RoleDefinition.objects.managed.clear() - return RoleDefinition.objects.create_from_permissions( - permissions=['view_organization', 'member_organization'], - name=role_manager.org_member.role_name, - content_type=permission_registry.content_type_model.objects.get_for_model(models.Organization), - managed=True, - ) + yield RoleDefinition.objects.managed.org_member + RoleDefinition.objects.managed.clear() @pytest.fixture def member_rd(): "Member role for a team, place in root conftest because it is needed for the team users tracked relationship" RoleDefinition.objects.managed.clear() - return RoleDefinition.objects.create_from_permissions( - permissions=[permission_registry.team_permission, f'view_{permission_registry.team_model._meta.model_name}'], - name=role_manager.team_member.role_name, - content_type=permission_registry.content_type_model.objects.get_for_model(permission_registry.team_model), - managed=True, - ) + yield RoleDefinition.objects.managed.team_member + RoleDefinition.objects.managed.clear() @pytest.fixture def admin_rd(): "Member role for a team, place in root conftest because it is needed for the team users tracked relationship" RoleDefinition.objects.managed.clear() - return RoleDefinition.objects.create_from_permissions( - permissions=[ - permission_registry.team_permission, - f'view_{permission_registry.team_model._meta.model_name}', - f'change_{permission_registry.team_model._meta.model_name}', - ], - name=role_manager.team_admin.role_name, - content_type=permission_registry.content_type_model.objects.get_for_model(permission_registry.team_model), - managed=True, - ) + yield RoleDefinition.objects.managed.team_admin + RoleDefinition.objects.managed.clear() diff --git a/test_app/tests/rbac/test_managed.py b/test_app/tests/rbac/test_managed.py new file mode 100644 index 000000000..d5762516e --- /dev/null +++ b/test_app/tests/rbac/test_managed.py @@ -0,0 +1,45 @@ +import pytest +from django.apps import apps + +from ansible_base.rbac import permission_registry +from ansible_base.rbac.managed import managed_role_templates +from ansible_base.rbac.models import DABPermission, RoleDefinition +from ansible_base.rbac.validators import validate_permissions_for_model + + +@pytest.mark.django_db +def test_courtesy_roles_pass_validation(): + """Because these use migration apps, we can not use normal model code, so we validate in tests""" + for template_name, cls in managed_role_templates.items(): + if '_base' in template_name: + continue # abstract, not intended to be used + constructor = cls() + perm_list = [DABPermission.objects.get(codename=str_perm) for str_perm in constructor.get_permissions(apps)] + model_cls = constructor.get_model(apps) + if model_cls is not None: + ct = permission_registry.content_type_model.objects.get_for_model(constructor.get_model(apps)) + else: + ct = None # system role + validate_permissions_for_model(perm_list, ct, managed=True) + + +@pytest.mark.django_db +def test_cow_admin(): + rd = RoleDefinition.objects.managed.cow_admin + perm_list = [perm.codename for perm in rd.permissions.all()] + assert set(perm_list) == {'change_cow', 'view_cow', 'delete_cow', 'say_cow'} + + +@pytest.mark.django_db +def test_cow_mooer(): + rd = RoleDefinition.objects.managed.cow_moo + perm_list = [perm.codename for perm in rd.permissions.all()] + assert set(perm_list) == {'view_cow', 'say_cow'} + assert rd.name == 'Cow Mooer' + + +@pytest.mark.django_db +def test_create_all_managed_roles(): + "This is a method that may be called in migrations, etc." + assert not RoleDefinition.objects.filter(name='Cow Mooer').exists() + permission_registry.create_managed_roles(apps) diff --git a/test_app/tests/rbac/test_migrations.py b/test_app/tests/rbac/test_migrations.py index 5c83dbf15..450d495cf 100644 --- a/test_app/tests/rbac/test_migrations.py +++ b/test_app/tests/rbac/test_migrations.py @@ -1,52 +1,12 @@ import pytest from django.apps import apps from django.contrib.contenttypes.models import ContentType -from django.test.utils import override_settings -from ansible_base.rbac.migrations._managed_definitions import setup_managed_role_definitions from ansible_base.rbac.migrations._utils import give_permissions -from ansible_base.rbac.models import DABPermission, RoleDefinition, RoleTeamAssignment, RoleUserAssignment +from ansible_base.rbac.models import DABPermission, RoleTeamAssignment, RoleUserAssignment from ansible_base.rbac.permission_registry import permission_registry from test_app.models import Team, User -INVENTORY_OBJ_PERMISSIONS = ['view_inventory', 'change_inventory', 'delete_inventory', 'update_inventory'] - - -@pytest.mark.django_db -def test_managed_definitions_precreate(): - with override_settings( - 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}', - } - ): - setup_managed_role_definitions(apps, None) - rd = RoleDefinition.objects.get(name='inventory-admin') - assert rd.managed is True - # add permissions do not go in the object-level admin - assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS) - - # test org-level object admin permissions - rd = RoleDefinition.objects.get(name='organization-inventory-admin') - assert rd.managed is True - assert set(rd.permissions.values_list('codename', flat=True)) == set(['add_inventory', 'view_organization'] + INVENTORY_OBJ_PERMISSIONS) - - -@pytest.mark.django_db -def test_managed_definitions_custom_obj_admin_name(): - with override_settings( - ANSIBLE_BASE_ROLE_PRECREATE={ - 'object_admin': 'foo-{cls._meta.model_name}-foo', - } - ): - setup_managed_role_definitions(apps, None) - rd = RoleDefinition.objects.get(name='foo-inventory-foo') - assert rd.managed is True - # add permissions do not go in the object-level admin - assert set(rd.permissions.values_list('codename', flat=True)) == set(INVENTORY_OBJ_PERMISSIONS) - @pytest.mark.django_db def test_give_permissions(organization, inventory, inv_rd): diff --git a/test_app/tests/rbac/test_validators.py b/test_app/tests/rbac/test_validators.py index 9cc9d1ecb..f0f1adcb5 100644 --- a/test_app/tests/rbac/test_validators.py +++ b/test_app/tests/rbac/test_validators.py @@ -88,7 +88,7 @@ def test_custom_team_roles_disabled(self): ) with pytest.raises(ValidationError) as exc: RoleDefinition.objects.create_from_permissions( - name=type(RoleDefinition.objects.managed).team_member.role_name, + name='Some new confusing team member role', permissions=['member_team', 'view_team'], content_type=permission_registry.content_type_model.objects.get_for_model(permission_registry.team_model), )