Skip to content

Commit

Permalink
TODO: Add a permission to pull from pull-through distributions
Browse files Browse the repository at this point in the history
closes #1624
  • Loading branch information
lubosmj committed Jul 5, 2024
1 parent 78ca84c commit dd6c2a4
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGES/1624.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Permitted users with the `pull_new_distributions` permission to pull data via pull-through
distributions.
4 changes: 4 additions & 0 deletions pulp_container/app/access_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def get_policy_statements(self, request, view):
access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="distributions/container/container"
)
elif isinstance(view.get_object(), models.ContainerPullThroughDistribution):
access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="distributions/container/pull-through"
)
else:
access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="pulp_container/namespaces"
Expand Down
43 changes: 37 additions & 6 deletions pulp_container/app/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@

from django.conf import settings
from django.http import HttpRequest
from django.db.models import F, Value
from rest_framework.request import Request

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

from pulp_container.app.models import ContainerDistribution, ContainerNamespace
from pulp_container.app.models import (
ContainerDistribution,
ContainerNamespace,
ContainerPullThroughDistribution,
)
from pulp_container.app.access_policy import RegistryAccessPolicy

TOKEN_EXPIRATION_TIME = settings.get("TOKEN_EXPIRATION_TIME", 300)
Expand All @@ -41,6 +46,15 @@ def __getattr__(self, *args, **kwargs):
FakeViewWithSerializer = partial(FakeView, get_serializer=get_serializer)


def get_pull_through_distribution(path):
return (
ContainerPullThroughDistribution.objects.annotate(path=Value(path))
.filter(path__startswith=F("base_path"))
.order_by("-base_path")
.first()
)


