Skip to content

Commit

Permalink
Allow users and groups for creating namespaces.
Browse files Browse the repository at this point in the history
No-Issue

Signed-off-by: James Tanner <[email protected]>
  • Loading branch information
jctanner committed Oct 10, 2023
1 parent d5d0e27 commit ca66e49
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 3 deletions.
50 changes: 50 additions & 0 deletions galaxy_ng/app/access_control/fields.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import get_user_model

from rest_framework import serializers
from rest_framework.exceptions import ValidationError
Expand All @@ -10,6 +11,9 @@
from galaxy_ng.app.models import auth as auth_models


User = get_user_model()


class GroupPermissionField(serializers.Field):
def _validate_group(self, group_data):
if 'object_roles' not in group_data:
Expand Down Expand Up @@ -71,6 +75,52 @@ def to_internal_value(self, data):
except ValueError:
raise ValidationError(detail={'group': _('Invalid group name or ID')})

print(f'GROUP FIELD INTERNAL FINAL: {internal}')
return internal


class UserPermissionField(serializers.Field):

def _validate_user(self, user_data):
# FIXME - fill this in ...
pass

def to_representation(self, value):
rep = []
for user in value:
print(f'TO_REP: {value}')
rep.append({
'id': user.id,
'name': user.username,
'object_roles': value[user]
})
return rep

def to_internal_value(self, data):
print(f'UserPermissionField data:{data}')

if not isinstance(data, list):
raise ValidationError(detail={
'users': _('Users must be a list of user objects')
})

internal = {}
for user_data in data:
self._validate_user(user_data)
user_filter = {}
for field in user_data:
if field in ('id', 'username'):
user_filter[field] = user_data[field]

user = User.objects.filter(**user_filter).first()
if not user:
raise ValidationError(detail={'user': _('Invalid user username or ID')})

if 'object_permissions' in user_data:
internal[user] = user_data['object_permissions']
if 'object_roles' in user_data:
internal[user] = user_data['object_roles']

return internal


Expand Down
57 changes: 57 additions & 0 deletions galaxy_ng/app/access_control/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@
assign_role,
remove_role,
get_groups_with_perms_attached_roles,
# get_users_with_perms_attached_roles,
)

# FIXME - workaround for https://github.com/pulp/pulpcore/pull/4479
from galaxy_ng.app.utils.pulp_rbac import get_users_with_perms_attached_roles

from django_lifecycle import hook


Expand Down Expand Up @@ -61,3 +65,56 @@ def _set_groups(self, groups):
def set_object_groups(self):
if self._groups:
self._set_groups(self._groups)


class UserModelPermissionsMixin:
_users = None

@property
def users(self):
return get_users_with_perms_attached_roles(
self, include_model_permissions=False, for_concrete_model=True)

@users.setter
def users(self, users):
self._set_users(users)

@transaction.atomic
def _set_users(self, users):
print(f'SETTING USERS ... {users}')
if self._state.adding:
self._users = users
else:
obj = self

# If the model is a proxy model, get the original model since pulp
# doesn't allow us to assign permissions to proxied models.
if self._meta.proxy:
obj = self._meta.concrete_model.objects.get(pk=self.pk)

current_users = get_users_with_perms_attached_roles(
obj, include_model_permissions=False)
for user in current_users:
for perm in current_users[user]:
print(f'REMOVE_ROLE perm:{perm} user:{user} obj:{obj}')
remove_role(perm, user, obj)

for user in users:
for role in users[user]:
try:
print(f'ASSIGN ROLE role:{role} user:{user} obj:{obj}')
assign_role(role, user, obj)
except BadRequest:
raise ValidationError(
detail={
'users': _(
'Role {role} does not exist or does not '
'have any permissions related to this object.'
).format(role=role)
}
)

@hook('after_save')
def set_object_users(self):
if self._users:
self._set_users(self._users)
16 changes: 14 additions & 2 deletions galaxy_ng/app/api/v3/serializers/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@

