diff --git a/galaxy_ng/app/access_control/fields.py b/galaxy_ng/app/access_control/fields.py index d898dccf9c..708978c111 100644 --- a/galaxy_ng/app/access_control/fields.py +++ b/galaxy_ng/app/access_control/fields.py @@ -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 @@ -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: @@ -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 diff --git a/galaxy_ng/app/access_control/mixins.py b/galaxy_ng/app/access_control/mixins.py index 52e37a01e8..26e1a46e9f 100644 --- a/galaxy_ng/app/access_control/mixins.py +++ b/galaxy_ng/app/access_control/mixins.py @@ -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 @@ -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) diff --git a/galaxy_ng/app/api/v3/serializers/namespace.py b/galaxy_ng/app/api/v3/serializers/namespace.py index 0742d3debc..289cd6c5be 100644 --- a/galaxy_ng/app/api/v3/serializers/namespace.py +++ b/galaxy_ng/app/api/v3/serializers/namespace.py @@ -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__) @@ -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() @@ -93,6 +98,7 @@ class Meta: 'description', 'links', 'groups', + 'users', 'resources', 'related_fields', 'metadata_sha256', @@ -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")}) @@ -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 = [] @@ -178,6 +189,7 @@ class Meta: 'avatar_url', 'description', 'groups', + 'users', 'related_fields', 'metadata_sha256', 'avatar_sha256' diff --git a/galaxy_ng/app/api/v3/viewsets/namespace.py b/galaxy_ng/app/api/v3/viewsets/namespace.py index 5a9c559729..40c6e721f7 100644 --- a/galaxy_ng/app/api/v3/viewsets/namespace.py +++ b/galaxy_ng/app/api/v3/viewsets/namespace.py @@ -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 diff --git a/galaxy_ng/app/models/namespace.py b/galaxy_ng/app/models/namespace.py index 2b7dbdcd38..b724d0d347 100644 --- a/galaxy_ng/app/models/namespace.py +++ b/galaxy_ng/app/models/namespace.py @@ -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. diff --git a/galaxy_ng/app/utils/pulp_rbac.py b/galaxy_ng/app/utils/pulp_rbac.py new file mode 100644 index 0000000000..0349d1675c --- /dev/null +++ b/galaxy_ng/app/utils/pulp_rbac.py @@ -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()} diff --git a/galaxy_ng/tests/integration/api/test_namespace_management.py b/galaxy_ng/tests/integration/api/test_namespace_management.py index 7405af12ec..393c1a962c 100644 --- a/galaxy_ng/tests/integration/api/test_namespace_management.py +++ b/galaxy_ng/tests/integration/api/test_namespace_management.py @@ -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