diff --git a/CHANGES/1290.feature b/CHANGES/1290.feature new file mode 100644 index 000000000..1bca8985e --- /dev/null +++ b/CHANGES/1290.feature @@ -0,0 +1,6 @@ +Added Role Based Access Control for each endpoint. +New default roles (creator, owner, viewer) have been added for ``AnsibleRepository``, ``AnsibleDistribution``, + ``CollectionRemote``, ``RoleRemote``, and ``GitRemote``. +New detail role management endpoints (``my_permissions``, ``list_roles``, ``add_role``, + ``remove_role``) have been added to ``AnsibleRepository``, ``AnsibleDistribution``, ``CollectionRemote``, ``GitRemote``, and ``RoleRemote``. + \ No newline at end of file diff --git a/pulp_ansible/app/galaxy/v3/views.py b/pulp_ansible/app/galaxy/v3/views.py index d6376de9f..6841305f6 100644 --- a/pulp_ansible/app/galaxy/v3/views.py +++ b/pulp_ansible/app/galaxy/v3/views.py @@ -72,9 +72,17 @@ from pulp_ansible.app.tasks.deletion import delete_collection_version, delete_collection + +_CAN_VIEW_REPO_CONTENT = { + "action": ["list", "retrieve", "download"], + "principal": "authenticated", + "effect": "allow", + "condition": "v3_can_view_repo_content", +} + _PERMISSIVE_ACCESS_POLICY = { "statements": [ - {"action": "*", "principal": "*", "effect": "allow"}, + _CAN_VIEW_REPO_CONTENT, ], "creation_hooks": [], } @@ -203,7 +211,23 @@ class CollectionViewSet( filterset_class = CollectionFilter pagination_class = LimitOffsetPagination - DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY + DEFAULT_ACCESS_POLICY = { + "statements": [ + _CAN_VIEW_REPO_CONTENT, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.delete_collection", + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.change_collection", + }, + ], + } def urlpattern(*args, **kwargs): """Return url pattern for RBAC.""" @@ -482,7 +506,20 @@ class CollectionUploadViewSet( serializer_class = CollectionVersionUploadSerializer pulp_tag_name = "Pulp_Ansible: Artifacts Collections V3" - DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY + DEFAULT_ACCESS_POLICY = { + "statements": [ + _CAN_VIEW_REPO_CONTENT, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_perms:ansible.add_collection", + "has_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + ], + } def urlpattern(*args, **kwargs): """Return url pattern for RBAC.""" @@ -673,7 +710,17 @@ class AnsibleNamespaceViewSet( "metadata_sha256": ["exact", "in"], } - DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY + # TODO: write an actual access policy + DEFAULT_ACCESS_POLICY = { + "statements": [ + _CAN_VIEW_REPO_CONTENT, + { + "action": "*", + "principal": "*", + "effect": "allow", + }, + ], + } def get_queryset(self): if getattr(self, "swagger_fake_view", False): @@ -776,7 +823,20 @@ class CollectionVersionViewSet( lookup_field = "version" - DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY + DEFAULT_ACCESS_POLICY = { + "statements": [ + _CAN_VIEW_REPO_CONTENT, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.delete_collection", + "has_model_or_obj_perms:ansible.view_collection", + ], + }, + ], + } def urlpattern(*args, **kwargs): """Return url pattern for RBAC.""" @@ -925,6 +985,8 @@ class CollectionImportViewSet( queryset = CollectionImport.objects.prefetch_related("task").all() serializer_class = CollectionImportDetailSerializer + queryset_filtering_required_permission = "ansible.view_ansiblerepository" + DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY since_filter = OpenApiParameter( @@ -1111,6 +1173,8 @@ class ClientConfigurationView(views.APIView): DEFAULT_ACCESS_POLICY = _PERMISSIVE_ACCESS_POLICY + action = "retrieve" + @extend_schema(responses=ClientConfigurationSerializer) def get(self, request, *args, **kwargs): """Get the client configs.""" diff --git a/pulp_ansible/app/global_access_conditions.py b/pulp_ansible/app/global_access_conditions.py new file mode 100644 index 000000000..5624480c5 --- /dev/null +++ b/pulp_ansible/app/global_access_conditions.py @@ -0,0 +1,15 @@ +def v3_can_view_repo_content(request, view, action): + """ + Check if the repo is private, only let users with view repository permissions + view the collections here. + """ + + # TODO: add when private repositories are a thing + # if "distro_base_path" in view.kwargs: + # distro_base_path = view.kwargs["distro_base_path"] + # repo = models.AnsibleDistribution.objects.get(base_path=distro_base_path).repository + + # if repo.is_private: + # return request.user.has_perm("ansible.view_ansiblerepository") + + return True diff --git a/pulp_ansible/app/migrations/0049_rbac_permissions.py b/pulp_ansible/app/migrations/0049_rbac_permissions.py new file mode 100644 index 000000000..45714d999 --- /dev/null +++ b/pulp_ansible/app/migrations/0049_rbac_permissions.py @@ -0,0 +1,33 @@ +# Generated by Django 3.2.18 on 2023-03-01 23:11 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ansible', '0048_collectionversionmark'), + ] + + operations = [ + migrations.AlterModelOptions( + name='ansibledistribution', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_ansibledistribution', 'Can manage roles on distributions')]}, + ), + migrations.AlterModelOptions( + name='ansiblerepository', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('rebuild_metadata_ansiblerepository', 'Can rebuild metadata on the repository'), ('repair_ansiblerepository', 'Can repair the repository'), ('sign_ansiblerepository', 'Can sign content on the repository'), ('sync_ansiblerepository', 'Can start a sync task on the repository'), ('manage_roles_ansiblerepository', 'Can manage roles on repositories'), ('modify_ansible_repo_content', 'Can modify repository content')]}, + ), + migrations.AlterModelOptions( + name='collectionremote', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': (('manage_roles_collectionremote', 'Can manage roles on collection remotes'),)}, + ), + migrations.AlterModelOptions( + name='gitremote', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_gitremote', 'Can manage roles on git remotes')]}, + ), + migrations.AlterModelOptions( + name='roleremote', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_roleremote', 'Can manage roles on role remotes')]}, + ), + ] diff --git a/pulp_ansible/app/models.py b/pulp_ansible/app/models.py index ae23c91c4..23d178108 100644 --- a/pulp_ansible/app/models.py +++ b/pulp_ansible/app/models.py @@ -11,6 +11,7 @@ from django_lifecycle import AFTER_UPDATE, BEFORE_SAVE, BEFORE_UPDATE, hook from pulpcore.plugin.models import ( + AutoAddObjPermsMixin, BaseModel, Content, Remote, @@ -355,7 +356,7 @@ class DownloadLog(BaseModel): ) -class RoleRemote(Remote): +class RoleRemote(Remote, AutoAddObjPermsMixin): """ A Remote for Ansible content. """ @@ -364,6 +365,7 @@ class RoleRemote(Remote): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [("manage_roles_roleremote", "Can manage roles on role remotes")] def _get_last_sync_task(pk): @@ -371,7 +373,7 @@ def _get_last_sync_task(pk): return sync_tasks.order_by("-pulp_created").first() -class CollectionRemote(Remote): +class CollectionRemote(Remote, AutoAddObjPermsMixin): """ A Remote for Collection content. """ @@ -421,9 +423,10 @@ def _reset_repository_last_synced_metadata_time(self): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = (("manage_roles_collectionremote", "Can manage roles on collection remotes"),) -class GitRemote(Remote): +class GitRemote(Remote, AutoAddObjPermsMixin): """ A Remote for Collection content hosted in Git repositories. """ @@ -435,6 +438,9 @@ class GitRemote(Remote): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_gitremote", "Can manage roles on git remotes"), + ] class AnsibleCollectionDeprecated(Content): @@ -452,7 +458,7 @@ class Meta: unique_together = ("namespace", "name") -class AnsibleRepository(Repository): +class AnsibleRepository(Repository, AutoAddObjPermsMixin): """ Repository for "ansible" content. @@ -482,7 +488,14 @@ def last_sync_task(self): class Meta: default_related_name = "%(app_label)s_%(model_name)s" - permissions = (("modify_ansible_repo_content", "Can modify ansible repository content"),) + permissions = [ + ("rebuild_metadata_ansiblerepository", "Can rebuild metadata on the repository"), + ("repair_ansiblerepository", "Can repair the repository"), + ("sign_ansiblerepository", "Can sign content on the repository"), + ("sync_ansiblerepository", "Can start a sync task on the repository"), + ("manage_roles_ansiblerepository", "Can manage roles on repositories"), + ("modify_ansible_repo_content", "Can modify repository content"), + ] def finalize_new_version(self, new_version): """Finalize repo version.""" @@ -532,7 +545,7 @@ def _reset_repository_last_synced_metadata_time(self): self.last_synced_metadata_time = None -class AnsibleDistribution(Distribution): +class AnsibleDistribution(Distribution, AutoAddObjPermsMixin): """ A Distribution for Ansible content. """ @@ -541,3 +554,5 @@ class AnsibleDistribution(Distribution): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + + permissions = [("manage_roles_ansibledistribution", "Can manage roles on distributions")] diff --git a/pulp_ansible/app/settings.py b/pulp_ansible/app/settings.py index bf1db2ea0..9e0f3b775 100644 --- a/pulp_ansible/app/settings.py +++ b/pulp_ansible/app/settings.py @@ -27,3 +27,8 @@ ANSIBLE_DEFAULT_DISTRIBUTION_PATH = None ANSIBLE_URL_NAMESPACE = "" ANSIBLE_COLLECT_DOWNLOAD_LOG = False + +DRF_ACCESS_POLICY = { + "dynaconf_merge_unique": True, + "reusable_conditions": ["pulp_ansible.app.global_access_conditions"], +} diff --git a/pulp_ansible/app/viewsets.py b/pulp_ansible/app/viewsets.py index a67378d75..72cc6e002 100644 --- a/pulp_ansible/app/viewsets.py +++ b/pulp_ansible/app/viewsets.py @@ -30,6 +30,7 @@ RemoteViewSet, RepositoryViewSet, RepositoryVersionViewSet, + RolesMixin, SingleArtifactContentUploadViewSet, ) from pulp_ansible.app.galaxy.mixins import UploadGalaxyCollectionMixin @@ -97,6 +98,28 @@ class RoleViewSet(ContentViewSet): serializer_class = RoleSerializer filterset_class = RoleFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_repository_model_or_obj_perms:ansible.view_role", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:ansible.modify_ansible_repo_content", + "has_required_repo_perms_on_upload:ansible.view_ansiblerepository", + "has_upload_param_model_or_obj_perms:core.change_upload", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class CollectionFilter(BaseFilterSet): """ @@ -111,7 +134,9 @@ class Meta: fields = ["namespace", "name"] -class CollectionViewset(NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin): +class CollectionViewset( + NamedModelViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin, RolesMixin +): """ Viewset for Ansible Collections. """ @@ -199,6 +224,27 @@ class CollectionVersionViewSet(UploadGalaxyCollectionMixin, SingleArtifactConten filterset_class = CollectionVersionFilter ordering_fields = ("pulp_created", "name", "version", "namespace") + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:ansible.modify_ansible_repo_content", + "has_required_repo_perms_on_upload:ansible.view_ansiblerepository", + "has_upload_param_model_or_obj_perms:core.change_upload", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class SignatureFilter(ContentFilter): """ @@ -233,6 +279,26 @@ class CollectionVersionSignatureViewSet(NoArtifactContentUploadViewSet): queryset = CollectionVersionSignature.objects.all() serializer_class = CollectionVersionSignatureSerializer + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:ansible.modify_ansible_repo_content", + "has_required_repo_perms_on_upload:ansible.view_ansiblerepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class CollectionVersionMarkFilter(ContentFilter): """ @@ -273,6 +339,26 @@ class CollectionDeprecatedViewSet(ContentViewSet): queryset = AnsibleCollectionDeprecated.objects.all() serializer_class = CollectionSerializer + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:ansible.modify_ansible_repo_content", + "has_required_repo_perms_on_upload:ansible.view_ansiblerepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class AnsibleNamespaceFilter(ContentFilter): """ @@ -299,7 +385,7 @@ class AnsibleNamespaceViewSet(ReadOnlyContentViewSet): filterset_class = AnsibleNamespaceFilter -class RoleRemoteViewSet(RemoteViewSet): +class RoleRemoteViewSet(RemoteViewSet, RolesMixin): """ ViewSet for Role Remotes. """ @@ -307,9 +393,62 @@ class RoleRemoteViewSet(RemoteViewSet): endpoint_name = "role" queryset = RoleRemote.objects.all() serializer_class = RoleRemoteSerializer - - -class GitRemoteViewSet(RemoteViewSet): + queryset_filtering_required_permission = "ansible.view_roleremote" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve", "my_permissions"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:ansible.add_roleremote", + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.delete_roleremote", + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.change_roleremote", + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.manage_roles_roleremote", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "ansible.roleremote_owner"}, + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "ansible.roleremote_creator": ["ansible.add_roleremote"], + "ansible.roleremote_owner": [ + "ansible.view_roleremote", + "ansible.change_roleremote", + "ansible.delete_roleremote", + "ansible.manage_roles_roleremote", + ], + "ansible.roleremote_viewer": ["ansible.view_roleremote"], + } + + +class GitRemoteViewSet(RemoteViewSet, RolesMixin): """ ViewSet for Ansible Remotes. @@ -319,9 +458,62 @@ class GitRemoteViewSet(RemoteViewSet): endpoint_name = "git" queryset = GitRemote.objects.all() serializer_class = GitRemoteSerializer - - -class AnsibleRepositoryViewSet(RepositoryViewSet, ModifyRepositoryActionMixin): + queryset_filtering_required_permission = "ansible.view_gitremote" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve", "my_permissions"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:ansible.add_gitremote", + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.delete_gitremote", + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.change_gitremote", + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.manage_roles_gitremote", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "ansible.gitremote_owner"}, + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "ansible.gitremote_creator": ["ansible.add_gitremote"], + "ansible.gitremote_owner": [ + "ansible.view_gitremote", + "ansible.change_gitremote", + "ansible.delete_gitremote", + "ansible.manage_roles_gitremote", + ], + "ansible.gitremote_viewer": ["ansible.view_gitremote"], + } + + +class AnsibleRepositoryViewSet(RepositoryViewSet, ModifyRepositoryActionMixin, RolesMixin): """ ViewSet for Ansible Repositories. """ @@ -330,6 +522,115 @@ class AnsibleRepositoryViewSet(RepositoryViewSet, ModifyRepositoryActionMixin): queryset = AnsibleRepository.objects.all() serializer_class = AnsibleRepositorySerializer + queryset_filtering_required_permission = "ansible.view_ansiblerepository" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve", "my_permissions"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:ansible.add_ansiblerepository", + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.delete_ansiblerepository", + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.change_ansiblerepository", + "has_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["modify", "mark", "unmark"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.modify_ansible_repo_content", + "has_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["rebuild_metadata"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.rebuild_metadata_ansiblerepository", + ], + }, + { + "action": ["repair"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.repair_ansiblerepository" + "has_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["sign"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.sign_ansiblerepository", + "has_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["sync"], + "principal": "authenticated", + "effect": "allow", + "condition_expression": [ # required to be single string + "has_model_or_obj_perms:ansible.sync_ansiblerepository " + "has_model_or_obj_perms:ansible.view_ansiblerepository " + "(has_remote_param_model_or_obj_perms:ansible.view_collectionremote " + "or has_remote_param_model_or_obj_perms:ansible.view_gitremote " + "or has_remote_param_model_or_obj_perms:ansible.view_roleremote)" + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.manage_roles_ansiblerepository", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "ansible.ansiblerepository_owner"}, + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "ansible.ansiblerepository_creator": ["ansible.add_ansiblerepository"], + "ansible.ansiblerepository_owner": [ + "ansible.view_ansiblerepository", + "ansible.change_ansiblerepository", + "ansible.delete_ansiblerepository", + "ansible.manage_roles_ansiblerepository", + "ansible.modify_ansible_repo_content", + "ansible.rebuild_metadata_ansiblerepository", + "ansible.repair_ansiblerepository", + "ansible.sign_ansiblerepository", + "ansible.sync_ansiblerepository", + ], + "ansible.ansiblerepository_viewer": ["ansible.view_ansiblerepository"], + } + @extend_schema( description="Trigger an asynchronous task to sync Ansible content.", responses={202: AsyncOperationResponseSerializer}, @@ -507,6 +808,44 @@ class AnsibleRepositoryVersionViewSet(RepositoryVersionViewSet): parent_viewset = AnsibleRepositoryViewSet + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_repository_model_or_obj_perms:ansible.view_ansiblerepository", + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_obj_perms:ansible.delete_ansiblerepository", + "has_repository_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["repair"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_obj_perms:ansible.repair_ansiblerepository", + "has_repository_model_or_obj_perms:ansible.view_ansiblerepository", + ], + }, + { + "action": ["rebuild_metadata"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_obj_perms:ansible.rebuild_metadata_ansiblerepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + @extend_schema( description="Trigger an asynchronous task to rebuild Ansible content meta.", responses={202: AsyncOperationResponseSerializer}, @@ -533,7 +872,7 @@ def rebuild_metadata(self, request, *args, **kwargs): return OperationPostponedResponse(result, request) -class CollectionRemoteViewSet(RemoteViewSet): +class CollectionRemoteViewSet(RemoteViewSet, RolesMixin): """ ViewSet for Collection Remotes. """ @@ -543,6 +882,60 @@ class CollectionRemoteViewSet(RemoteViewSet): serializer_class = CollectionRemoteSerializer filterset_class = CollectionRemoteFilter + queryset_filtering_required_permission = "ansible.view_collectionremote" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve", "my_permissions"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_perms:ansible.add_collectionremote", + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.delete_collectionremote", + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.change_collectionremote", + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.manage_roles_collectionremote", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "ansible.collectionremote_owner"}, + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "ansible.collectionremote_creator": ["ansible.add_collectionremote"], + "ansible.collectionremote_owner": [ + "ansible.view_collectionremote", + "ansible.change_collectionremote", + "ansible.delete_collectionremote", + "ansible.manage_roles_collectionremote", + ], + "ansible.collectionremote_viewer": ["ansible.view_collectionremote"], + } + def async_reserved_resources(self, instance): if instance is None: return [] @@ -601,7 +994,7 @@ def create(self, request): return OperationPostponedResponse(async_result, request) -class AnsibleDistributionViewSet(DistributionViewSet): +class AnsibleDistributionViewSet(DistributionViewSet, RolesMixin): """ ViewSet for Ansible Distributions. """ @@ -610,6 +1003,66 @@ class AnsibleDistributionViewSet(DistributionViewSet): queryset = AnsibleDistribution.objects.all() serializer_class = AnsibleDistributionSerializer + queryset_filtering_required_permission = "ansible.view_ansibledistribution" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve", "my_permissions"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": "create", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_perms:ansible.add_ansibledistribution", + ], + }, + { + "action": "destroy", + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.delete_ansibledistribution", + ], + }, + { + "action": ["update", "partial_update"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_obj_perms:ansible.change_ansibledistribution", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_obj_perms:ansible.manage_roles_ansibledistribution", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "ansible.ansibledistribution_owner"}, + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "ansible.ansibledistribution_creator": ["ansible.add_ansibledistribution"], + "ansible.ansibledistribution_owner": [ + "ansible.view_ansibledistribution", + "ansible.change_ansibledistribution", + "ansible.delete_ansibledistribution", + "ansible.manage_roles_ansibledistribution", + ], + "ansible.ansibledistribution_viewer": ["ansible.view_ansibledistribution"], + } + class TagViewSet(NamedModelViewSet, mixins.ListModelMixin): """ diff --git a/pulp_ansible/tests/functional/api/collection/v3/test_client_configuration.py b/pulp_ansible/tests/functional/api/collection/v3/test_client_configuration.py index df3d1b48d..cbd334ccf 100644 --- a/pulp_ansible/tests/functional/api/collection/v3/test_client_configuration.py +++ b/pulp_ansible/tests/functional/api/collection/v3/test_client_configuration.py @@ -2,12 +2,12 @@ def test_client_config_distro( ansible_repo, ansible_distribution_factory, ansible_client_configuration_api_client ): distro = ansible_distribution_factory(ansible_repo) - res = ansible_client_configuration_api_client.get(path=distro.base_path) + res = ansible_client_configuration_api_client.read(path=distro.base_path) assert res.default_distribution_path == distro.base_path def test_client_config_default_distro( ansible_repo, ansible_distribution_factory, ansible_client_default_configuration_api_client ): - res = ansible_client_default_configuration_api_client.get() + res = ansible_client_default_configuration_api_client.read() assert res.default_distribution_path is None diff --git a/pulp_ansible/tests/functional/api/collection/v3/test_namespace.py b/pulp_ansible/tests/functional/api/collection/v3/test_namespace.py index 29c19ab37..b839bbad6 100644 --- a/pulp_ansible/tests/functional/api/collection/v3/test_namespace.py +++ b/pulp_ansible/tests/functional/api/collection/v3/test_namespace.py @@ -170,38 +170,51 @@ def test_namespace_syncing( monitor_task, ansible_sync_factory, ansible_collection_remote_factory, + gen_user, ): """Test syncing a Collection w/ a Namespace also syncs the Namespace.""" - # Set up first Repo to have 3 Collections and 3 Namespaces - repo1, distro1 = ansible_repo_and_distro_factory() - kwargs1 = {"path": distro1.base_path, "distro_base_path": distro1.base_path} - - collections = [] - namespaces = {} - for i in range(3): - namespace = random_string() - collection, _ = build_and_upload_collection(repo1, config={"namespace": namespace}) - avatar_path = random_image_factory() - task = galaxy_v3_plugin_namespaces_api_client.create( - name=namespace, avatar=avatar_path, **kwargs1 - ) - result = monitor_task(task.task) - collections.append(collection) - namespaces[namespace] = result.created_resources[-1] - - # Set up second Repo and sync 2 Collections from first Repo - repo2, distro2 = ansible_repo_and_distro_factory() - kwargs2 = {"path": distro2.base_path, "distro_base_path": distro2.base_path} - collections_string = "\n - ".join((f"{c.namespace}.{c.name}" for c in collections[0:2])) - requirements = f"collections:\n - {collections_string}" - remote = ansible_collection_remote_factory( - url=distro1.client_url, requirements_file=requirements + user = gen_user( + model_roles=[ + "ansible.ansibledistribution_creator", + "ansible.ansiblerepository_creator", + "ansible.collectionremote_creator", + ] ) - ansible_sync_factory(ansible_repo=repo2, remote=remote.pulp_href) - # 2 Namespaces should have also been synced - synced_namespaces = galaxy_v3_plugin_namespaces_api_client.list(**kwargs2) - assert synced_namespaces.count == 2 - for namespace in synced_namespaces.results: - assert namespace.name in namespaces - assert namespaces[namespace.name] == namespace.pulp_href + with user: + # Set up first Repo to have 3 Collections and 3 Namespaces + repo1, distro1 = ansible_repo_and_distro_factory() + kwargs1 = {"path": distro1.base_path, "distro_base_path": distro1.base_path} + + collections = [] + namespaces = {} + for i in range(3): + namespace = random_string() + collection, _ = build_and_upload_collection(repo1, config={"namespace": namespace}) + avatar_path = random_image_factory() + task = galaxy_v3_plugin_namespaces_api_client.create( + name=namespace, avatar=avatar_path, **kwargs1 + ) + result = monitor_task(task.task) + collections.append(collection) + namespaces[namespace] = result.created_resources[-1] + + # Set up second Repo and sync 2 Collections from first Repo + repo2, distro2 = ansible_repo_and_distro_factory() + kwargs2 = {"path": distro2.base_path, "distro_base_path": distro2.base_path} + collections_string = "\n - ".join((f"{c.namespace}.{c.name}" for c in collections[0:2])) + requirements = f"collections:\n - {collections_string}" + remote = ansible_collection_remote_factory( + url=distro1.client_url, + requirements_file=requirements, + username=user.username, + password=user.password, + ) + + ansible_sync_factory(ansible_repo=repo2, remote=remote.pulp_href) + # 2 Namespaces should have also been synced + synced_namespaces = galaxy_v3_plugin_namespaces_api_client.list(**kwargs2) + assert synced_namespaces.count == 2 + for namespace in synced_namespaces.results: + assert namespace.name in namespaces + assert namespaces[namespace.name] == namespace.pulp_href diff --git a/pulp_ansible/tests/functional/api/git/test_sync.py b/pulp_ansible/tests/functional/api/git/test_sync.py index 695577f48..1ec56b022 100644 --- a/pulp_ansible/tests/functional/api/git/test_sync.py +++ b/pulp_ansible/tests/functional/api/git/test_sync.py @@ -7,6 +7,8 @@ from pulp_ansible.tests.functional.utils import gen_ansible_remote +from pulp_smash.pulp3.utils import gen_repo + @pytest.fixture def sync_and_count( @@ -32,39 +34,61 @@ def _sync_and_count(remote_body, repo=None, **sync_kwargs): @pytest.mark.parallel -def test_sync_collection_from_git(ansible_repo, sync_and_count, ansible_distribution_factory): +def test_sync_collection_from_git( + ansible_repo_api_client, + sync_and_count, + ansible_distribution_factory, + gen_user, + gen_object_with_cleanup, + create_ansible_cfg, +): """Sync collections from Git repositories and then install one of them.""" - body = gen_ansible_remote(url="https://github.com/pulp/pulp_installer.git") + user = gen_user( + model_roles=[ + "ansible.ansibledistribution_creator", + "ansible.ansiblerepository_creator", + "ansible.gitremote_creator", + ] + ) - count = sync_and_count(body, repo=ansible_repo) - assert count == 1 + with user: + body = gen_ansible_remote(url="https://github.com/pulp/pulp_installer.git") - body.pop("name") # Forces a new remote to be created - count = sync_and_count(body, repo=ansible_repo) - assert count == 1 + repo = gen_object_with_cleanup(ansible_repo_api_client, gen_repo()) + count = sync_and_count(body, repo=repo) + assert count == 1 - body = gen_ansible_remote(url="https://github.com/pulp/squeezer.git") + body.pop("name") # Forces a new remote to be created + count = sync_and_count(body, repo=repo) + assert count == 1 - count = sync_and_count(body, repo=ansible_repo) - assert count == 2 + body = gen_ansible_remote(url="https://github.com/pulp/squeezer.git") - # Create a distribution. - distribution = ansible_distribution_factory(ansible_repo) + count = sync_and_count(body, repo=repo) + assert count == 2 + + # Create a distribution. + distribution = ansible_distribution_factory(repo) + + collection_name = "pulp.squeezer" + + with tempfile.TemporaryDirectory() as temp_dir: + create_ansible_cfg(temp_dir, distribution.client_url, user) - collection_name = "pulp.squeezer" - with tempfile.TemporaryDirectory() as temp_dir: - # The install command needs --pre so a pre-release collection versions install - cmd = "ansible-galaxy collection install --pre {} -c -s {} -p {}".format( - collection_name, distribution.client_url, temp_dir - ) + # The install command needs --pre so a pre-release collection versions install + cmd = "ansible-galaxy collection install --pre {} -c -p {}".format( + collection_name, temp_dir + ) - directory = "{}/ansible_collections/{}".format(temp_dir, collection_name.replace(".", "/")) + directory = "{}/ansible_collections/{}".format( + temp_dir, collection_name.replace(".", "/") + ) - assert not path.exists(directory), "Directory {} already exists".format(directory) + assert not path.exists(directory), "Directory {} already exists".format(directory) - subprocess.run(cmd.split()) + subprocess.run(cmd.split(), cwd=temp_dir) - assert path.exists(directory), "Could not find directory {}".format(directory) + assert path.exists(directory), "Could not find directory {}".format(directory) @pytest.mark.parallel diff --git a/pulp_ansible/tests/functional/api/test_rbac.py b/pulp_ansible/tests/functional/api/test_rbac.py new file mode 100644 index 000000000..59c42ff15 --- /dev/null +++ b/pulp_ansible/tests/functional/api/test_rbac.py @@ -0,0 +1,421 @@ +import pytest +import uuid + +from pulp_ansible.tests.functional.constants import ANSIBLE_GALAXY_URL +from pulp_ansible.tests.functional.utils import gen_ansible_remote + +from pulp_smash.pulp3.bindings import monitor_task +from pulp_smash.pulp3.utils import gen_distribution, gen_repo + +from pulpcore.client.pulp_ansible import ApiException, AsyncOperationResponse + + +@pytest.fixture() +def gen_users(gen_user): + """Returns a user generator function for the tests.""" + + def _gen_users(role_names=list()): + if isinstance(role_names, str): + role_names = [role_names] + creator_roles = [f"ansible.{role}_creator" for role in role_names] + viewer_roles = [f"ansible.{role}_viewer" for role in role_names] + user_creator = gen_user(model_roles=creator_roles) + user_reader = gen_user(model_roles=viewer_roles) + user_helpless = gen_user() + return user_creator, user_reader, user_helpless + + return _gen_users + + +def try_action(user, client, action, outcome, *args, **kwargs): + action_api = getattr(client, f"{action}_with_http_info") + try: + with user: + response, status, _ = action_api(*args, **kwargs, _return_http_data_only=False) + if isinstance(response, AsyncOperationResponse): + response = monitor_task(response.task) + except ApiException as e: + assert e.status == outcome, f"{e}" + else: + assert status == outcome, f"User performed {action} when they shouldn't been able to" + return response + + +def test_ansible_repository_rbac(ansible_repo_api_client, gen_users): + user_creator, user_reader, user_helpless = gen_users("ansiblerepository") + + # List testing + try_action(user_creator, ansible_repo_api_client, "list", 200) + try_action(user_reader, ansible_repo_api_client, "list", 200) + try_action(user_helpless, ansible_repo_api_client, "list", 200) + + # Create testing + repo = try_action(user_creator, ansible_repo_api_client, "create", 201, gen_repo()) + try_action(user_reader, ansible_repo_api_client, "create", 403, gen_repo()) + try_action(user_helpless, ansible_repo_api_client, "create", 403, gen_repo()) + + # View testing + try_action(user_creator, ansible_repo_api_client, "read", 200, repo.pulp_href) + try_action(user_reader, ansible_repo_api_client, "read", 200, repo.pulp_href) + try_action(user_helpless, ansible_repo_api_client, "read", 404, repo.pulp_href) + + # Update testing + update_args = [repo.pulp_href, gen_repo()] + try_action(user_creator, ansible_repo_api_client, "update", 202, *update_args) + try_action(user_reader, ansible_repo_api_client, "update", 403, *update_args) + try_action(user_helpless, ansible_repo_api_client, "update", 404, *update_args) + + # Partial update testing + partial_update_args = [repo.pulp_href, {"name": str(uuid.uuid4())}] + try_action(user_creator, ansible_repo_api_client, "partial_update", 202, *partial_update_args) + try_action(user_reader, ansible_repo_api_client, "partial_update", 403, *partial_update_args) + try_action(user_helpless, ansible_repo_api_client, "partial_update", 404, *partial_update_args) + + # Delete testing + try_action(user_reader, ansible_repo_api_client, "delete", 403, repo.pulp_href) + try_action(user_helpless, ansible_repo_api_client, "delete", 404, repo.pulp_href) + try_action(user_creator, ansible_repo_api_client, "delete", 202, repo.pulp_href) + + +def test_ansible_repository_version_repair( + ansible_repo_api_client, ansible_repo_version_api_client, gen_users, gen_object_with_cleanup +): + """Test the repository version repair action""" + user_creator, user_reader, user_helpless = gen_users("ansiblerepository") + + with user_creator: + repo = gen_object_with_cleanup(ansible_repo_api_client, gen_repo()) + ver_href = repo.latest_version_href + body = {"verify_checksums": True} + try_action(user_creator, ansible_repo_version_api_client, "repair", 202, ver_href, body) + try_action(user_reader, ansible_repo_version_api_client, "repair", 403, ver_href, body) + try_action(user_helpless, ansible_repo_version_api_client, "repair", 403, ver_href, body) + + +def test_repository_apis( + ansible_repo_api_client, + ansible_remote_collection_api_client, + gen_users, + gen_object_with_cleanup, +): + """Test repository specific actions, modify, sync and rebuild_metadata.""" + user_creator, user_reader, user_helpless = gen_users(["ansiblerepository", "collectionremote"]) + + # Sync tests + with user_creator: + user_creator_remote = gen_object_with_cleanup( + ansible_remote_collection_api_client, + gen_ansible_remote( + url="https://galaxy.ansible.com", + requirements_file="collections:\n - name: community.docker\n version: 3.0.0", + ), + ) + repo = gen_object_with_cleanup(ansible_repo_api_client, gen_repo()) + + body = {"mirror": False, "optimize": True, "remote": user_creator_remote.pulp_href} + try_action(user_reader, ansible_repo_api_client, "sync", 403, repo.pulp_href, body) + try_action(user_creator, ansible_repo_api_client, "sync", 202, repo.pulp_href, body) + try_action(user_helpless, ansible_repo_api_client, "sync", 404, repo.pulp_href, body) + + # Modify tests + try_action(user_reader, ansible_repo_api_client, "modify", 403, repo.pulp_href, {}) + try_action(user_creator, ansible_repo_api_client, "modify", 202, repo.pulp_href, {}) + try_action(user_helpless, ansible_repo_api_client, "modify", 404, repo.pulp_href, {}) + + # Rebuild metadata tests + rebuild_metadata_args = [ + repo.pulp_href, + {"namespace": "community", "name": "docker", "version": "3.0.0"}, + ] + try_action( + user_reader, + ansible_repo_api_client, + "rebuild_metadata", + 403, + *rebuild_metadata_args, + ) + try_action( + user_creator, + ansible_repo_api_client, + "rebuild_metadata", + 202, + *rebuild_metadata_args, + ) + try_action( + user_helpless, + ansible_repo_api_client, + "rebuild_metadata", + 404, + *rebuild_metadata_args, + ) + + +def test_repository_role_management(gen_users, ansible_repo_api_client, gen_object_with_cleanup): + """Check repository role management apis.""" + user_creator, user_reader, user_helpless = gen_users("ansiblerepository") + + with user_creator: + href = gen_object_with_cleanup(ansible_repo_api_client, gen_repo()).pulp_href + + # Permission check testing + aperm_response = try_action(user_reader, ansible_repo_api_client, "my_permissions", 200, href) + assert aperm_response.permissions == [] + bperm_response = try_action(user_creator, ansible_repo_api_client, "my_permissions", 200, href) + assert len(bperm_response.permissions) > 0 + try_action(user_helpless, ansible_repo_api_client, "my_permissions", 404, href) + + # Add "viewer" role testing + nested_role = {"users": [user_helpless.username], "role": "ansible.ansiblerepository_viewer"} + try_action(user_reader, ansible_repo_api_client, "add_role", 403, href, nested_role=nested_role) + try_action( + user_helpless, ansible_repo_api_client, "add_role", 404, href, nested_role=nested_role + ) + try_action( + user_creator, ansible_repo_api_client, "add_role", 201, href, nested_role=nested_role + ) + + # Permission check testing again + cperm_response = try_action(user_helpless, ansible_repo_api_client, "my_permissions", 200, href) + assert len(cperm_response.permissions) == 1 + + # Remove "viewer" role testing + try_action( + user_reader, ansible_repo_api_client, "remove_role", 403, href, nested_role=nested_role + ) + try_action( + user_helpless, ansible_repo_api_client, "remove_role", 403, href, nested_role=nested_role + ) + try_action( + user_creator, ansible_repo_api_client, "remove_role", 201, href, nested_role=nested_role + ) + + # Permission check testing one more time + try_action(user_helpless, ansible_repo_api_client, "my_permissions", 404, href) + + +def test_ansible_distribution_rbac(ansible_distro_api_client, gen_users): + user_creator, user_reader, user_helpless = gen_users("ansibledistribution") + + # Create testing + dist_body = gen_distribution() + with user_creator: + distribution_create = ansible_distro_api_client.create(dist_body) + created_resources = monitor_task(distribution_create.task).created_resources + distribution = ansible_distro_api_client.read(created_resources[0]) + try_action(user_reader, ansible_distro_api_client, "create", 403, gen_distribution()) + try_action(user_helpless, ansible_distro_api_client, "create", 403, gen_distribution()) + + # List testing + try_action(user_creator, ansible_distro_api_client, "list", 200) + try_action(user_reader, ansible_distro_api_client, "list", 200) + try_action(user_helpless, ansible_distro_api_client, "list", 200) + + # View testing + view_args = [distribution.pulp_href] + try_action(user_creator, ansible_distro_api_client, "read", 200, *view_args) + try_action(user_reader, ansible_distro_api_client, "read", 200, *view_args) + try_action(user_helpless, ansible_distro_api_client, "read", 404, *view_args) + + # Update testing + update_args = [distribution.pulp_href, gen_distribution()] + try_action(user_creator, ansible_distro_api_client, "update", 202, *update_args) + try_action(user_reader, ansible_distro_api_client, "update", 403, *update_args) + try_action(user_helpless, ansible_distro_api_client, "update", 404, *update_args) + + # Partial update testing + partial_update_args = [distribution.pulp_href, {"name": str(uuid.uuid4())}] + try_action(user_creator, ansible_distro_api_client, "partial_update", 202, *partial_update_args) + try_action(user_reader, ansible_distro_api_client, "partial_update", 403, *partial_update_args) + try_action( + user_helpless, ansible_distro_api_client, "partial_update", 404, *partial_update_args + ) + + # Delete testing + try_action(user_reader, ansible_distro_api_client, "delete", 403, distribution.pulp_href) + try_action(user_helpless, ansible_distro_api_client, "delete", 404, distribution.pulp_href) + try_action(user_creator, ansible_distro_api_client, "delete", 202, distribution.pulp_href) + + +def test_ansible_distribution_role_management(ansible_distro_api_client, gen_users): + user_creator, user_reader, user_helpless = gen_users("ansibledistribution") + + # Check role management apis + dist_body = gen_distribution() + with user_creator: + distribution_create = ansible_distro_api_client.create(dist_body) + created_resources = monitor_task(distribution_create.task).created_resources + distribution = ansible_distro_api_client.read(created_resources[0]) + href = distribution.pulp_href + + # Permission check testing + urperm_response = try_action( + user_reader, ansible_distro_api_client, "my_permissions", 200, href + ) + assert urperm_response.permissions == [] + ucperm_response = try_action( + user_creator, ansible_distro_api_client, "my_permissions", 200, href + ) + assert len(ucperm_response.permissions) > 0 + try_action(user_helpless, ansible_distro_api_client, "my_permissions", 404, href) + + # Add "viewer" role testing + nested_role = {"users": [user_helpless.username], "role": "ansible.ansibledistribution_viewer"} + try_action( + user_reader, ansible_distro_api_client, "add_role", 403, href, nested_role=nested_role + ) + try_action( + user_helpless, ansible_distro_api_client, "add_role", 404, href, nested_role=nested_role + ) + try_action( + user_creator, ansible_distro_api_client, "add_role", 201, href, nested_role=nested_role + ) + + # Permission check testing again + uhperm_response = try_action( + user_helpless, ansible_distro_api_client, "my_permissions", 200, href + ) + assert len(uhperm_response.permissions) == 1 + + # Remove "viewer" role testing + try_action( + user_reader, ansible_distro_api_client, "remove_role", 403, href, nested_role=nested_role + ) + try_action( + user_helpless, ansible_distro_api_client, "remove_role", 403, href, nested_role=nested_role + ) + try_action( + user_creator, ansible_distro_api_client, "remove_role", 201, href, nested_role=nested_role + ) + + # Permission check testing one more time + try_action(user_helpless, ansible_distro_api_client, "my_permissions", 404, href) + + +REMOTES = { + "collection": {"role_name": "collectionremote"}, + "git": {"role_name": "gitremote"}, + "role": {"role_name": "roleremote"}, +} + + +@pytest.mark.parametrize("remote", REMOTES) +def test_remotes_rbac( + ansible_remote_collection_api_client, + ansible_remote_git_api_client, + ansible_remote_role_api_client, + gen_users, + remote, +): + REMOTE_APIS = { + "collectionremote": ansible_remote_collection_api_client, + "gitremote": ansible_remote_git_api_client, + "roleremote": ansible_remote_role_api_client, + } + role_name = REMOTES[remote]["role_name"] + remote_api = REMOTE_APIS[role_name] + + user_creator, user_reader, user_helpless = gen_users(role_name) + + # List testing + try_action(user_creator, remote_api, "list", 200) + try_action(user_reader, remote_api, "list", 200) + try_action(user_helpless, remote_api, "list", 200) + + # Create testing + remote = try_action( + user_creator, + remote_api, + "create", + 201, + gen_ansible_remote(url=ANSIBLE_GALAXY_URL), + ) + try_action( + user_reader, + remote_api, + "create", + 403, + gen_ansible_remote(url=ANSIBLE_GALAXY_URL), + ) + try_action( + user_helpless, + remote_api, + "create", + 403, + gen_ansible_remote(url=ANSIBLE_GALAXY_URL), + ) + + # View testing + try_action(user_creator, remote_api, "read", 200, remote.pulp_href) + try_action(user_reader, remote_api, "read", 200, remote.pulp_href) + try_action(user_helpless, remote_api, "read", 404, remote.pulp_href) + + # Update testing + update_args = [remote.pulp_href, gen_ansible_remote(url=ANSIBLE_GALAXY_URL)] + try_action(user_creator, remote_api, "update", 202, *update_args) + try_action(user_reader, remote_api, "update", 403, *update_args) + try_action(user_helpless, remote_api, "update", 404, *update_args) + + # Partial update testing + partial_update_args = [remote.pulp_href, {"name": str(uuid.uuid4())}] + try_action(user_creator, remote_api, "partial_update", 202, *partial_update_args) + try_action(user_reader, remote_api, "partial_update", 403, *partial_update_args) + try_action(user_helpless, remote_api, "partial_update", 404, *partial_update_args) + + # Delete testing + try_action(user_reader, remote_api, "delete", 403, remote.pulp_href) + try_action(user_helpless, remote_api, "delete", 404, remote.pulp_href) + try_action(user_creator, remote_api, "delete", 202, remote.pulp_href) + + +@pytest.mark.parametrize("remote", REMOTES) +def test_remotes_role_management( + ansible_remote_collection_api_client, + ansible_remote_git_api_client, + ansible_remote_role_api_client, + gen_users, + remote, +): + REMOTE_APIS = { + "collectionremote": ansible_remote_collection_api_client, + "gitremote": ansible_remote_git_api_client, + "roleremote": ansible_remote_role_api_client, + } + role_name = REMOTES[remote]["role_name"] + remote_api = REMOTE_APIS[role_name] + + user_creator, user_reader, user_helpless = gen_users(role_name) + + # Check role management apis + remote = try_action( + user_creator, + remote_api, + "create", + 201, + gen_ansible_remote(url=ANSIBLE_GALAXY_URL), + ) + href = remote.pulp_href + + # Permission check testing + urperm_response = try_action(user_reader, remote_api, "my_permissions", 200, href) + assert urperm_response.permissions == [] + ucperm_response = try_action(user_creator, remote_api, "my_permissions", 200, href) + assert len(ucperm_response.permissions) > 0 + try_action(user_helpless, remote_api, "my_permissions", 404, href) + + # Add "viewer" role testing + nested_role = {"users": [user_helpless.username], "role": f"ansible.{role_name}_viewer"} + try_action(user_reader, remote_api, "add_role", 403, href, nested_role=nested_role) + try_action(user_helpless, remote_api, "add_role", 404, href, nested_role=nested_role) + try_action(user_creator, remote_api, "add_role", 201, href, nested_role=nested_role) + + # Permission check testing again + uhperm_response = try_action(user_helpless, remote_api, "my_permissions", 200, href) + assert len(uhperm_response.permissions) == 1 + + # Remove "viewer" role testing + try_action(user_reader, remote_api, "remove_role", 403, href, nested_role=nested_role) + try_action(user_helpless, remote_api, "remove_role", 403, href, nested_role=nested_role) + try_action(user_creator, remote_api, "remove_role", 201, href, nested_role=nested_role) + + # Permission check testing one more time + try_action(user_helpless, remote_api, "my_permissions", 404, href) diff --git a/pulp_ansible/tests/functional/cli/test_collection_install.py b/pulp_ansible/tests/functional/cli/test_collection_install.py index b22cc79cc..5e7b52db7 100644 --- a/pulp_ansible/tests/functional/cli/test_collection_install.py +++ b/pulp_ansible/tests/functional/cli/test_collection_install.py @@ -39,29 +39,33 @@ def install_scenario_distribution( return ansible_distribution_factory(repo) -def test_install_collection(tmp_path, install_scenario_distribution): +def test_install_collection( + tmp_path, install_scenario_distribution, pulp_admin_user, create_ansible_cfg +): """Test that the collection can be installed from Pulp.""" - collection_name = ANSIBLE_DEMO_COLLECTION - collection_version = ANSIBLE_DEMO_COLLECTION_VERSION - - temp_dir = str(tmp_path) - cmd = [ - "ansible-galaxy", - "collection", - "install", - collection_name, - "-c", - "-s", - install_scenario_distribution.client_url, - "-p", - temp_dir, - ] - - directory = "{}/ansible_collections/{}".format(temp_dir, collection_name.replace(".", "/")) - - assert not path.exists(directory), "Directory {} already exists".format(directory) - subprocess.run(cmd) - assert path.exists(directory), "Could not find directory {}".format(directory) + with pulp_admin_user: + collection_name = ANSIBLE_DEMO_COLLECTION + collection_version = ANSIBLE_DEMO_COLLECTION_VERSION + + temp_dir = str(tmp_path) + + create_ansible_cfg(temp_dir, install_scenario_distribution.client_url, pulp_admin_user) + + cmd = [ + "ansible-galaxy", + "collection", + "install", + collection_name, + "-c", + "-p", + temp_dir, + ] + + directory = "{}/ansible_collections/{}".format(temp_dir, collection_name.replace(".", "/")) + + assert not path.exists(directory), "Directory {} already exists".format(directory) + subprocess.run(cmd, cwd=temp_dir) + assert path.exists(directory), "Could not find directory {}".format(directory) dl_log_dump = subprocess.check_output(["pulpcore-manager", "download-log"]) dl_log = json.loads(dl_log_dump) assert ( @@ -76,32 +80,37 @@ def test_install_signed_collection( install_scenario_distribution, signing_gpg_homedir_path, ascii_armored_detached_signing_service, + gen_user, + create_ansible_cfg, ): """Test that the collection can be installed from Pulp.""" - collection_name = ANSIBLE_DEMO_COLLECTION - repository_href = install_scenario_distribution.repository - signing_service = ascii_armored_detached_signing_service - # Switch this over to signature upload in the future - signing_body = {"signing_service": signing_service.pulp_href, "content_units": ["*"]} - monitor_task(ansible_repo_api_client.sign(repository_href, signing_body).task) - - temp_dir = str(tmp_path) - cmd = [ - "ansible-galaxy", - "collection", - "install", - collection_name, - "-c", - "-s", - install_scenario_distribution.client_url, - "-p", - temp_dir, - "--keyring", - f"{signing_gpg_homedir_path}/pubring.kbx", - ] - - directory = "{}/ansible_collections/{}".format(temp_dir, collection_name.replace(".", "/")) - - assert not path.exists(directory), "Directory {} already exists".format(directory) - subprocess.run(cmd) - assert path.exists(directory), "Could not find directory {}".format(directory) + user = gen_user(model_roles=["ansible.ansiblerepository_owner"]) + with user: + collection_name = ANSIBLE_DEMO_COLLECTION + repository_href = install_scenario_distribution.repository + signing_service = ascii_armored_detached_signing_service + # Switch this over to signature upload in the future + signing_body = {"signing_service": signing_service.pulp_href, "content_units": ["*"]} + monitor_task(ansible_repo_api_client.sign(repository_href, signing_body).task) + + temp_dir = str(tmp_path) + + create_ansible_cfg(temp_dir, install_scenario_distribution.client_url, user) + + cmd = [ + "ansible-galaxy", + "collection", + "install", + collection_name, + "-c", + "-p", + temp_dir, + "--keyring", + f"{signing_gpg_homedir_path}/pubring.kbx", + ] + + directory = "{}/ansible_collections/{}".format(temp_dir, collection_name.replace(".", "/")) + + assert not path.exists(directory), "Directory {} already exists".format(directory) + subprocess.run(cmd, cwd=temp_dir) + assert path.exists(directory), "Could not find directory {}".format(directory) diff --git a/pulp_ansible/tests/functional/conftest.py b/pulp_ansible/tests/functional/conftest.py index dd22abd86..41ec668cb 100644 --- a/pulp_ansible/tests/functional/conftest.py +++ b/pulp_ansible/tests/functional/conftest.py @@ -1,3 +1,4 @@ +import os import uuid import pytest @@ -222,3 +223,24 @@ def _build_and_upload_collection(ansible_repo=None, **kwargs): return collection, collection_href[0] return _build_and_upload_collection + + +@pytest.fixture +def create_ansible_cfg(): + """A factory to create a local ansible.cfg file with authentication""" + + def _create_ansible_cfg_factory(dir, server, user): + ansible_cfg = ( + "[galaxy]\nserver_list = pulp_ansible\n\n" + "[galaxy_server.pulp_ansible]\n" + "url = {}\n" + "username = {}\n" + "password = {}" + ).format(server, user.username, user.password) + + cfg_file = "{}/ansible.cfg".format(dir) + with open(cfg_file, "w", encoding="utf8") as f: + f.write(ansible_cfg) + return os.path.exists(cfg_file) + + return _create_ansible_cfg_factory