class AuthorizationService:
"""
A class responsible for generating and managing a Bearer token.
Expand Down Expand Up @@ -207,13 +221,30 @@ def has_pull_permissions(self, path):
try:
namespace = ContainerNamespace.objects.get(name=namespace_name)
except ContainerNamespace.DoesNotExist:
# Check if user is allowed to create a new namespace
# Check if the user is allowed to create a new namespace
return self.has_permission(None, "POST", "create", {"name": namespace_name})
# Check if user is allowed to view distributions in the namespace
return self.has_permission(
namespace, "GET", "view_distribution", {"name": namespace_name}
)

if pt_distribution := get_pull_through_distribution(path):
# Check if the user is allowed to create a new distribution
return (
self.has_permission(pt_distribution, "GET", "pull_new_distribution", {})
or
self.has_permission(namespace, "POST", "create_distribution", {})
)
else:
# Check if the user is allowed to view distributions in the namespace
return self.has_permission(
namespace, "GET", "view_distribution", {"name": namespace_name}
)

if pt_distribution := get_pull_through_distribution(path):
# Check if the user is allowed to pull content via a pull-through distribution
if self.has_permission(
pt_distribution, "GET", "pull_new_distribution", {"base_path": path}
):
return True

# Check if the user has general pull permissions
return self.has_permission(distribution, "GET", "pull", {"base_path": path})

def has_push_permissions(self, path):
Expand Down
9 changes: 4 additions & 5 deletions pulp_container/app/global_access_conditions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from logging import getLogger

from pulpcore.plugin.models import Repository
from pulpcore.plugin.models import Repository, RepositoryVersion
from pulpcore.plugin.viewsets import RepositoryVersionViewSet

from pulp_container.app import models
Expand All @@ -27,10 +27,9 @@ def has_namespace_obj_perms(request, view, action, permission):
for dist in obj.distributions.all():
if request.user.has_perm(permission, dist.cast().namespace):
return True
elif type(obj) is models.ContainerPushRepositoryVersion:
for dist in obj.repository.distributions.all():
if request.user.has_perm(permission, dist.cast().namespace):
return True
elif type(obj) is models.ContainerPullThroughDistribution:
namespace = obj.namespace
return request.user.has_perm(permission, namespace)
return False


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.11 on 2024-07-04 21:50

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('container', '0040_add_remote_repo_filter'),
]

operations = [
migrations.AlterModelOptions(
name='containerpullthroughdistribution',
options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_containerpullthroughdistribution', 'Can manage role assignments on pull-through cache distribution'), ('pull_new_containerdistribution', 'Can pull new content via the pull-through cache distribution')]},
),
]
4 changes: 4 additions & 0 deletions pulp_container/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,10 @@ class Meta:
"manage_roles_containerpullthroughdistribution",
"Can manage role assignments on pull-through cache distribution",
),
(
"pull_new_containerdistribution",
"Can pull new content via the pull-through cache distribution",
),
]


Expand Down
2 changes: 1 addition & 1 deletion pulp_container/app/registry_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,7 @@ def get_content_units_to_add(self, manifest, tag):
models.MEDIA_TYPE.MANIFEST_LIST,
models.MEDIA_TYPE.INDEX_OCI,
):
for listed_manifest in manifest.listed_manifests:
for listed_manifest in manifest.listed_manifests.all():
add_content_units.append(listed_manifest.pk)
add_content_units.append(listed_manifest.config_blob_id)
add_content_units.extend(listed_manifest.blobs.values_list("pk", flat=True))
Expand Down
19 changes: 19 additions & 0 deletions pulp_container/app/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1453,6 +1453,22 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin):
"has_model_or_obj_perms:container.manage_roles_containerpullthroughdistribution"
],
},
{
"action": ["pull_new_distribution"],
"principal": "*",
"effect": "allow",
"condition_expression": [
"not is_private",
],
},
{
"action": ["pull_new_distribution"],
"principal": "authenticated",
"effect": "allow",
"condition_expression": [
"has_model_or_obj_perms:container.pull_new_containerdistribution"
],
},
],
"creation_hooks": [
{
Expand All @@ -1472,12 +1488,15 @@ class ContainerPullThroughDistributionViewSet(DistributionViewSet, RolesMixin):
"container.delete_containerpullthroughdistribution",
"container.change_containerpullthroughdistribution",
"container.manage_roles_containerpullthroughdistribution",
"container.pull_new_containerdistribution",
],
"container.containerpullthroughdistribution_collaborator": [
"container.view_containerpullthroughdistribution",
"container.pull_new_containerdistribution",
],
"container.containerpullthroughdistribution_consumer": [
"container.view_containerpullthroughdistribution",
"container.pull_new_containerdistribution",
],
}

Expand Down
62 changes: 48 additions & 14 deletions pulp_container/tests/functional/api/test_pull_through_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,47 +117,81 @@ def test_anonymous_pull_by_digest(
anonymous_user,
local_registry,
pull_through_distribution,
gen_user,
):
image = f"{PULP_HELLO_WORLD_REPO}@{PULP_HELLO_WORLD_LINUX_AMD64_DIGEST}"
local_image_path = f"{pull_through_distribution().base_path}/{image}"
image1 = f"{PULP_HELLO_WORLD_REPO}@{PULP_HELLO_WORLD_LINUX_AMD64_DIGEST}"
image2 = f"{PULP_FIXTURE_1}:latest"
local_image_path1 = f"{pull_through_distribution().base_path}/{image1}"
local_image_path2 = f"{pull_through_distribution().base_path}/{image2}"

with anonymous_user, pytest.raises(CalledProcessError):
local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

add_pull_through_entities_to_cleanup(local_image_path.split("@")[0])
add_pull_through_entities_to_cleanup(local_image_path1.split("@")[0])

with anonymous_user:
local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

with gen_user():
local_registry.pull(local_image_path1)

with gen_user(), pytest.raises(CalledProcessError):
local_registry.pull(local_image_path2)

with gen_user(model_roles=["container.containernamespace_collaborator"]):
local_registry.pull(local_image_path1)
local_registry.pull(local_image_path2)


def test_pull_from_private_distribution(
delete_orphans_pre,
add_pull_through_entities_to_cleanup,
anonymous_user,
container_namespace_api,
local_registry,
pull_through_distribution,
gen_user,
):
if settings.TOKEN_AUTH_DISABLED:
pytest.skip("RBAC cannot be tested when token authentication is disabled")

image = f"{PULP_HELLO_WORLD_REPO}:latest"
local_image_path = f"{pull_through_distribution(private=True).base_path}/{image}"
pull_through_distribution = pull_through_distribution(private=True)

image1 = f"{PULP_HELLO_WORLD_REPO}:latest"
image2 = f"{PULP_FIXTURE_1}:manifest_a"
local_image_path1 = f"{pull_through_distribution.base_path}/{image1}"
local_image_path2 = f"{pull_through_distribution.base_path}/{image2}"

with anonymous_user, pytest.raises(CalledProcessError):
local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

add_pull_through_entities_to_cleanup(local_image_path.split(":")[0])
namespace = container_namespace_api.list(name=pull_through_distribution.base_path).results[0]
add_pull_through_entities_to_cleanup(local_image_path1.split(":")[0])

with anonymous_user, pytest.raises(CalledProcessError):
local_registry.pull(local_image_path)
local_registry.pull(local_image_path1)

with gen_user(model_roles=["container.containernamespace_collaborator"]):
local_registry.pull(local_image_path)
namespace_collaborator = gen_user(
object_roles=[("container.containernamespace_collaborator", namespace.pulp_href)]
)
with namespace_collaborator:
local_registry.pull(local_image_path1)

with gen_user(model_roles=["container.containerpullthroughdistribution_consumer"]):
local_registry.pull(local_image_path1)
local_registry.pull(local_image_path2)

add_pull_through_entities_to_cleanup(local_image_path2.split(":")[0])

with (
gen_user(model_roles=["container.containerdistribution_collaborator"]),
pytest.raises(CalledProcessError),
):
local_registry.pull(local_image_path2)


def test_conflicting_names_and_paths(
Expand Down

0 comments on commit dd6c2a4

Please sign in to comment.