Skip to content

Commit

Permalink
Adds granular permissions for push repositories
Browse files Browse the repository at this point in the history
The RegistryAccessPolicy is a combination of AccessPolicies for ContainerNamespace,
ContainerDistribution, and ContainerPushRepository viewsets.

The default policy requires users to have "container.add_containernamespace" and
"container.add_containerdistribution" permissions in order to push a new repository
into a new namespace. The permissions also enable user to use the Pulp API to create
a namespace and distribution.

The ContainerNamespaceViewset provides a create_namespace_group() function that can
be used to assign permissions to a group of users. The default permission assignment
for a namespace creates three groups Owners, Collaborators, and Consumers.

Namespace Owners get the following permissions:

     "container.view_containernamespace",
     "container.delete_containernamespace",
     "container.namespace_add_containerdistribution",
     "container.namespace_delete_containerdistribution",
     "container.namespace_view_containerdistribution",
     "container.namespace_pull_containerdistribution",
     "container.namespace_push_containerdistribution",
     "container.namespace_change_containerdistribution",
     "container.namespace_view_containerpushrepository",
     "container.namespace_modify_content_containerpushrepository"

Namespace Collaborators get the following permissions:

     "container.view_containernamespace",
     "container.namespace_add_containerdistribution",
     "container.namespace_delete_containerdistribution",
     "container.namespace_view_containerdistribution",
     "container.namespace_pull_containerdistribution",
     "container.namespace_push_containerdistribution",
     "container.namespace_change_containerdistribution",
     "container.namespace_view_containerpushrepository",
     "container.namespace_modify_content_containerpushrepository"

Namespace Consumers get the following permissions:

     "container.view_containernamespace",
     "container.namespace_view_containerdistribution",
     "container.namespace_pull_containerdistribution",
     "container.namespace_view_containerpushrepository",

The ContainerDistributionsViewset provides a create_distribution_group() function
that can be used to assign permissions to a group of users. The default permission
assignment for a newly created ContainerDistribution consists of three groups:
Owners, Collaborators, and Consumers.

ContainerDistribution Owners get the following permissions:

     "container.view_containerdistribution",
     "container.pull_containerdistribution",
     "container.push_containerdistribution",
     "container.delete_containerdistribution",
     "container.change_containerdistribution",
     "container.view_containerpushrepository",
     "container.modify_content_containerpushrepository"

ContainerDistribution Collaborators get the following permissions:

     "container.view_containerdistribution",
     "container.pull_containerdistribution",
     "container.push_containerdistribution",
     "container.view_containerpushrepository",
     "container.modify_content_containerpushrepository"

ContainerDistribution Consumers get the following permissions:

     "container.view_containerdistribution",
     "container.pull_containerdistribution",
     "container.view_containerpushrepository",

The ContainerPushRepositoryViewset provides an add_perms_to_distribution_group()
function to assign ContainerPushRepository permissions to the groups associated
with the ContainerDistribution that serves the specific ContainerPushRepository.

closes: #8101
https://pulp.plan.io/issues/8101
  • Loading branch information
dkliban committed Feb 4, 2021
1 parent 9c148dc commit 9e5d171
Show file tree
Hide file tree
Showing 9 changed files with 511 additions and 120 deletions.
1 change: 1 addition & 0 deletions CHANGES/8101.feature
@@ -0,0 +1 @@
Added Owner, Collaborator, and Consumer groups and permissions for Namespaces and Repositories.
77 changes: 37 additions & 40 deletions pulp_container/app/access_policy.py
Expand Up @@ -6,46 +6,37 @@
from pulp_container.app import models


class NamespacePermissionsChecker:
class NamespaceAccessPolicyMixin:
"""
A class that contains a function which checks permissions required for modifying namespaces.
Access policy mixin for ContainerDistributionViewSet and ContainerPushRepositoryViewSet which
handles namespace permissions.
"""

@staticmethod
def has_permissions(namespace, user, permission):
def has_namespace_obj_perms(self, request, view, action, permission):
"""
Check whether a user have permissions to manage the passed namespace.
Check if a user has object-level perms on the namespace associated with the distribution
or repository.
"""

try:
namespace = models.ContainerNamespace.objects.get(name=namespace)
except models.ContainerNamespace.DoesNotExist:
# check model permissions for namespace creation
return user.has_perm("container.add_containernamespace")
obj = view.get_object()
if type(obj) == models.ContainerDistribution:
namespace = obj.namespace
return request.user.has_perm(permission, namespace)
else:
# existing namespace
return user.has_perm(permission) or user.has_perm(permission, namespace)
dists_qs = models.ContainerDistribution.objects.filter(repository=obj)
for dist in dists_qs:
if request.user.has_perm(permission, dist.namespace):
return True
return False


class DistributionAccessPolicyMixin:
"""
Access policy mixin for DistributionViewSet which handles namespace permissions.
"""

def has_manage_namespace_dist_perms(self, request, view, action, permission):
def has_namespace_or_obj_perms(self, request, view, action, permission):
"""
Check whether a user can create a namespace or it can manage distributions
for existing namespace.
Check if a user has a namespace-level perms or object-level permission
"""
namespace = request.data["base_path"].split("/")[0]
return NamespacePermissionsChecker.has_permissions(namespace, request.user, permission)

def has_namespace_obj_perms(self, request, view, action, permission):
"""
Check if a user has object-level perms on the namespace associated with the distribution.
"""
namespace = view.get_object().namespace
return request.user.has_perm(permission, namespace)
ns_perm = "container.namespace_{}".format(permission.split(".", 1)[1])
if self.has_namespace_obj_perms(request, view, action, ns_perm):
return True
else:
return request.user.has_perm(permission, view.get_object())

