diff --git a/CHANGES/domain-enablement.feature b/CHANGES/domain-enablement.feature new file mode 100644 index 000000000..55207dd9b --- /dev/null +++ b/CHANGES/domain-enablement.feature @@ -0,0 +1,2 @@ +Add partial support for Domains. The plugin can be installed with the feature turned on, but only +the default domain will properly work. diff --git a/pulp_container/app/__init__.py b/pulp_container/app/__init__.py index 6e831d11b..f44c82173 100644 --- a/pulp_container/app/__init__.py +++ b/pulp_container/app/__init__.py @@ -8,6 +8,7 @@ class PulpContainerPluginAppConfig(PulpPluginAppConfig): label = "container" version = "2.23.0.dev" python_package_name = "pulp-container" + domain_compatible = True def ready(self): super().ready() diff --git a/pulp_container/app/authorization.py b/pulp_container/app/authorization.py index e79fff151..56e9a140e 100644 --- a/pulp_container/app/authorization.py +++ b/pulp_container/app/authorization.py @@ -17,6 +17,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization +from pulpcore.plugin.util import get_domain from pulp_container.app.models import ( ContainerDistribution, ContainerNamespace, @@ -209,10 +210,10 @@ def generate_claim_set(issuer, issued_at, subject, audience, access): } -def get_pull_through_distribution(path): +def get_pull_through_distribution(path, domain): return ( ContainerPullThroughDistribution.objects.annotate(path=Value(path)) - .filter(path__startswith=F("base_path")) + .filter(pulp_domain=domain, path__startswith=F("base_path")) .order_by("-base_path") .first() ) @@ -239,15 +240,18 @@ def has_pull_permissions(self, path): """ Check if the user has permissions to pull from the repository specified by the path. """ + domain = get_domain() try: - distribution = ContainerDistribution.objects.get(base_path=path) + distribution = ContainerDistribution.objects.get(base_path=path, pulp_domain=domain) except ContainerDistribution.DoesNotExist: namespace_name = path.split("/")[0] try: - namespace = ContainerNamespace.objects.get(name=namespace_name) + namespace = ContainerNamespace.objects.get(name=namespace_name, pulp_domain=domain) except ContainerNamespace.DoesNotExist: # Check if the user is allowed to create a new namespace - return self.has_permission(None, "POST", "create", {"name": namespace_name}) + return self.has_permission( + None, "POST", "create", {"name": namespace_name, "pulp_domain": domain} + ) if pt_distribution := get_pull_through_distribution(path): # Check if the user is allowed to create a new distribution @@ -255,34 +259,46 @@ def has_pull_permissions(self, path): else: # Check if the user is allowed to view distributions in the namespace return self.has_permission( - namespace, "GET", "view_distribution", {"name": namespace_name} + namespace, + "GET", + "view_distribution", + {"name": namespace_name, "pulp_domain": domain}, ) - if pt_distribution := get_pull_through_distribution(path): + if pt_distribution := get_pull_through_distribution(path, domain): # Check if the user is allowed to pull new content via a pull-through distribution - if self.has_pull_through_permissions(distribution): + if self.has_pull_through_permissions( + pt_distribution + ): # was this using the wrong variable? return True # Check if the user has general pull permissions - return self.has_permission(distribution, "GET", "pull", {"base_path": path}) + return self.has_permission( + distribution, "GET", "pull", {"base_path": path, "pulp_domain": domain} + ) def has_push_permissions(self, path): """ Check if the user has permissions to push to the repository specified by the path. """ + domain = get_domain() try: - distribution = ContainerDistribution.objects.get(base_path=path) + distribution = ContainerDistribution.objects.get(base_path=path, pulp_domain=domain) except ContainerDistribution.DoesNotExist: namespace_name = path.split("/")[0] try: - namespace = ContainerNamespace.objects.get(name=namespace_name) + namespace = ContainerNamespace.objects.get(name=namespace_name, pulp_domain=domain) except ContainerNamespace.DoesNotExist: # Check if user is allowed to create a new namespace - return self.has_permission(None, "POST", "create", {"name": namespace_name}) + return self.has_permission( + None, "POST", "create", {"name": namespace_name, "pulp_domain": domain} + ) # Check if user is allowed to create a new distribution in the namespace return self.has_permission(namespace, "POST", "create_distribution", {}) - return self.has_permission(distribution, "POST", "push", {"base_path": path}) + return self.has_permission( + distribution, "POST", "push", {"base_path": path, "pulp_domain": domain} + ) def has_view_catalog_permissions(self, path): """ diff --git a/pulp_container/app/cache.py b/pulp_container/app/cache.py index 38ed477e4..c19604712 100644 --- a/pulp_container/app/cache.py +++ b/pulp_container/app/cache.py @@ -2,6 +2,7 @@ from django.db.models import F, Value from pulpcore.plugin.cache import CacheKeys, AsyncContentCache, SyncContentCache +from pulpcore.plugin.util import get_domain, cache_key from pulp_container.app.models import ContainerDistribution, ContainerPullThroughDistribution from pulp_container.app.exceptions import RepositoryNotFound @@ -65,16 +66,17 @@ def find_base_path_cached(request, cached): """ path = request.resolver_match.kwargs["path"] - path_exists = cached.exists(base_key=path) + path_exists = cached.exists(base_key=cache_key(path)) if path_exists: return path else: + domain = get_domain() try: - distro = ContainerDistribution.objects.get(base_path=path) + distro = ContainerDistribution.objects.get(base_path=path, pulp_domain=domain) except ObjectDoesNotExist: distro = ( ContainerPullThroughDistribution.objects.annotate(path=Value(path)) - .filter(path__startswith=F("base_path")) + .filter(path__startswith=F("base_path"), pulp_domain=domain) .order_by("-base_path") .first() ) diff --git a/pulp_container/app/content.py b/pulp_container/app/content.py index fcdc7786f..dbe6418d3 100644 --- a/pulp_container/app/content.py +++ b/pulp_container/app/content.py @@ -1,16 +1,19 @@ from aiohttp import web +from django.conf import settings from pulpcore.plugin.content import app from pulp_container.app.registry import Registry registry = Registry() +PREFIX = "/pulp/container/{pulp_domain}/" if settings.DOMAIN_ENABLED else "/pulp/container/" + app.add_routes( [ web.get( - r"/pulp/container/{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}", + PREFIX + r"{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}", registry.get_by_digest, ) ] ) -app.add_routes([web.get(r"/pulp/container/{path:.+}/manifests/{tag_name}", registry.get_tag)]) +app.add_routes([web.get(PREFIX + r"{path:.+}/manifests/{tag_name}", registry.get_tag)]) diff --git a/pulp_container/app/global_access_conditions.py b/pulp_container/app/global_access_conditions.py index 48ef3120b..f58470f02 100644 --- a/pulp_container/app/global_access_conditions.py +++ b/pulp_container/app/global_access_conditions.py @@ -1,4 +1,5 @@ from logging import getLogger +from django.conf import settings from pulpcore.plugin.models import Repository from pulpcore.plugin.viewsets import RepositoryVersionViewSet @@ -12,10 +13,13 @@ def has_namespace_obj_perms(request, view, action, permission): """ Check if a user has object-level perms on the namespace associated with the distribution - or repository. + or repository. If they have model/domain level permission then return True. """ if request.user.has_perm(permission): return True + if settings.DOMAIN_ENABLED: + if request.user.has_perm(permission, request.pulp_domain): + return True if isinstance(view, RepositoryVersionViewSet): obj = Repository.objects.get(pk=view.kwargs["repository_pk"]).cast() else: @@ -42,26 +46,18 @@ def has_namespace_perms(request, view, action, permission): base_path = request.data.get("base_path") if not base_path: return False - namespace = base_path.split("/")[0] - try: - namespace = models.ContainerNamespace.objects.get(name=namespace) - except models.ContainerNamespace.DoesNotExist: - return False - else: - return request.user.has_perm(permission) or request.user.has_perm(ns_perm, namespace) + return has_namespace_obj_perms(request, view, action, ns_perm) def has_namespace_or_obj_perms(request, view, action, permission): """ - Check if a user has a namespace-level perms or object-level permission + Check if a user has a namespace-level perms or permissions on the original object """ ns_perm = "container.namespace_{}".format(permission.split(".", 1)[1]) if has_namespace_obj_perms(request, view, action, ns_perm): return True else: - return request.user.has_perm(permission) or request.user.has_perm( - permission, view.get_object() - ) + return request.user.has_perm(permission, view.get_object()) def obj_exists(request, view, action): @@ -99,13 +95,15 @@ def has_namespace_model_perms(request, view, action): """ if request.user.has_perm("container.add_containernamespace"): return True + if settings.DOMAIN_ENABLED: + return request.user.has_perm("container.add_containernamespace", obj=request.pulp_domain) return False def has_distribution_perms(request, view, action, permission): """ Check if the user has permissions on the corresponding distribution. - Model or object permission is sufficient. + Model, domain or object permission is sufficient. """ if request.user.has_perm(permission): return True diff --git a/pulp_container/app/migrations/0044_add_domain.py b/pulp_container/app/migrations/0044_add_domain.py new file mode 100644 index 000000000..021f8e2e3 --- /dev/null +++ b/pulp_container/app/migrations/0044_add_domain.py @@ -0,0 +1,81 @@ +# Generated by Django 4.2.16 on 2024-11-21 20:59 + +from django.db import migrations, models +import django.db.models.deletion +import pulpcore.app.util + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0125_openpgpdistribution_openpgpkeyring_openpgppublickey_and_more'), + ('container', '0043_add_os_arch_image_size_manifest_fields'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='blob', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='containernamespace', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='manifest', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='manifestsignature', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together=set(), + ), + migrations.AddField( + model_name='blob', + name='_pulp_domain', + field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), + ), + migrations.AddField( + model_name='containernamespace', + name='pulp_domain', + field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), + ), + migrations.AddField( + model_name='manifest', + name='_pulp_domain', + field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), + ), + migrations.AddField( + model_name='manifestsignature', + name='_pulp_domain', + field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), + ), + migrations.AddField( + model_name='tag', + name='_pulp_domain', + field=models.ForeignKey(default=pulpcore.app.util.get_domain_pk, on_delete=django.db.models.deletion.PROTECT, to='core.domain'), + ), + migrations.AlterUniqueTogether( + name='blob', + unique_together={('digest', '_pulp_domain')}, + ), + migrations.AlterUniqueTogether( + name='containernamespace', + unique_together={('name', 'pulp_domain')}, + ), + migrations.AlterUniqueTogether( + name='manifest', + unique_together={('digest', '_pulp_domain')}, + ), + migrations.AlterUniqueTogether( + name='manifestsignature', + unique_together={('digest', '_pulp_domain')}, + ), + migrations.AlterUniqueTogether( + name='tag', + unique_together={('name', 'tagged_manifest', '_pulp_domain')}, + ), + ] diff --git a/pulp_container/app/models.py b/pulp_container/app/models.py index a20ee91f8..2832f4281 100644 --- a/pulp_container/app/models.py +++ b/pulp_container/app/models.py @@ -27,7 +27,7 @@ Upload as CoreUpload, ) from pulpcore.plugin.repo_version_utils import remove_duplicates, validate_repo_version -from pulpcore.plugin.util import gpg_verify +from pulpcore.plugin.util import gpg_verify, get_domain_pk from . import downloaders @@ -63,10 +63,11 @@ class Blob(Content): TYPE = "blob" digest = models.TextField(db_index=True) + _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) class Meta: default_related_name = "%(app_label)s_%(model_name)s" - unique_together = ("digest",) + unique_together = ("digest", "_pulp_domain") class Manifest(Content): @@ -138,6 +139,7 @@ class Manifest(Content): symmetrical=False, through_fields=("image_manifest", "manifest_list"), ) + _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) def __init__(self, *args, **kwargs): self._json_manifest = None @@ -302,7 +304,7 @@ def is_artifact(self): class Meta: default_related_name = "%(app_label)s_%(model_name)s" - unique_together = ("digest",) + unique_together = ("digest", "_pulp_domain") class BlobManifest(models.Model): @@ -381,10 +383,11 @@ class Tag(Content): tagged_manifest = models.ForeignKey( Manifest, null=False, related_name="tagged_manifests", on_delete=models.CASCADE ) + _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) class Meta: default_related_name = "%(app_label)s_%(model_name)s" - unique_together = (("name", "tagged_manifest"),) + unique_together = ("name", "tagged_manifest", "_pulp_domain") class ManifestSignature(Content): @@ -421,12 +424,13 @@ class ManifestSignature(Content): signed_manifest = models.ForeignKey( Manifest, null=False, related_name="signed_manifests", on_delete=models.CASCADE ) + _pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) # TODO: Maybe there should be an optional field with a FK to a signing_service for the cases # when Pulp creates a signature. class Meta: default_related_name = "%(app_label)s_%(model_name)s" - unique_together = (("digest",),) + unique_together = ("digest", "_pulp_domain") class ContainerNamespace(BaseModel, AutoAddObjPermsMixin): @@ -438,9 +442,10 @@ class ContainerNamespace(BaseModel, AutoAddObjPermsMixin): """ name = models.TextField(db_index=True) + pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT) class Meta: - unique_together = (("name",),) + unique_together = ("name", "pulp_domain") permissions = [ ("namespace_add_containerdistribution", "Add any distribution to a namespace"), ("namespace_delete_containerdistribution", "Delete any distribution from a namespace"), diff --git a/pulp_container/app/redirects.py b/pulp_container/app/redirects.py index b938a562a..22bbecaf4 100644 --- a/pulp_container/app/redirects.py +++ b/pulp_container/app/redirects.py @@ -20,13 +20,19 @@ def __init__(self, distribution, path, request): self.distribution = distribution self.path = path self.request = request + self.path_prefix = ( + f"pulp/container/{request.pulp_domain.name}" + if settings.DOMAIN_ENABLED + else "pulp/container" + ) def redirect_to_content_app(self, content_type, content_id): """ Redirect to the content app. """ + return self.distribution.redirect_to_content_app( - f"{settings.CONTENT_ORIGIN}/pulp/container/{self.path}/{content_type}/{content_id}" + f"{settings.CONTENT_ORIGIN}/{self.path_prefix}/{self.path}/{content_type}/{content_id}" ) def issue_manifest_redirect(self, manifest): diff --git a/pulp_container/app/registry.py b/pulp_container/app/registry.py index be6b1cced..98e2737aa 100644 --- a/pulp_container/app/registry.py +++ b/pulp_container/app/registry.py @@ -22,6 +22,7 @@ from pulpcore.plugin.content import ArtifactResponse from pulpcore.plugin.tasking import dispatch from pulpcore.plugin.exceptions import TimeoutException +from pulpcore.plugin.util import get_domain from pulp_container.app.cache import RegistryContentCache from pulp_container.app.models import ContainerDistribution, Tag, Blob, Manifest, BlobManifest @@ -272,11 +273,11 @@ async def get_by_digest(self, request): content = repository_version.content | Content.objects.filter(pk__in=pending_content) # "/pulp/container/{path:.+}/{content:(blobs|manifests)}/sha256:{digest:.+}" content_type = request.match_info["content"] - + domain = get_domain() try: if content_type == "manifests": manifest = await Manifest.objects.prefetch_related("contentartifact_set").aget( - digest=digest + digest=digest, _pulp_domain=domain ) headers = { "Content-Type": manifest.media_type, @@ -438,6 +439,7 @@ async def run_pipeline(self, raw_text_manifest_data): ) async def init_pending_content(self, digest, manifest_data, media_type, raw_text_data): + domain = get_domain() if config := manifest_data.get("config", None): config_digest = config["digest"] config_blob = await self.save_config_blob(config_digest) @@ -453,6 +455,7 @@ async def init_pending_content(self, digest, manifest_data, media_type, raw_text media_type=media_type, config_blob=config_blob, data=raw_text_data, + _pulp_domain=domain, # For clarity ) await sync_to_async(manifest.init_architecture_and_os)() @@ -464,7 +467,7 @@ async def init_pending_content(self, digest, manifest_data, media_type, raw_text try: await manifest.asave() except IntegrityError: - manifest = await Manifest.objects.aget(digest=manifest.digest) + manifest = await Manifest.objects.aget(digest=manifest.digest, _pulp_domain=domain) await sync_to_async(manifest.touch)() await sync_to_async(self.repository.pending_manifests.add)(manifest) @@ -474,11 +477,12 @@ async def init_pending_content(self, digest, manifest_data, media_type, raw_text await sync_to_async(self.repository.pending_blobs.add)(blob) async def save_blob(self, digest, manifest): - blob = Blob(digest=digest) + domain = get_domain() + blob = Blob(digest=digest, _pulp_domain=domain) try: await blob.asave() except IntegrityError: - blob = await Blob.objects.aget(digest=digest) + blob = await Blob.objects.aget(digest=digest, _pulp_domain=domain) await sync_to_async(blob.touch)() bm_rel = BlobManifest(manifest=manifest, manifest_blob=blob) @@ -509,6 +513,7 @@ async def save_blob(self, digest, manifest): return blob async def save_config_blob(self, config_digest): + domain = get_domain() blob_relative_url = "/v2/{name}/blobs/{digest}".format( name=self.remote.namespaced_upstream_name, digest=config_digest ) @@ -517,13 +522,14 @@ async def save_config_blob(self, config_digest): response = await downloader.run() response.artifact_attributes["file"] = response.path + response.artifact_attributes["pulp_domain"] = domain saved_artifact = await save_artifact(response.artifact_attributes) - config_blob = Blob(digest=config_digest) + config_blob = Blob(digest=config_digest, _pulp_domain=domain) try: await config_blob.asave() except IntegrityError: - config_blob = await Blob.objects.aget(digest=config_digest) + config_blob = await Blob.objects.aget(digest=config_digest, _pulp_domain=domain) await sync_to_async(config_blob.touch)() content_artifact = ContentArtifact( diff --git a/pulp_container/app/registry_api.py b/pulp_container/app/registry_api.py index 72e416ab3..0b3f4fa5c 100644 --- a/pulp_container/app/registry_api.py +++ b/pulp_container/app/registry_api.py @@ -18,7 +18,6 @@ from tempfile import NamedTemporaryFile from django.core.exceptions import ObjectDoesNotExist -from django.core.files.storage import default_storage as storage from django.core.files.base import ContentFile, File from django.db import IntegrityError, transaction from django.db.models import F, Value @@ -30,7 +29,7 @@ from pulpcore.plugin.models import Artifact, ContentArtifact, UploadChunk from pulpcore.plugin.files import PulpTemporaryUploadedFile from pulpcore.plugin.tasking import add_and_remove, dispatch -from pulpcore.plugin.util import get_objects_for_user, get_url +from pulpcore.plugin.util import get_objects_for_user, get_url, get_domain from pulpcore.plugin.exceptions import TimeoutException from rest_framework.exceptions import ( @@ -292,10 +291,11 @@ def get_drv_pull(self, path): """ Get distribution, repository and repository_version for pull access. """ + domain = get_domain() try: distribution = models.ContainerDistribution.objects.prefetch_related( "pull_through_distribution" - ).get(base_path=path) + ).get(base_path=path, pulp_domain=domain) except models.ContainerDistribution.DoesNotExist: # get a pull-through cache distribution whose base_path is a substring of the path return self.get_pull_through_drv(path) @@ -310,9 +310,10 @@ def get_drv_pull(self, path): return distribution, repository, repository_version def get_pull_through_drv(self, path): + domain = get_domain() pull_through_cache_distribution = ( models.ContainerPullThroughDistribution.objects.annotate(path=Value(path)) - .filter(path__startswith=F("base_path")) + .filter(path__startswith=F("base_path"), pulp_domain=domain) .order_by("-base_path") .first() ) @@ -337,7 +338,7 @@ def get_pull_through_drv(self, path): try: with transaction.atomic(): repository, _ = models.ContainerRepository.objects.get_or_create( - name=path, retain_repo_versions=1 + name=path, retain_repo_versions=1, pulp_domain=domain ) remote_data = model_to_dict( @@ -346,6 +347,7 @@ def get_pull_through_drv(self, path): remote, _ = models.ContainerRemote.objects.get_or_create( name=path, upstream_name=upstream_name, + pulp_domain=domain, **remote_data, ) @@ -356,6 +358,7 @@ def get_pull_through_drv(self, path): repository=repository, private=pull_through_cache_distribution.private, namespace=pull_through_cache_distribution.namespace, + pulp_domain=domain, ) except IntegrityError: # some entities needed to be created, but their keys already exist in the database @@ -372,8 +375,11 @@ def get_dr_push(self, request, path, create=False): Optionally create them if not found. """ + domain = get_domain() try: - distribution = models.ContainerDistribution.objects.get(base_path=path) + distribution = models.ContainerDistribution.objects.get( + base_path=path, pulp_domain=domain + ) except models.ContainerDistribution.DoesNotExist: if create: distribution, repository = self.create_dr(path, request) @@ -388,7 +394,7 @@ def get_dr_push(self, request, path, create=False): elif create: with transaction.atomic(): repository = serializers.ContainerPushRepositorySerializer.get_or_create( - {"name": path} + {"name": path, "pulp_domain": domain} ) distribution.repository = repository distribution.save() @@ -397,13 +403,15 @@ def get_dr_push(self, request, path, create=False): return distribution, repository def create_dr(self, path, request): + domain = get_domain() with transaction.atomic(): try: repository = serializers.ContainerPushRepositorySerializer.get_or_create( - {"name": path} + {"name": path, "pulp_domain": domain} ) distribution = serializers.ContainerDistributionSerializer.get_or_create( - {"base_path": path, "name": path}, {"repository": get_url(repository)} + {"base_path": path, "name": path, "pulp_domain": domain}, + {"repository": get_url(repository)}, ) except ObjectDoesNotExist: raise RepositoryInvalid(name=path, message="Repository is read-only.") @@ -535,7 +543,8 @@ class CatalogView(ContainerRegistryApiMixin, ListAPIView): def get_queryset(self, *args, **kwargs): """Filter the queryset based on public repositories and assigned permissions.""" - queryset = super().get_queryset() + domain = get_domain() + queryset = super().get_queryset().filter(pulp_domain=domain) distribution_permission = "container.pull_containerdistribution" namespace_permission = "container.namespace_pull_containerdistribution" @@ -587,6 +596,7 @@ def get(self, request): req_architectures = None req_label_exists = set() req_label_values = {} + domain = get_domain() for key, values in request.query_params.lists(): if key == "repository": req_repositories = values @@ -613,12 +623,15 @@ def get(self, request): if "org.flatpak.ref" not in req_label_exists: raise ParseError(detail="Missing label:org.flatpak.ref:exists=1.") - distributions = models.ContainerDistribution.objects.filter(private=False).only("base_path") + distributions = models.ContainerDistribution.objects.filter( + private=False, pulp_domain=domain + ).only("base_path") if req_repositories: distributions = distributions.filter(base_path__in=req_repositories) results = [] + storage = domain.get_storage() for distribution in distributions: images = [] if distribution.repository: @@ -801,17 +814,18 @@ def create_single_chunk_artifact(self, chunk): artifact = Artifact.init_and_validate(uploaded_file) artifact.save() except IntegrityError: - artifact = Artifact.objects.get(sha256=artifact.sha256) + artifact = Artifact.objects.get(sha256=artifact.sha256, pulp_domain=get_domain()) artifact.touch() return artifact def create_blob(self, artifact, digest): + domain = get_domain() with transaction.atomic(): try: - blob = models.Blob(digest=digest) + blob = models.Blob(digest=digest, _pulp_domain=domain) blob.save() except IntegrityError: - blob = models.Blob.objects.get(digest=digest) + blob = models.Blob.objects.get(digest=digest, _pulp_domain=domain) blob.touch() try: blob_artifact = ContentArtifact( @@ -843,7 +857,9 @@ def mount_blob(self, request, path, repository): """Mount a blob that is already present in another repository.""" from_path = request.query_params["from"] try: - distribution = models.ContainerDistribution.objects.get(base_path=from_path) + distribution = models.ContainerDistribution.objects.get( + base_path=from_path, pulp_domain=get_domain() + ) except models.ContainerDistribution.DoesNotExist: raise RepositoryNotFound(name=path) @@ -944,7 +960,7 @@ def put(self, request, path, pk=None): artifact = Artifact.init_and_validate(uploaded_file) artifact.save() except IntegrityError: - artifact = Artifact.objects.get(sha256=artifact.sha256) + artifact = Artifact.objects.get(sha256=artifact.sha256, pulp_domain=get_domain()) artifact.touch() blob = self.create_blob(artifact, digest) @@ -1046,6 +1062,7 @@ def handle_safe_method(self, request, path, pk): """ distribution, repository, repository_version = self.get_drv_pull(path) redirects = self.redirects_class(distribution, path, request) + domain = get_domain() if pk[:7] != "sha256:": try: @@ -1066,11 +1083,13 @@ def handle_safe_method(self, request, path, pk): if manifest is None: return redirects.redirect_to_content_app("manifests", pk) - tag = models.Tag(name=pk, tagged_manifest=manifest) + tag = models.Tag(name=pk, tagged_manifest=manifest, _pulp_domain=domain) try: tag.save() except IntegrityError: - tag = models.Tag.objects.get(name=tag.name, tagged_manifest=manifest) + tag = models.Tag.objects.get( + name=tag.name, tagged_manifest=manifest, _pulp_domain=domain + ) tag.touch() add_content_units = self.get_content_units_to_add(manifest, tag) @@ -1165,12 +1184,14 @@ def fetch_manifest(self, remote, pk): raise GatewayTimeout() else: digest = response.headers.get("docker-content-digest") - return models.Manifest.objects.filter(digest=digest).first() + return models.Manifest.objects.filter(digest=digest, pulp_domain=get_domain()).first() def put(self, request, path, pk=None): """ Responds with the actual manifest """ + domain = get_domain() + storage = domain.get_storage() # iterate over all the layers and create chunk = request.META["wsgi.input"] artifact = self.receive_artifact(chunk) @@ -1309,11 +1330,13 @@ def put(self, request, path, pk=None): # a manifest cannot be tagged by its digest - an identifier specified in the 'pk' parameter if not pk.startswith("sha256:"): - tag = models.Tag(name=pk, tagged_manifest=manifest) + tag = models.Tag(name=pk, tagged_manifest=manifest, _pulp_domain=domain) try: tag.save() except IntegrityError: - tag = models.Tag.objects.get(name=tag.name, tagged_manifest=manifest) + tag = models.Tag.objects.get( + name=tag.name, tagged_manifest=manifest, _pulp_domain=domain + ) tag.touch() add_content_units = [str(tag.pk), str(manifest.pk)] + [ @@ -1359,19 +1382,23 @@ def _init_manifest(self, manifest_digest, media_type, raw_text_data, config_blob media_type=media_type, config_blob=config_blob, data=raw_text_data, + _pulp_domain=get_domain(), ) def _save_manifest(self, manifest): try: manifest.save() except IntegrityError: - manifest = models.Manifest.objects.get(digest=manifest.digest) + manifest = models.Manifest.objects.get( + digest=manifest.digest, _pulp_domain=get_domain() + ) manifest.touch() return manifest def receive_artifact(self, chunk): """Handles assembling of Manifest as it's being uploaded.""" + domain = get_domain() with NamedTemporaryFile("ab") as temp_file: size = 0 hashers = {} @@ -1389,11 +1416,11 @@ def receive_artifact(self, chunk): digests = {} for algorithm in Artifact.DIGEST_FIELDS: digests[algorithm] = hashers[algorithm].hexdigest() - artifact = Artifact(file=temp_file.name, size=size, **digests) + artifact = Artifact(file=temp_file.name, size=size, pulp_domain=domain, **digests) try: artifact.save() except IntegrityError: - artifact = Artifact.objects.get(sha256=artifact.sha256) + artifact = Artifact.objects.get(sha256=artifact.sha256, pulp_domain=domain) artifact.touch() return artifact @@ -1410,14 +1437,14 @@ def head(self, request, path, pk=None): def get(self, request, path, pk): """Return a signature identified by its sha256 checksum.""" _, _, repository_version = self.get_drv_pull(path) - + domain = get_domain() try: manifest = models.Manifest.objects.get(digest=pk, pk__in=repository_version.content) except models.Manifest.DoesNotExist: try: # the manifest was initialized as a pending content unit # or has not been assigned to any repository yet - manifest = models.Manifest.objects.get(digest=pk) + manifest = models.Manifest.objects.get(digest=pk, _pulp_domain=domain) manifest.touch() except models.Manifest.DoesNotExist: raise ManifestNotFound(reference=pk) @@ -1445,6 +1472,7 @@ def get_response_data(signatures): def put(self, request, path, pk): """Create a new signature from the received data.""" _, repository = self.get_dr_push(request, path) + domain = get_domain() try: manifest = models.Manifest.objects.get( @@ -1481,11 +1509,14 @@ def put(self, request, path, pk): creator=signature_json["optional"].get("creator"), data=signature_dict["content"], signed_manifest=manifest, + _pulp_domain=domain, ) try: signature.save() except IntegrityError: - signature = models.ManifestSignature.objects.get(digest=signature.digest) + signature = models.ManifestSignature.objects.get( + digest=signature.digest, _pulp_domain=domain + ) signature.touch() immediate_task = dispatch( diff --git a/pulp_container/app/serializers.py b/pulp_container/app/serializers.py index 32ca51778..69b03b94e 100644 --- a/pulp_container/app/serializers.py +++ b/pulp_container/app/serializers.py @@ -27,6 +27,7 @@ SingleArtifactContentSerializer, ValidateFieldsMixin, ) +from pulpcore.plugin.util import get_domain from pulp_file.app.models import FileContent from pulp_container.app import models, fields @@ -419,7 +420,7 @@ def validate(self, data): validated_data = super().validate(data) if "content_guard" not in validated_data: validated_data["content_guard"] = ContentRedirectContentGuardSerializer.get_or_create( - {"name": "content redirect"} + {"name": "content redirect", "pulp_domain": get_domain()} ) if validated_data.get("repository_version"): repository = validated_data["repository_version"].repository.cast() @@ -435,7 +436,7 @@ def validate(self, data): if base_path: namespace_name = base_path.split("/")[0] validated_data["namespace"] = ContainerNamespaceSerializer.get_or_create( - {"name": namespace_name} + {"name": namespace_name, "pulp_domain": get_domain()} ) return validated_data @@ -504,14 +505,14 @@ def validate(self, data): if "content_guard" not in validated_data: validated_data["content_guard"] = ContentRedirectContentGuardSerializer.get_or_create( - {"name": "content redirect"} + {"name": "content redirect", "pulp_domain": get_domain()} ) base_path = validated_data.get("base_path") if base_path: namespace_name = base_path.split("/")[0] validated_data["namespace"] = ContainerNamespaceSerializer.get_or_create( - {"name": namespace_name} + {"name": namespace_name, "pulp_domain": get_domain()} ) return validated_data diff --git a/pulp_container/app/tasks/builder.py b/pulp_container/app/tasks/builder.py index 530e64480..866bb7846 100644 --- a/pulp_container/app/tasks/builder.py +++ b/pulp_container/app/tasks/builder.py @@ -20,6 +20,7 @@ Content, PulpTemporaryFile, ) +from pulpcore.plugin.util import get_domain def get_or_create_blob(layer_json, manifest, path): @@ -35,8 +36,9 @@ def get_or_create_blob(layer_json, manifest, path): class:`pulp_container.app.models.Blob` """ + domain = get_domain() try: - blob = Blob.objects.get(digest=layer_json["digest"]) + blob = Blob.objects.get(digest=layer_json["digest"], _pulp_domain=domain) blob.touch() except Blob.DoesNotExist: layer_file_name = os.path.join(path, layer_json["digest"][7:]) @@ -66,6 +68,7 @@ def add_image_from_directory_to_repository(path, repository, tag): image and tag. """ + domain = get_domain() manifest_path = os.path.join(path, "manifest.json") with open(manifest_path, "rb") as f: @@ -78,8 +81,9 @@ def add_image_from_directory_to_repository(path, repository, tag): schema_version=2, media_type=MEDIA_TYPE.MANIFEST_OCI, data=manifest_text_data, + _pulp_domain=domain, ) - tag, _ = Tag.objects.get_or_create(name=tag, tagged_manifest=manifest) + tag, _ = Tag.objects.get_or_create(name=tag, tagged_manifest=manifest, _pulp_domain=domain) with repository.new_version() as new_repo_version: manifest_json = json.loads(manifest_text_data) diff --git a/pulp_container/app/tasks/sync_stages.py b/pulp_container/app/tasks/sync_stages.py index 3b3163532..d453550ef 100644 --- a/pulp_container/app/tasks/sync_stages.py +++ b/pulp_container/app/tasks/sync_stages.py @@ -10,6 +10,7 @@ from asgiref.sync import sync_to_async from pulpcore.plugin.models import Artifact, ProgressReport, Remote from pulpcore.plugin.stages import DeclarativeArtifact, DeclarativeContent, Stage, ContentSaver +from pulpcore.plugin.util import get_domain from pulp_container.constants import ( MANIFEST_TYPE, @@ -80,7 +81,7 @@ async def _check_for_existing_manifest(self, download_tag): if ( manifest := await Manifest.objects.prefetch_related("contentartifact_set") - .filter(digest=digest) + .filter(digest=digest, pulp_domain=get_domain()) .afirst() ): if raw_text_data := manifest.data: @@ -461,7 +462,7 @@ async def create_listed_manifest(self, manifest_data): if ( manifest := await Manifest.objects.prefetch_related("contentartifact_set") - .filter(digest=digest) + .filter(digest=digest, pulp_domain=get_domain()) .afirst() ): if manifest.data: diff --git a/pulp_container/app/tasks/tag.py b/pulp_container/app/tasks/tag.py index 924381142..f7ca2862e 100644 --- a/pulp_container/app/tasks/tag.py +++ b/pulp_container/app/tasks/tag.py @@ -1,4 +1,5 @@ from pulpcore.plugin.models import CreatedResource, Repository +from pulpcore.plugin.util import get_domain from pulp_container.app.models import Manifest, Tag @@ -21,7 +22,9 @@ def tag_image(manifest_pk, tag, repository_pk): tagged_manifest=manifest ) - manifest_tag, created = Tag.objects.get_or_create(name=tag, tagged_manifest=manifest) + manifest_tag, created = Tag.objects.get_or_create( + name=tag, tagged_manifest=manifest, _pulp_domain=get_domain() + ) if created: resource = CreatedResource(content_object=manifest_tag) diff --git a/pulp_container/app/utils.py b/pulp_container/app/utils.py index ed038fa0e..4155e860a 100644 --- a/pulp_container/app/utils.py +++ b/pulp_container/app/utils.py @@ -10,12 +10,12 @@ from asgiref.sync import sync_to_async from jsonschema import Draft7Validator, validate, ValidationError -from django.core.files.storage import default_storage as storage from django.db import IntegrityError from functools import partial from rest_framework.exceptions import Throttled from pulpcore.plugin.models import Artifact, Task +from pulpcore.plugin.util import get_domain from pulp_container.constants import ( MANIFEST_MEDIA_TYPES, @@ -299,6 +299,7 @@ def pad_unpadded_b64(unpadded_b64): async def save_artifact(artifact_attributes): + artifact_attributes.setdefault("pulp_domain", get_domain()) saved_artifact = Artifact(**artifact_attributes) try: await saved_artifact.asave() @@ -310,7 +311,8 @@ async def save_artifact(artifact_attributes): def get_content_data(saved_artifact): - with storage.open(saved_artifact.file.name, mode="rb") as file: + # I don't think this is async safe, it might perform a query + with saved_artifact.file.storage.open(saved_artifact.file.name, mode="rb") as file: raw_data = file.read() content_data = json.loads(raw_data) return content_data, raw_data diff --git a/pulp_container/app/viewsets.py b/pulp_container/app/viewsets.py index 67db707cb..e699f3f20 100644 --- a/pulp_container/app/viewsets.py +++ b/pulp_container/app/viewsets.py @@ -23,6 +23,7 @@ extract_pk, get_objects_for_user, raise_for_unknown_content_units, + get_domain, ) from pulpcore.plugin.viewsets import ( AsyncUpdateMixin, @@ -203,13 +204,13 @@ def get_content_qs(self, qs, push_perm, mirror_perm): distributions__in=get_objects_for_user( self.request.user, push_perm, - models.ContainerDistribution.objects.all(), + models.ContainerDistribution.objects.filter(pulp_domain=get_domain()), ) ).only("pk") allowed_mirror_repos = get_objects_for_user( self.request.user, mirror_perm, - models.ContainerRepository.objects.all(), + models.ContainerRepository.objects.filter(pulp_domain=get_domain()), ).only("pk") content_qs = qs.model.objects.filter( Q(repositories__in=allowed_push_repos) | Q(repositories__in=allowed_mirror_repos) @@ -374,21 +375,21 @@ class ContainerRemoteViewSet(RemoteViewSet, RolesMixin): "action": ["create"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:container.add_containerremote", + "condition": "has_model_or_domain_perms:container.add_containerremote", }, { "action": ["retrieve"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.view_containerremote", + "condition": "has_model_or_domain_or_obj_perms:container.view_containerremote", }, { "action": ["update", "partial_update", "set_label", "unset_label"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.change_containerremote", - "has_model_or_obj_perms:container.view_containerremote", + "has_model_or_domain_or_obj_perms:container.change_containerremote", + "has_model_or_domain_or_obj_perms:container.view_containerremote", ], }, { @@ -396,15 +397,17 @@ class ContainerRemoteViewSet(RemoteViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.delete_containerremote", - "has_model_or_obj_perms:container.view_containerremote", + "has_model_or_domain_or_obj_perms:container.delete_containerremote", + "has_model_or_domain_or_obj_perms:container.view_containerremote", ], }, { "action": ["list_roles", "add_role", "remove_role"], "principal": "authenticated", "effect": "allow", - "condition": ["has_model_or_obj_perms:container.manage_roles_containerremote"], + "condition": [ + "has_model_or_domain_or_obj_perms:container.manage_roles_containerremote" + ], }, ], "creation_hooks": [ @@ -452,21 +455,21 @@ class ContainerPullThroughRemoteViewSet(RemoteViewSet, RolesMixin): "action": ["create"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:container.add_containerpullthroughremote", + "condition": "has_model_or_domain_perms:container.add_containerpullthroughremote", }, { "action": ["retrieve"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.view_containerpullthroughremote", + "condition": "has_model_or_domain_or_obj_perms:container.view_containerpullthroughremote", # noqa }, { "action": ["update", "partial_update"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.change_containerpullthroughremote", - "has_model_or_obj_perms:container.view_containerpullthroughremote", + "has_model_or_domain_or_obj_perms:container.change_containerpullthroughremote", + "has_model_or_domain_or_obj_perms:container.view_containerpullthroughremote", ], }, { @@ -474,8 +477,8 @@ class ContainerPullThroughRemoteViewSet(RemoteViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.delete_containerpullthroughremote", - "has_model_or_obj_perms:container.view_containerpullthroughremote", + "has_model_or_domain_or_obj_perms:container.delete_containerpullthroughremote", + "has_model_or_domain_or_obj_perms:container.view_containerpullthroughremote", ], }, { @@ -483,7 +486,7 @@ class ContainerPullThroughRemoteViewSet(RemoteViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.manage_roles_containerpullthroughremote" + "has_model_or_domain_or_obj_perms:container.manage_roles_containerpullthroughremote" # noqa ], }, ], @@ -602,9 +605,9 @@ def sign(self, request, pk): tags_list = serializer.validated_data.get("tags_list") if tags_list: - tags_list_pks = models.Tag.objects.filter(name__in=tags_list).values_list( - "pk", flat=True - ) + tags_list_pks = models.Tag.objects.filter( + name__in=tags_list, pulp_domain=get_domain() + ).values_list("pk", flat=True) tags_list_pks = list(tags_list_pks) else: tags_list_pks = None @@ -643,21 +646,21 @@ class ContainerRepositoryViewSet( "action": ["create"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:container.add_containerrepository", + "condition": "has_model_or_domain_perms:container.add_containerrepository", }, { "action": ["retrieve"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.view_containerrepository", + "condition": "has_model_or_domain_or_obj_perms:container.view_containerrepository", }, { "action": ["destroy"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.delete_containerrepository", - "has_model_or_obj_perms:container.view_containerrepository", + "has_model_or_domain_or_obj_perms:container.delete_containerrepository", + "has_model_or_domain_or_obj_perms:container.view_containerrepository", ], }, { @@ -665,8 +668,8 @@ class ContainerRepositoryViewSet( "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.change_containerrepository", - "has_model_or_obj_perms:container.view_containerrepository", + "has_model_or_domain_or_obj_perms:container.change_containerrepository", + "has_model_or_domain_or_obj_perms:container.view_containerrepository", ], }, { @@ -674,9 +677,9 @@ class ContainerRepositoryViewSet( "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.sync_containerrepository", - "has_remote_param_model_or_obj_perms:container.view_containerremote", - "has_model_or_obj_perms:container.view_containerrepository", + "has_model_or_domain_or_obj_perms:container.sync_containerrepository", + "has_remote_param_model_or_domain_or_obj_perms:container.view_containerremote", + "has_model_or_domain_or_obj_perms:container.view_containerrepository", ], }, { @@ -684,8 +687,8 @@ class ContainerRepositoryViewSet( "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.modify_content_containerrepository", - "has_model_or_obj_perms:container.view_containerrepository", + "has_model_or_domain_or_obj_perms:container.modify_content_containerrepository", + "has_model_or_domain_or_obj_perms:container.view_containerrepository", ], }, { @@ -693,16 +696,18 @@ class ContainerRepositoryViewSet( "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.build_image_containerrepository", - "has_model_or_obj_perms:container.view_containerrepository", - "has_repo_or_repo_ver_param_model_or_obj_perms:file.view_filerepository", + "has_model_or_domain_or_obj_perms:container.build_image_containerrepository", + "has_model_or_domain_or_obj_perms:container.view_containerrepository", + "has_repo_or_repo_ver_param_model_or_domain_or_obj_perms:file.view_filerepository", # noqa ], }, { "action": ["list_roles", "add_role", "remove_role"], "principal": "authenticated", "effect": "allow", - "condition": ["has_model_or_obj_perms:container.manage_roles_containerrepository"], + "condition": [ + "has_model_or_domain_or_obj_perms:container.manage_roles_containerrepository" + ], }, ], "creation_hooks": [ @@ -826,7 +831,7 @@ def remove(self, request, pk): """ Queues a task that creates a new RepositoryVersion by removing content units. """ - remove_content_units = [] + remove_content_units = {} repository = self.get_object() serializer = serializers.RecursiveManageSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -837,13 +842,17 @@ def remove(self, request, pk): remove_content_units = [url] break - content = NamedModelViewSet.get_resource(url, Content) - remove_content_units.append(str(content.pk)) + remove_content_units[extract_pk(url)] = url + else: + self.touch_content_units(remove_content_units) result = dispatch( tasks.recursive_remove_content, exclusive_resources=[repository], - kwargs={"repository_pk": str(repository.pk), "content_units": remove_content_units}, + kwargs={ + "repository_pk": str(repository.pk), + "content_units": list(remove_content_units.keys()), + }, ) return OperationPostponedResponse(result, request) @@ -975,15 +984,15 @@ class ContainerRepositoryVersionViewSet(RepositoryVersionViewSet): "action": ["list", "retrieve"], "principal": "authenticated", "effect": "allow", - "condition": "has_repository_model_or_obj_perms:container.view_containerrepository", + "condition": "has_repository_model_or_domain_or_obj_perms:container.view_containerrepository", # noqa }, { "action": ["destroy"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_repository_model_or_obj_perms:container.delete_containerrepository_versions", # noqa - "has_repository_model_or_obj_perms:container.view_containerrepository", + "has_repository_model_or_domain_or_obj_perms:container.delete_containerrepository_versions", # noqa + "has_repository_model_or_domain_or_obj_perms:container.view_containerrepository", # noqa ], }, { @@ -991,8 +1000,8 @@ class ContainerRepositoryVersionViewSet(RepositoryVersionViewSet): "principal": "authenticated", "effect": "allow", "condition": [ - "has_repository_model_or_obj_perms:container.delete_containerrepository", - "has_repository_model_or_obj_perms:container.view_containerrepository", + "has_repository_model_or_domain_or_obj_perms:container.delete_containerrepository", # noqa + "has_repository_model_or_domain_or_obj_perms:container.view_containerrepository", # noqa ], }, { @@ -1000,8 +1009,8 @@ class ContainerRepositoryVersionViewSet(RepositoryVersionViewSet): "principal": "authenticated", "effect": "allow", "condition": [ - "has_repository_model_or_obj_perms:container.sync_containerrepository", - "has_repository_model_or_obj_perms:container.view_containerrepository", + "has_repository_model_or_domain_or_obj_perms:container.sync_containerrepository", # noqa + "has_repository_model_or_domain_or_obj_perms:container.view_containerrepository", # noqa ], }, ], @@ -1133,23 +1142,25 @@ def get_push_repos_qs(self, qs, ns_perm, dist_perm): Returns a queryset by filtering by namespace permission to view distributions and distribution level permissions. """ - - qs = models.ContainerPushRepository.objects.all() + domain = get_domain() + qs = models.ContainerPushRepository.objects.filter(pulp_domain=domain) namespaces = get_objects_for_user( self.request.user, ns_perm, - models.ContainerNamespace.objects.all(), + models.ContainerNamespace.objects.filter(pulp_domain=domain), ) ns_repository_pks = models.ContainerDistribution.objects.filter( - namespace__in=namespaces + namespace__in=namespaces, + pulp_domain=domain, ).values_list("repository") dist_repository_pks = get_objects_for_user( self.request.user, dist_perm, - models.ContainerDistribution.objects.all(), + models.ContainerDistribution.objects.filter(pulp_domain=domain), ).values_list("repository") public_repository_pks = models.ContainerDistribution.objects.filter( - private=False + private=False, + pulp_domain=domain, ).values_list("repository") return qs.filter( Q(pk__in=ns_repository_pks) @@ -1295,7 +1306,7 @@ class ContainerDistributionViewSet(DistributionViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.manage_roles_containerdistribution" + "has_model_or_domain_or_obj_perms:container.manage_roles_containerdistribution" ], }, { @@ -1348,24 +1359,26 @@ def get_dist_qs(self, qs, ns_perm, dist_perm): """ Returns a queryset of distributions filtered by namespace permissions and public status. """ - - public_qs = models.ContainerDistribution.objects.filter(private=False) + domain = get_domain() + public_qs = models.ContainerDistribution.objects.filter(private=False, pulp_domain=domain) obj_perm_qs = get_objects_for_user( self.request.user, dist_perm, - models.ContainerDistribution.objects.all(), + models.ContainerDistribution.objects.filter(pulp_domain=domain), ) namespaces = get_objects_for_user( self.request.user, ns_perm, - models.ContainerNamespace.objects.all(), + models.ContainerNamespace.objects.filter(pulp_domain=domain), ) namespaces |= get_objects_for_user( self.request.user, dist_perm, - models.ContainerNamespace.objects.all(), + models.ContainerNamespace.objects.filter(pulp_domain=domain), + ) + ns_qs = models.ContainerDistribution.objects.filter( + namespace__in=namespaces, pulp_domain=domain ) - ns_qs = models.ContainerDistribution.objects.filter(namespace__in=namespaces) return public_qs | obj_perm_qs | ns_qs @extend_schema( @@ -1458,7 +1471,7 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.manage_roles_containerpullthroughdistribution" + "has_model_or_domain_or_obj_perms:container.manage_roles_containerpullthroughdistribution" # noqa ], }, { @@ -1466,7 +1479,7 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin): "principal": "authenticated", "effect": "allow", "condition_expression": [ - "has_model_or_obj_perms:container.pull_new_containerdistribution" + "has_model_or_domain_or_obj_perms:container.pull_new_containerdistribution" ], }, ], @@ -1530,7 +1543,7 @@ class ContainerNamespaceViewSet( "action": ["create"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_perms:container.add_containernamespace", + "condition": "has_model_or_domain_perms:container.add_containernamespace", }, { "action": ["create"], @@ -1542,36 +1555,36 @@ class ContainerNamespaceViewSet( "action": ["retrieve", "my_permissions"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.view_containernamespace", + "condition": "has_model_or_domain_or_obj_perms:container.view_containernamespace", }, { "action": ["destroy"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.delete_containernamespace", - "has_model_or_obj_perms:container.view_containernamespace", + "has_model_or_domain_or_obj_perms:container.delete_containernamespace", + "has_model_or_domain_or_obj_perms:container.view_containernamespace", ], }, { "action": ["create_distribution"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.namespace_add_containerdistribution", + "condition": "has_model_or_domain_or_obj_perms:container.namespace_add_containerdistribution", # noqa }, { "action": ["view_distribution"], "principal": "authenticated", "effect": "allow", "condition": [ - "has_model_or_obj_perms:container.namespace_view_containerdistribution" + "has_model_or_domain_or_obj_perms:container.namespace_view_containerdistribution" # noqa ], }, { "action": ["list_roles", "add_role", "remove_role"], "principal": "authenticated", "effect": "allow", - "condition": "has_model_or_obj_perms:container.manage_roles_containernamespace", + "condition": "has_model_or_domain_or_obj_perms:container.manage_roles_containernamespace", # noqa }, ], "creation_hooks": [