Skip to content

Commit

Permalink
Enable using users in addition to groups for RBAC.
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 12, 2023
1 parent 5c72492 commit 96992b5
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 10 deletions.
46 changes: 46 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 @@ -74,6 +78,48 @@ def to_internal_value(self, data):
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:
rep.append({
'id': user.id,
'name': user.username,
'object_roles': value[user]
})
return rep

def to_internal_value(self, 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


class MyPermissionsField(serializers.Serializer):
def to_representation(self, original_obj):
request = self.context.get('request', None)
Expand Down
51 changes: 51 additions & 0 deletions galaxy_ng/app/access_control/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
assign_role,
remove_role,
get_groups_with_perms_attached_roles,
get_users_with_perms_attached_roles,
)

from django_lifecycle import hook
Expand Down Expand Up @@ -61,3 +62,53 @@ 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):
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]:
remove_role(perm, user, obj)

for user in users:
for role in users[user]:
try:
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)
11 changes: 9 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 Down Expand Up @@ -178,6 +184,7 @@ class Meta:
'avatar_url',
'description',
'groups',
'users',
'related_fields',
'metadata_sha256',
'avatar_sha256'
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
105 changes: 105 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,108 @@ 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 sorted(resp['users'][0]['object_roles']) == sorted(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 sorted(resp['users'][0]['object_roles']) == sorted(object_roles)
4 changes: 2 additions & 2 deletions requirements/requirements.common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,13 @@ psycopg[binary]==3.1.9
# via pulpcore
psycopg-binary==3.1.9
# via psycopg
pulp-ansible==0.19.0
pulp-ansible==0.20.1
# via galaxy-ng (setup.py)
pulp-container==2.15.2
# via galaxy-ng (setup.py)
pulp-glue==0.19.5
# via pulpcore
pulpcore==3.28.12
pulpcore==3.28.17
# via
# galaxy-ng (setup.py)
# pulp-ansible
Expand Down
4 changes: 2 additions & 2 deletions requirements/requirements.insights.txt
Original file line number Diff line number Diff line change
Expand Up @@ -325,13 +325,13 @@ psycopg[binary]==3.1.9
# via pulpcore
psycopg-binary==3.1.9
# via psycopg
pulp-ansible==0.19.0
pulp-ansible==0.20.1
# via galaxy-ng (setup.py)
pulp-container==2.15.2
# via galaxy-ng (setup.py)
pulp-glue==0.19.5
# via pulpcore
pulpcore==3.28.12
pulpcore==3.28.17
# via
# galaxy-ng (setup.py)
# pulp-ansible
Expand Down
4 changes: 2 additions & 2 deletions requirements/requirements.standalone.txt
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,13 @@ psycopg[binary]==3.1.9
# via pulpcore
psycopg-binary==3.1.9
# via psycopg
pulp-ansible==0.19.0
pulp-ansible==0.20.1
# via galaxy-ng (setup.py)
pulp-container==2.15.2
# via galaxy-ng (setup.py)
pulp-glue==0.19.5
# via pulpcore
pulpcore==3.28.12
pulpcore==3.28.17
# via
# galaxy-ng (setup.py)
# pulp-ansible
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _format_pulp_requirement(plugin, specifier=None, ref=None, gh_namespace="pul
requirements = [
"galaxy-importer>=0.4.13,<0.5.0",
"pulpcore>=3.28.12,<3.29.0",
"pulp_ansible>=0.19.0,<0.20.0",
"pulp_ansible>=0.20.0,<0.21.0",
"django-prometheus>=2.0.0",
"drf-spectacular",
"pulp-container>=2.15.0,<2.16.0",
Expand Down

0 comments on commit 96992b5

Please sign in to comment.