from galaxy_ng.app import models
from galaxy_ng.app.tasks import dispatch_create_pulp_namespace_metadata
from galaxy_ng.app.access_control.fields import GroupPermissionField, MyPermissionsField
from galaxy_ng.app.access_control.fields import (
GroupPermissionField,
UserPermissionField,
MyPermissionsField
)
from galaxy_ng.app.api.base import RelatedFieldsBaseSerializer

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,7 +77,8 @@ def validate_url(self, url):

class NamespaceSerializer(serializers.ModelSerializer):
links = NamespaceLinkSerializer(many=True, required=False)
groups = GroupPermissionField()
groups = GroupPermissionField(required=False)
users = UserPermissionField(required=False)
related_fields = NamespaceRelatedFieldSerializer(source="*")
avatar_url = fields.URLField(required=False, allow_blank=True)
avatar_sha256 = serializers.SerializerMethodField()
Expand All @@ -93,6 +98,7 @@ class Meta:
'description',
'links',
'groups',
'users',
'resources',
'related_fields',
'metadata_sha256',
Expand All @@ -101,6 +107,7 @@ class Meta:

# replace with a NamespaceNameSerializer and validate_name() ?
def validate_name(self, name):
print(f'NamespaceSerializer.validate_name({name})')
if not name:
raise ValidationError(detail={
'name': _("Attribute 'name' is required")})
Expand All @@ -122,9 +129,13 @@ def get_avatar_sha256(self, obj):

@transaction.atomic
def create(self, validated_data):

print(f'NamespaceSerializer.create({validated_data})')

links_data = validated_data.pop('links', [])

instance = models.Namespace.objects.create(**validated_data)
print(f'NamespaceSerializer instance:{instance}')

# create NamespaceLink objects if needed
new_links = []
Expand Down Expand Up @@ -178,6 +189,7 @@ class Meta:
'avatar_url',
'description',
'groups',
'users',
'related_fields',
'metadata_sha256',
'avatar_sha256'
Expand Down
1 change: 1 addition & 0 deletions galaxy_ng/app/api/v3/viewsets/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def create(self, request: Request, *args, **kwargs) -> Response:
raise ConflictError(
detail={'name': _('A namespace named %s already exists.') % name}
)
print(f'NAMESPACEVIEWSET.CREATE -> ARGS:{args} KWARGS:{kwargs} REQUEST.DATA:{request.data}')
return super().create(request, *args, **kwargs)

@transaction.atomic
Expand Down
6 changes: 5 additions & 1 deletion galaxy_ng/app/models/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@
__all__ = ("Namespace", "NamespaceLink")