def obj_exists(self, request, view, action):
"""
Expand All @@ -60,20 +51,22 @@ def is_private(self, request, view, action):
return view.get_object().private


class DistributionAccessPolicyFromDB(AccessPolicyFromDB, DistributionAccessPolicyMixin):
class NamespaceAccessPolicyFromDB(AccessPolicyFromDB, NamespaceAccessPolicyMixin):
"""
Access policy for DistributionViewSet which handles namespace permissions.
Access policy for ContainerDistributionViewSet and ContainerPushRepositoryViewSet which handles
namespace permissions.
"""


class RegistryAccessPolicy(AccessPolicy, DistributionAccessPolicyMixin):
class RegistryAccessPolicy(AccessPolicy, NamespaceAccessPolicyMixin):
"""
An AccessPolicy that loads statements from the container distribution viewset.
An AccessPolicy that loads statements from the ContainerDistribution, ContainerNamespace,
and ContainerPushRepository viewsets.
"""

def get_policy_statements(self, request, view):
"""
Return the policy statements for the container distribution viewset.
Return the policy statements for the container distribution and namespace viewsets.
Args:
request (rest_framework.request.Request): The request being checked for authorization.
Expand All @@ -83,8 +76,12 @@ def get_policy_statements(self, request, view):
The access policy statements in drf-access-policy policy structure.
"""

access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="distributions/container/container"
)
if isinstance(view.get_object(), models.ContainerDistribution):
access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="distributions/container/container"
)
else:
access_policy_obj = AccessPolicyModel.objects.get(
viewset_name="pulp_container/namespaces"
)
return access_policy_obj.statements
54 changes: 34 additions & 20 deletions pulp_container/app/authorization.py
Expand Up @@ -15,7 +15,7 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

from pulp_container.app.models import ContainerDistribution
from pulp_container.app.models import ContainerDistribution, ContainerNamespace
from pulp_container.app.access_policy import RegistryAccessPolicy

TOKEN_EXPIRATION_TIME = settings.get("TOKEN_EXPIRATION_TIME", 300)
Expand Down Expand Up @@ -151,23 +151,37 @@ def determine_access(self):

return {"type": typ, "name": name, "actions": list(permitted_actions)}

def has_permission(self, obj, method, action, data):
"""Check if user has permission to perform action."""

# Fake the request
request = Request(HttpRequest())
request.method = method
request.user = self.user
request._full_data = data
# Fake the corresponding view
view = namedtuple("FakeView", ["action", "get_object"])(action, lambda: obj)
return self.access_policy.has_permission(request, view)

def has_pull_permissions(self, path):
"""
Check if the user has permissions to pull from the repository specified by the path.
"""
try:
distribution = ContainerDistribution.objects.get(base_path=path)
except ContainerDistribution.DoesNotExist:
return False
namespace_name = path.split("/")[0]
try:
namespace = ContainerNamespace.objects.get(name=namespace_name)
except ContainerNamespace.DoesNotExist:
# Check if 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}
)

# Fake the request
request = Request(HttpRequest())
request.method = "GET"
request.user = self.user
request._full_data = {"base_path": path}
# Fake the corresponding view
view = namedtuple("FakeView", ["action", "get_object"])("pull", lambda: distribution)
return self.access_policy.has_permission(request, view)
return self.has_permission(distribution, "GET", "pull", {"base_path": path})

def has_push_permissions(self, path):
"""
Expand All @@ -176,16 +190,16 @@ def has_push_permissions(self, path):
try:
distribution = ContainerDistribution.objects.get(base_path=path)
except ContainerDistribution.DoesNotExist:
distribution = None

# Fake the request
request = Request(HttpRequest())
request.method = "POST"
request.user = self.user
request._full_data = {"base_path": path}
# Fake the corresponding view
view = namedtuple("FakeView", ["action", "get_object"])("push", lambda: distribution)
return self.access_policy.has_permission(request, view)
namespace_name = path.split("/")[0]
try:
namespace = ContainerNamespace.objects.get(name=namespace_name)
except ContainerNamespace.DoesNotExist:
# Check if user is allowed to create a new namespace
return self.has_permission(None, "POST", "create", {"name": namespace_name})
# 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})

def has_view_catalog_permissions(self, path):
"""
Expand Down
17 changes: 17 additions & 0 deletions pulp_container/app/migrations/0017_add_granular_perms.py
@@ -0,0 +1,17 @@
# Generated by Django 2.2.17 on 2021-02-04 02:18

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('container', '0016_add_delete_versions_permission'),
]

operations = [
migrations.AlterModelOptions(
name='containernamespace',
options={'permissions': [('namespace_add_containerdistribution', 'Add any distribution to a namespace'), ('namespace_delete_containerdistribution', 'Delete any distribution from a namespace'), ('namespace_view_containerdistribution', 'View any distribution in a namespace'), ('namespace_pull_containerdistribution', 'Pull from any distribution in a namespace'), ('namespace_push_containerdistribution', 'Push to any distribution in a namespace'), ('namespace_change_containerdistribution', 'Change any distribution in a namespace'), ('namespace_view_containerpushrepository', 'View any push repository in a namespace'), ('namespace_modify_content_containerpushrepository', 'Modify content in any push repository in a namespace')]},
),
]

0 comments on commit 9e5d171

Please sign in to comment.