diff --git a/CHANGES/860.feature b/CHANGES/860.feature new file mode 100644 index 000000000..5be6ba8be --- /dev/null +++ b/CHANGES/860.feature @@ -0,0 +1 @@ +Added initial RBAC support. diff --git a/pulp_deb/app/migrations/0030_rbac_permissions.py b/pulp_deb/app/migrations/0030_rbac_permissions.py new file mode 100644 index 000000000..443a1ac6f --- /dev/null +++ b/pulp_deb/app/migrations/0030_rbac_permissions.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.9 on 2024-03-22 11:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('deb', '0029_distributedpublication'), + ] + + operations = [ + migrations.AlterModelOptions( + name='aptdistribution', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_aptdistribution', 'Can manage roles on an APT distribution')]}, + ), + migrations.AlterModelOptions( + name='aptpublication', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_aptpublication', 'Can manage roles on an APT publication')]}, + ), + migrations.AlterModelOptions( + name='aptremote', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_aptremote', 'Can manage roles on an APT remote')]}, + ), + migrations.AlterModelOptions( + name='aptrepository', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_aptrepository', 'Can manage roles on APT repositories'), ('modify_content_aptrepository', 'Add content to, or remove content from a repository'), ('repair_aptrepository', 'Copy an APT repository'), ('sync_aptrepository', 'Sync an APT repository'), ('delete_aptrepository_version', 'Delete a repository version')]}, + ), + migrations.AlterModelOptions( + name='verbatimpublication', + options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_verbatimpublication', 'Can manage roles on a verbatim publication')]}, + ), + ] diff --git a/pulp_deb/app/models/publication.py b/pulp_deb/app/models/publication.py index 6cff53a5b..6329163e3 100644 --- a/pulp_deb/app/models/publication.py +++ b/pulp_deb/app/models/publication.py @@ -6,6 +6,7 @@ from django_lifecycle import hook, AFTER_CREATE, AFTER_UPDATE from pulpcore.plugin.models import ( + AutoAddObjPermsMixin, BaseModel, Distribution, Publication, @@ -37,7 +38,7 @@ def latest_publication(repo_pk): ) -class VerbatimPublication(Publication): +class VerbatimPublication(Publication, AutoAddObjPermsMixin): """ A verbatim Publication for Content. @@ -54,9 +55,12 @@ def set_distributed_publication(self): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_verbatimpublication", "Can manage roles on a verbatim publication"), + ] -class AptPublication(Publication): +class AptPublication(Publication, AutoAddObjPermsMixin): """ A Publication for DebContent. @@ -79,9 +83,12 @@ def set_distributed_publication(self): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_aptpublication", "Can manage roles on an APT publication"), + ] -class AptDistribution(Distribution): +class AptDistribution(Distribution, AutoAddObjPermsMixin): """ A Distribution for DebContent. """ @@ -120,6 +127,9 @@ def content_handler(self, path): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_aptdistribution", "Can manage roles on an APT distribution"), + ] class DistributedPublication(BaseModel): diff --git a/pulp_deb/app/models/remote.py b/pulp_deb/app/models/remote.py index b4d6804b4..233948aec 100644 --- a/pulp_deb/app/models/remote.py +++ b/pulp_deb/app/models/remote.py @@ -1,9 +1,9 @@ from django.db import models -from pulpcore.plugin.models import Remote +from pulpcore.plugin.models import Remote, AutoAddObjPermsMixin -class AptRemote(Remote): +class AptRemote(Remote, AutoAddObjPermsMixin): """ A Remote for DebContent. """ @@ -21,3 +21,6 @@ class AptRemote(Remote): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_aptremote", "Can manage roles on an APT remote"), + ] diff --git a/pulp_deb/app/models/repository.py b/pulp_deb/app/models/repository.py index 3530a3d5e..9fdb1b7f7 100644 --- a/pulp_deb/app/models/repository.py +++ b/pulp_deb/app/models/repository.py @@ -1,6 +1,11 @@ from django.db import models -from pulpcore.plugin.models import BaseModel, Content, Repository +from pulpcore.plugin.models import ( + AutoAddObjPermsMixin, + BaseModel, + Content, + Repository, +) from pulpcore.plugin.repo_version_utils import ( remove_duplicates, validate_version_paths, @@ -32,7 +37,7 @@ log = logging.getLogger(__name__) -class AptRepository(Repository): +class AptRepository(Repository, AutoAddObjPermsMixin): """ A Repository for DebContent. """ @@ -66,6 +71,13 @@ class AptRepository(Repository): class Meta: default_related_name = "%(app_label)s_%(model_name)s" + permissions = [ + ("manage_roles_aptrepository", "Can manage roles on APT repositories"), + ("modify_content_aptrepository", "Add content to, or remove content from a repository"), + ("repair_aptrepository", "Copy an APT repository"), + ("sync_aptrepository", "Sync an APT repository"), + ("delete_aptrepository_version", "Delete a repository version"), + ] def release_signing_service(self, release): """ diff --git a/pulp_deb/app/viewsets/content.py b/pulp_deb/app/viewsets/content.py index ee4ef4a75..ba2908e32 100644 --- a/pulp_deb/app/viewsets/content.py +++ b/pulp_deb/app/viewsets/content.py @@ -41,6 +41,26 @@ class GenericContentViewSet(SingleArtifactContentUploadViewSet): serializer_class = serializers.GenericContentSerializer filterset_class = GenericContentFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class ContentRelationshipFilter(Filter): """ @@ -217,6 +237,26 @@ class PackageViewSet(SingleArtifactContentUploadViewSet): serializer_class = serializers.PackageSerializer filterset_class = PackageFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class InstallerPackageFilter(ContentFilter): """ @@ -262,6 +302,26 @@ class InstallerPackageViewSet(SingleArtifactContentUploadViewSet): serializer_class = serializers.InstallerPackageSerializer filterset_class = InstallerPackageFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + # Metadata @@ -294,6 +354,26 @@ class ReleaseFileViewSet(ContentViewSet): serializer_class = serializers.ReleaseFileSerializer filterset_class = ReleaseFileFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class PackageIndexFilter(ContentFilter): """ @@ -324,6 +404,26 @@ class PackageIndexViewSet(ContentViewSet): serializer_class = serializers.PackageIndexSerializer filterset_class = PackageIndexFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class SourceIndexFilter(ContentFilter): """ @@ -354,6 +454,26 @@ class SourceIndexViewSet(ContentViewSet): serializer_class = serializers.SourceIndexSerializer filterset_class = SourceIndexFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class InstallerFileIndexFilter(ContentFilter): """ @@ -383,6 +503,26 @@ class InstallerFileIndexViewSet(ContentViewSet): serializer_class = serializers.InstallerFileIndexSerializer filterset_class = InstallerFileIndexFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class ReleaseToPackageFilter(ContentRelationshipFilter): HELP = "Filter results where Release contains Package" @@ -430,6 +570,26 @@ class ReleaseViewSet(NoArtifactContentViewSet): serializer_class = serializers.ReleaseSerializer filterset_class = ReleaseFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class ReleaseArchitectureFilter(ContentFilter): """ @@ -519,6 +679,26 @@ class PackageReleaseComponentViewSet(ContentViewSet): serializer_class = serializers.PackageReleaseComponentSerializer filterset_class = PackageReleaseComponentFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class SourcePackageToReleaseComponentFilter(ContentRelationshipFilter): HELP = "Filter results where SourcePackage in ReleaseComponent" @@ -600,6 +780,26 @@ class SourcePackageViewSet(SingleArtifactContentUploadViewSet): serializer_class = serializers.SourcePackageSerializer filterset_class = SourcePackageFilter + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + class SourcePackageReleaseComponentFilter(ContentFilter): """ @@ -625,3 +825,23 @@ class SourcePackageReleaseComponentViewSet(ContentViewSet): queryset = models.SourcePackageReleaseComponent.objects.all() serializer_class = serializers.SourcePackageReleaseComponentSerializer filterset_class = SourcePackageReleaseComponentFilter + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_required_repo_perms_on_upload:deb.modify_content_aptrepository", + "has_required_repo_perms_on_upload:deb.view_aptrepository", + ], + }, + ], + "queryset_scoping": {"function": "scope_queryset"}, + } diff --git a/pulp_deb/app/viewsets/publication.py b/pulp_deb/app/viewsets/publication.py index db099bd7d..f0d5dc567 100644 --- a/pulp_deb/app/viewsets/publication.py +++ b/pulp_deb/app/viewsets/publication.py @@ -8,12 +8,13 @@ DistributionViewSet, OperationPostponedResponse, PublicationViewSet, + RolesMixin, ) from pulp_deb.app import models, serializers, tasks -class VerbatimPublicationViewSet(PublicationViewSet): +class VerbatimPublicationViewSet(PublicationViewSet, RolesMixin): # The doc string is a top level element of the user facing REST API documentation: """ An VerbatimPublication is the Pulp-internal representation of a "mirrored" AptRepositoryVersion. @@ -27,6 +28,70 @@ class VerbatimPublicationViewSet(PublicationViewSet): endpoint_name = "verbatim" queryset = models.VerbatimPublication.objects.exclude(complete=False) serializer_class = serializers.VerbatimPublicationSerializer + queryset_filtering_required_permission = "deb.view_verbatimpublication" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "my_permissions"], + "principal": ["authenticated"], + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_perms:deb.add_verbatimpublication", + "has_repo_or_repo_ver_param_model_or_domain_or_obj_perms:" + "deb.view_aptrepository", + ], + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.view_verbatimpublication", + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.delete_verbatimpublication", + "has_model_or_domain_or_obj_perms:deb.view_verbatimpublication", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:" + "deb.manage_roles_verbatimpublication", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "deb.verbatimpublication_owner"}, + } + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "deb.verbatimpublication_owner": [ + "deb.delete_verbatimpublication", + "deb.manage_roles_verbatimpublication", + "deb.view_verbatimpublication", + ], + "deb.verbatimpublication_creator": [ + "deb.add_verbatimpublication", + ], + "deb.verbatimpublication_viewer": [ + "deb.view_verbatimpublication", + ], + } @extend_schema( description="Trigger an asynchronous task to publish content", @@ -51,7 +116,7 @@ def create(self, request): return OperationPostponedResponse(result, request) -class AptPublicationViewSet(PublicationViewSet): +class AptPublicationViewSet(PublicationViewSet, RolesMixin): # The doc string is a top level element of the user facing REST API documentation: """ An AptPublication is the ready to serve Pulp-internal representation of an AptRepositoryVersion. @@ -66,6 +131,69 @@ class AptPublicationViewSet(PublicationViewSet): endpoint_name = "apt" queryset = models.AptPublication.objects.exclude(complete=False) serializer_class = serializers.AptPublicationSerializer + queryset_filtering_required_permission = "deb.view_aptpublication" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "my_permissions"], + "principal": ["authenticated"], + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_perms:deb.add_aptpublication", + "has_repo_or_repo_ver_param_model_or_domain_or_obj_perms:" + "deb.view_aptrepository", + ], + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.view_aptpublication", + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.delete_aptpublication", + "has_model_or_domain_or_obj_perms:deb.view_aptpublication", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.manage_roles_aptpublication", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "deb.aptpublication_owner"}, + } + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "deb.aptpublication_owner": [ + "deb.delete_aptpublication", + "deb.manage_roles_aptpublication", + "deb.view_aptpublication", + ], + "deb.aptpublication_creator": [ + "deb.add_aptpublication", + ], + "deb.aptpublication_viewer": [ + "deb.view_aptpublication", + ], + } @extend_schema( description="Trigger an asynchronous task to publish content", @@ -102,7 +230,7 @@ def create(self, request): return OperationPostponedResponse(result, request) -class AptDistributionViewSet(DistributionViewSet): +class AptDistributionViewSet(DistributionViewSet, RolesMixin): # The doc string is a top level element of the user facing REST API documentation: """ An AptDistribution is just an AptPublication made available via the content app. @@ -115,3 +243,80 @@ class AptDistributionViewSet(DistributionViewSet): endpoint_name = "apt" queryset = models.AptDistribution.objects.all() serializer_class = serializers.AptDistributionSerializer + queryset_filtering_required_permission = "deb.view_aptdistribution" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "my_permissions"], + "principal": ["authenticated"], + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_perms:deb.add_aptdistribution", + "has_publication_param_model_or_domain_or_obj_perms:deb.view_aptpublication", + "has_repo_or_repo_ver_param_model_or_domain_or_obj_perms:" + "deb.view_aptrepository", + ], + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.view_aptdistribution", + }, + { + "action": ["update", "partial_update", "set_label", "unset_label"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.change_aptdistribution", + "has_model_or_domain_or_obj_perms:deb.view_aptdistribution", + "has_publication_param_model_or_domain_or_obj_perms:deb.view_aptpublication", + "has_repo_or_repo_ver_param_model_or_domain_or_obj_perms:" + "deb.view_aptrepository", + ], + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.delete_aptdistribution", + "has_model_or_domain_or_obj_perms:deb.view_aptdistribution", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.manage_roles_aptdistribution", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "deb.aptdistribution_owner"}, + } + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "deb.aptdistribution_owner": [ + "deb.change_aptdistribution", + "deb.delete_aptdistribution", + "deb.manage_roles_aptdistribution", + "deb.view_aptdistribution", + ], + "deb.aptdistribution_creator": [ + "deb.add_aptdistribution", + ], + "deb.aptdistribution_viewer": [ + "deb.view_aptdistribution", + ], + } diff --git a/pulp_deb/app/viewsets/remote.py b/pulp_deb/app/viewsets/remote.py index 5e8ae0516..a3da02fda 100644 --- a/pulp_deb/app/viewsets/remote.py +++ b/pulp_deb/app/viewsets/remote.py @@ -1,11 +1,11 @@ from gettext import gettext as _ # noqa -from pulpcore.plugin.viewsets import RemoteViewSet +from pulpcore.plugin.viewsets import RemoteViewSet, RolesMixin from pulp_deb.app import models, serializers -class AptRemoteViewSet(RemoteViewSet): +class AptRemoteViewSet(RemoteViewSet, RolesMixin): # The doc string is a top level element of the user facing REST API documentation: """ An AptRemote represents an external APT repository content source. @@ -17,3 +17,72 @@ class AptRemoteViewSet(RemoteViewSet): endpoint_name = "apt" queryset = models.AptRemote.objects.all() serializer_class = serializers.AptRemoteSerializer + queryset_filtering_required_permission = "deb.view_aptremote" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "my_permissions"], + "principal": ["authenticated"], + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_perms:deb.add_aptremote", + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.view_aptremote", + }, + { + "action": ["update", "partial_update", "set_label", "unset_label"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.change_aptremote", + "has_model_or_domain_or_obj_perms:deb.view_aptremote", + ], + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.delete_aptremote", + "has_model_or_domain_or_obj_perms:deb.view_aptremote", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.manage_roles_aptremote", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "deb.aptremote_owner"}, + } + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "deb.aptremote_owner": [ + "deb.change_aptremote", + "deb.delete_aptremote", + "deb.manage_roles_aptremote", + "deb.view_aptremote", + ], + "deb.aptremote_creator": [ + "deb.add_aptremote", + ], + "deb.aptremote_viewer": [ + "deb.view_aptremote", + ], + } diff --git a/pulp_deb/app/viewsets/repository.py b/pulp_deb/app/viewsets/repository.py index 1804853d1..64aea054d 100644 --- a/pulp_deb/app/viewsets/repository.py +++ b/pulp_deb/app/viewsets/repository.py @@ -22,6 +22,7 @@ RepositoryVersionViewSet, RepositoryViewSet, NamedModelViewSet, + RolesMixin, ) from pulp_deb.app import models, serializers, tasks @@ -53,7 +54,7 @@ def _get_matching_prc_hrefs(self, package_hrefs): return prc_hrefs -class AptRepositoryViewSet(AptModifyRepositoryActionMixin, RepositoryViewSet): +class AptRepositoryViewSet(AptModifyRepositoryActionMixin, RepositoryViewSet, RolesMixin): # The doc string is a top level element of the user facing REST API documentation: """ An AptRepository is the locally stored, Pulp-internal representation of a APT repository. @@ -65,6 +66,140 @@ class AptRepositoryViewSet(AptModifyRepositoryActionMixin, RepositoryViewSet): endpoint_name = "apt" queryset = models.AptRepository.objects.all() serializer_class = serializers.AptRepositorySerializer + queryset_filtering_required_permission = "deb.view_aptrepository" + + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "my_permissions"], + "principal": ["authenticated"], + "effect": "allow", + }, + { + "action": ["create"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_remote_param_model_or_domain_or_obj_perms:deb.view_aptremote", + "has_model_or_domain_perms:deb.add_aptrepository", + ], + }, + { + "action": ["retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.view_aptrepository", + }, + { + "action": ["update", "partial_update", "set_label", "unset_label"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.change_aptrepository", + "has_model_or_domain_or_obj_perms:deb.view_aptrepository", + "has_remote_param_model_or_domain_or_obj_perms:deb.view_aptremote", + ], + }, + { + "action": ["modify"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.modify_content_aptrepository", + "has_model_or_domain_or_obj_perms:deb.view_aptrepository", + ], + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.delete_aptrepository", + "has_model_or_domain_or_obj_perms:deb.view_aptrepository", + ], + }, + { + "action": ["sync"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_model_or_domain_or_obj_perms:deb.sync_aptrepository", + "has_model_or_domain_or_obj_perms:deb.view_aptrepository", + "has_remote_param_model_or_domain_or_obj_perms:deb.view_aptremote", + ], + }, + { + "action": ["list_roles", "add_role", "remove_role"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_model_or_domain_or_obj_perms:deb.manage_roles_aptrepository", + }, + ], + "creation_hooks": [ + { + "function": "add_roles_for_object_creator", + "parameters": {"roles": "deb.aptrepository_owner"}, + } + ], + "queryset_scoping": {"function": "scope_queryset"}, + } + + LOCKED_ROLES = { + "deb.aptrepository_owner": [ + "deb.change_aptrepository", + "deb.delete_aptrepository", + "deb.delete_aptrepository_version", + "deb.manage_roles_aptrepository", + "deb.modify_content_aptrepository", + "deb.repair_aptrepository", + "deb.sync_aptrepository", + "deb.view_aptrepository", + ], + "deb.aptrepository_creator": [ + "deb.add_aptrepository", + ], + "deb.aptrepository_viewer": [ + "deb.view_aptrepository", + ], + # A locked role to allow all APT permissions + "deb.admin": [ + "deb.add_aptdistribution", + "deb.add_aptpublication", + "deb.add_aptremote", + "deb.add_aptrepository", + "deb.add_verbatimpublication", + "deb.change_aptdistribution", + "deb.change_aptremote", + "deb.change_aptrepository", + "deb.delete_aptdistribution", + "deb.delete_aptpublication", + "deb.delete_aptremote", + "deb.delete_aptrepository", + "deb.delete_aptrepository_version", + "deb.delete_verbatimpublication", + "deb.manage_roles_aptdistribution", + "deb.manage_roles_aptpublication", + "deb.manage_roles_aptremote", + "deb.manage_roles_aptrepository", + "deb.manage_roles_verbatimpublication", + "deb.modify_content_aptrepository", + "deb.repair_aptrepository", + "deb.sync_aptrepository", + "deb.view_aptdistribution", + "deb.view_aptpublication", + "deb.view_aptremote", + "deb.view_aptrepository", + "deb.view_verbatimpublication", + ], + # A locked role to allow APT view permissions + "deb.viewer": [ + "deb.view_aptdistribution", + "deb.view_aptpublication", + "deb.view_aptremote", + "deb.view_aptrepository", + "deb.view_verbatimpublication", + ], + } # This decorator is necessary since a sync operation is asyncrounous and returns # the id and href of the sync task. @@ -114,6 +249,35 @@ class AptRepositoryVersionViewSet(RepositoryVersionViewSet): parent_viewset = AptRepositoryViewSet + DEFAULT_ACCESS_POLICY = { + "statements": [ + { + "action": ["list", "retrieve"], + "principal": "authenticated", + "effect": "allow", + "condition": "has_repository_model_or_domain_or_obj_perms:deb.view_aptrepository", + }, + { + "action": ["destroy"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_domain_or_obj_perms:deb.delete_aptrepository_version", + "has_repository_model_or_domain_or_obj_perms:deb.view_aptrepository", + ], + }, + { + "action": ["repair"], + "principal": "authenticated", + "effect": "allow", + "condition": [ + "has_repository_model_or_domain_or_obj_perms:deb.repair_aptrepository", + "has_repository_model_or_domain_or_obj_perms:deb.view_aptrepository", + ], + }, + ], + } + class CopyViewSet(viewsets.ViewSet): """ diff --git a/pulp_deb/tests/functional/api/test_rbac.py b/pulp_deb/tests/functional/api/test_rbac.py new file mode 100644 index 000000000..69fff540b --- /dev/null +++ b/pulp_deb/tests/functional/api/test_rbac.py @@ -0,0 +1,123 @@ +"""Tests role-based access control.""" + +import pytest + +from pulp_deb.tests.functional.constants import DEB_PACKAGE_RELPATH +from pulp_deb.tests.functional.utils import get_local_package_absolute_path + +from pulpcore.client.pulp_deb.exceptions import ApiException + + +@pytest.mark.parallel +def test_rbac_repositories(apt_repository_api, deb_repository_factory, gen_user): + """ + Test creation of a repository. + """ + user_allowed = gen_user( + model_roles=[ + "deb.aptrepository_creator", + "deb.aptrepository_viewer", + ] + ) + user_denied = gen_user() + + with user_allowed: + repo = deb_repository_factory() + assert apt_repository_api.read(repo.pulp_href) + + with user_denied, pytest.raises(ApiException): + deb_repository_factory() + + +@pytest.mark.parallel +def test_rbac_upload(deb_repository_factory, deb_package_factory, gen_user): + """ + Test upload of a package. + """ + user_allowed = gen_user( + model_roles=[ + "deb.aptrepository_owner", + ] + ) + user_denied = gen_user() + repo = deb_repository_factory() + package_attrs = { + "file": get_local_package_absolute_path(DEB_PACKAGE_RELPATH), + "relative_path": DEB_PACKAGE_RELPATH, + "repository": repo.pulp_href, + } + + with user_allowed: + package = deb_package_factory(**package_attrs) + assert package.pulp_created + + with user_denied, pytest.raises(ApiException): + deb_package_factory(**package_attrs) + + +@pytest.mark.parallel +def test_rbac_publication(deb_publication_factory, deb_repository_factory, gen_user): + """Test publication.""" + user_allowed = gen_user( + model_roles=[ + "deb.aptpublication_creator", + "deb.aptpublication_viewer", + "deb.aptrepository_viewer", + ] + ) + user_denied = gen_user() + repo = deb_repository_factory() + + with user_allowed: + publication = deb_publication_factory(repo) + assert publication.repository == repo.pulp_href + + with user_denied, pytest.raises(ApiException): + deb_publication_factory(repo) + + +@pytest.mark.parallel +def test_rbac_verbatim_publication( + deb_verbatim_publication_factory, deb_repository_factory, gen_user +): + """Test verbatim publication.""" + user_allowed = gen_user( + model_roles=[ + "deb.verbatimpublication_creator", + "deb.verbatimpublication_viewer", + "deb.aptrepository_viewer", + ] + ) + user_denied = gen_user() + repo = deb_repository_factory() + + with user_allowed: + verbatim_publication = deb_verbatim_publication_factory(repo) + assert verbatim_publication.repository == repo.pulp_href + + with user_denied, pytest.raises(ApiException): + deb_verbatim_publication_factory(repo) + + +@pytest.mark.parallel +def test_rbac_distribution( + deb_distribution_factory, deb_publication_factory, deb_repository_factory, gen_user +): + """Test distribution.""" + user_allowed = gen_user( + model_roles=[ + "deb.aptdistribution_creator", + "deb.aptdistribution_viewer", + "deb.aptpublication_viewer", + ] + ) + user_denied = gen_user() + repo = deb_repository_factory() + publication = deb_publication_factory(repo) + + with user_allowed: + distribution = deb_distribution_factory(publication) + assert distribution.publication == publication.pulp_href + + with user_denied, pytest.raises(ApiException): + deb_distribution_factory(publication)