class Namespace(LifecycleModel, mixins.GroupModelPermissionsMixin):
class Namespace(
LifecycleModel,
mixins.GroupModelPermissionsMixin,
mixins.UserModelPermissionsMixin
):
"""
A model representing Ansible content namespace.
Expand Down
45 changes: 45 additions & 0 deletions galaxy_ng/app/utils/pulp_rbac.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from collections import defaultdict

from django.db.models import Q
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

from pulpcore.plugin.models.role import GroupRole
from pulpcore.plugin.models.role import UserRole


def get_users_with_perms_attached_roles(
obj,
with_group_users=True,
only_with_perms_in=None,
include_domain_permissions=True,
include_model_permissions=True,
for_concrete_model=False,
):
# DELETE ONCE https://github.com/pulp/pulpcore/pull/4479 IS RELEASED AND BUMPED IN GALAXY_NG
ctype = ContentType.objects.get_for_model(obj, for_concrete_model=for_concrete_model)
perms = Permission.objects.filter(content_type__pk=ctype.id)
if only_with_perms_in:
codenames = [
split_perm[-1]
for split_perm in (perm.split(".", maxsplit=1) for perm in only_with_perms_in)
if len(split_perm) == 1 or split_perm[0] == ctype.app_label
]
perms = perms.filter(codename__in=codenames)

object_query = Q(content_type=ctype, object_id=obj.pk)
if include_domain_permissions and getattr(obj, "pulp_domain", None):
object_query = Q(domain=obj.pulp_domain_id) | object_query
if include_model_permissions:
object_query = Q(object_id=None) | object_query

user_roles = UserRole.objects.filter(role__permissions__in=perms).filter(object_query)
res = defaultdict(set)
for user_role in user_roles:
res[user_role.user].add(user_role.role.name)
if with_group_users:
group_roles = GroupRole.objects.filter(role__permissions__in=perms).filter(object_query)
for group_role in group_roles:
for user in group_role.group.user_set.all():
res[user].add(group_role.role.name)
return {k: list(v) for k, v in res.items()}
106 changes: 106 additions & 0 deletions galaxy_ng/tests/integration/api/test_namespace_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,109 @@ def test_namespace_create_and_delete(ansible_config, api_version):
existing3 = get_all_namespaces(api_client=api_client, api_version=api_version)
existing3 = dict((x['name'], x) for x in existing3)
assert new_namespace not in existing3


@pytest.mark.galaxyapi_smoke
@pytest.mark.namespace
@pytest.mark.all
@pytest.mark.parametrize(
"user_property",
[
'id',
'username'
]
)
def test_namespace_create_with_user(ansible_config, user_property):
config = ansible_config("partner_engineer")
api_client = get_client(config, request_token=True, require_auth=True)
api_prefix = config.get("api_prefix").rstrip("/")

# find this client's user info...
me = api_client(f'{api_prefix}/_ui/v1/me/')
username = me['username']

new_namespace = generate_unused_namespace(api_client=api_client)

# make a namespace with a user and without defining groups ...
object_roles = [
'galaxy.collection_namespace_owner',
'galaxy.collection_publisher'
]
payload = {
'name': new_namespace,
'users': [
{
user_property: me.get(user_property),
'object_roles': object_roles,
}
]
}
resp = api_client(f'{api_prefix}/_ui/v1/my-namespaces/', args=payload, method='POST')

# should have the right results ...
assert resp['name'] == new_namespace
assert resp['groups'] == []
assert resp['users'] != []
assert username in [x['name'] for x in resp['users']]
assert username in [x['name'] for x in resp['users']]
assert resp['users'][0]['object_roles'] == object_roles


@pytest.mark.galaxyapi_smoke
@pytest.mark.namespace
@pytest.mark.all
@pytest.mark.parametrize(
"user_property",
[
'id',
'username'
]
)
def test_namespace_edit_with_user(ansible_config, user_property):
config = ansible_config("partner_engineer")
api_client = get_client(config, request_token=True, require_auth=True)
api_prefix = config.get("api_prefix").rstrip("/")

# find this client's user info...
me = api_client(f'{api_prefix}/_ui/v1/me/')
username = me['username']

new_namespace = generate_unused_namespace(api_client=api_client)

# make a namespace without users and without groups ...
payload = {
'name': new_namespace,
}
resp = api_client(f'{api_prefix}/_ui/v1/my-namespaces/', args=payload, method='POST')

# should have the right results ...
assert resp['name'] == new_namespace
assert resp['groups'] == []
assert resp['users'] == []

# now edit the namespace to add the user
object_roles = [
'galaxy.collection_namespace_owner',
'galaxy.collection_publisher'
]
payload = {
'name': new_namespace,
'users': [
{
user_property: me.get(user_property),
'object_roles': object_roles,
}
]
}
resp = api_client(
f'{api_prefix}/_ui/v1/my-namespaces/{new_namespace}/',
args=payload,
method='PUT'
)

# should have the right results ...
assert resp['name'] == new_namespace
assert resp['groups'] == []
assert resp['users'] != []
assert username in [x['name'] for x in resp['users']]
assert resp['users'][0]['object_roles'] == object_roles

0 comments on commit ca66e49

Please sign in to comment.