Skip to content

Commit f693b61

Browse files
committed
Use api_slug in serializers
1 parent f881db3 commit f693b61

File tree

9 files changed

+40
-164
lines changed

9 files changed

+40
-164
lines changed

ansible_base/rbac/api/serializers.py

Lines changed: 23 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
from django.core.exceptions import ObjectDoesNotExist
33
from django.db import transaction
44
from django.db.utils import IntegrityError
5-
from django.utils.functional import cached_property
65
from django.utils.translation import gettext_lazy as _
76
from rest_framework import serializers
87
from rest_framework.exceptions import PermissionDenied
9-
from rest_framework.fields import flatten_choices_dict, to_choices_dict
108
from rest_framework.serializers import ValidationError
119

1210
from ansible_base.lib.abstract_models.common import get_url_for_object
@@ -16,117 +14,29 @@
1614
from ansible_base.rbac.policies import check_content_obj_permission, visible_users
1715
from ansible_base.rbac.validators import check_locally_managed, validate_permissions_for_model
1816

19-
from ..remote import get_resource_prefix
20-
21-
22-
class ChoiceLikeMixin(serializers.ChoiceField):
23-
"""
24-
This uses a ForeignKey to populate the choices of a choice field.
25-
This also manages some string manipulation, right now, adding the local service name.
26-
"""
27-
28-
default_error_messages = serializers.PrimaryKeyRelatedField.default_error_messages
29-
30-
def get_dynamic_choices(self):
31-
raise NotImplementedError
32-
33-
def get_dynamic_object(self, data):
34-
raise NotImplementedError
35-
36-
def to_representation(self, value):
37-
raise NotImplementedError
38-
39-
def __init__(self, **kwargs):
40-
# Workaround so that the parent class does not resolve the choices right away
41-
self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff)
42-
self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text)
43-
44-
self.allow_blank = kwargs.pop('allow_blank', False)
45-
super(serializers.ChoiceField, self).__init__(**kwargs)
46-
47-
def _initialize_choices(self):
48-
choices = self.get_dynamic_choices()
49-
self._grouped_choices = to_choices_dict(choices)
50-
self._choices = flatten_choices_dict(self._grouped_choices)
51-
self.choice_strings_to_values = {str(k): k for k in self._choices}
52-
53-
@cached_property
54-
def grouped_choices(self):
55-
self._initialize_choices()
56-
return self._grouped_choices
57-
58-
@cached_property
59-
def choices(self):
60-
self._initialize_choices()
61-
return self._choices
62-
63-
def to_internal_value(self, data):
64-
try:
65-
return self.get_dynamic_object(data)
66-
except ObjectDoesNotExist:
67-
self.fail('does_not_exist', pk_value=data)
68-
except (TypeError, ValueError):
69-
self.fail('incorrect_type', data_type=type(data).__name__)
70-
71-
72-
class ContentTypeField(ChoiceLikeMixin):
73-
def __init__(self, **kwargs):
74-
kwargs['help_text'] = _('The type of resource this applies to.')
75-
super().__init__(**kwargs)
76-
77-
def get_resource_type_name(self, cls) -> str:
78-
return f"{get_resource_prefix(cls)}.{cls._meta.model_name}"
79-
80-
def get_dynamic_choices(self):
81-
return list(sorted((self.get_resource_type_name(cls), cls._meta.verbose_name.title()) for cls in permission_registry.all_registered_models))
82-
83-
def get_dynamic_object(self, data):
84-
model = data.rsplit('.')[-1]
85-
cls = permission_registry.get_model_by_name(model)
86-
if cls is None:
87-
return permission_registry.content_type_model.objects.none().get() # raises correct DoesNotExist
88-
return permission_registry.content_type_model.objects.get_for_model(cls)
89-
90-
def to_representation(self, value):
91-
if isinstance(value, str):
92-
return value # slight hack to work to AWX schema tests
93-
return self.get_resource_type_name(value.model_class())
94-
95-
96-
class PermissionField(ChoiceLikeMixin):
97-
def get_dynamic_choices(self):
98-
perms = []
99-
for cls in permission_registry.all_registered_models:
100-
cls_name = cls._meta.model_name
101-
for action in cls._meta.default_permissions:
102-
perms.append(f'{get_resource_prefix(cls)}.{action}_{cls_name}')
103-
for perm_name, description in cls._meta.permissions:
104-
perms.append(f'{get_resource_prefix(cls)}.{perm_name}')
105-
return list(sorted(perms))
106-
107-
def get_dynamic_object(self, data):
108-
codename = data.rsplit('.')[-1]
109-
return permission_registry.permission_qs.get(codename=codename)
110-
111-
def to_representation(self, value):
112-
if isinstance(value, str):
113-
return value # slight hack to work to AWX schema tests
114-
ct = permission_registry.content_type_model.objects.get_for_id(value.content_type_id) # optimization
115-
return f'{get_resource_prefix(ct.model_class())}.{value.codename}'
116-
117-
118-
class ManyRelatedListField(serializers.ListField):
119-
def to_representation(self, data):
120-
"Adds the .all() to treat the value as a queryset"
121-
return [self.child.to_representation(item) if item is not None else None for item in data.all()]
17+
from ..models import DABContentType, DABPermission
12218

12319

12420
class RoleDefinitionSerializer(CommonModelSerializer):
125-
# Relational versions - we may switch to these if custom permission and type models are exposed but out of scope here
126-
# permissions = serializers.SlugRelatedField(many=True, slug_field='codename', queryset=DABPermission.objects.all())
127-
# content_type = ContentTypeField(slug_field='model', queryset=permission_registry.content_type_model.objects.all(), allow_null=True, default=None)
128-
permissions = ManyRelatedListField(child=PermissionField())
129-
content_type = ContentTypeField(allow_null=True, default=None)
21+
permissions = serializers.SlugRelatedField(
22+
slug_field='api_slug',
23+
queryset=DABPermission.objects.all(),
24+
many=True,
25+
error_messages={
26+
'does_not_exist': "Cannot use permission with api_slug '{value}', object does not exist",
27+
'invalid': "Each content type must be a valid slug string",
28+
},
29+
)
30+
content_type = serializers.SlugRelatedField(
31+
slug_field='api_slug',
32+
queryset=DABContentType.objects.all(),
33+
allow_null=True, # for global roles
34+
default=None,
35+
error_messages={
36+
'does_not_exist': "Cannot use type with api_slug '{value}', object does not exist",
37+
'invalid': "Each content type must be a valid slug string",
38+
},
39+
)
13040

13141
class Meta:
13242
model = RoleDefinition
@@ -141,7 +51,7 @@ def validate(self, validated_data):
14151
permissions = list(self.instance.permissions.all())
14252
if 'content_type' in validated_data:
14353
content_type = validated_data['content_type']
144-
else:
54+
elif self.instance:
14555
content_type = self.instance.content_type
14656
validate_permissions_for_model(permissions, content_type)
14757
if getattr(self, 'instance', None):
@@ -150,11 +60,11 @@ def validate(self, validated_data):
15060

15161

15262
class RoleDefinitionDetailSerializer(RoleDefinitionSerializer):
153-
content_type = ContentTypeField(read_only=True)
63+
content_type = serializers.SlugRelatedField(slug_field='api_slug', read_only=True)
15464

15565

15666
class BaseAssignmentSerializer(CommonModelSerializer):
157-
content_type = ContentTypeField(read_only=True)
67+
content_type = serializers.SlugRelatedField(slug_field='api_slug', read_only=True)
15868
object_ansible_id = serializers.UUIDField(
15969
required=False,
16070
help_text=_('The resource id of the object this role applies to. An alternative to the object_id field.'),
Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import pytest
22

3-
from ansible_base.lib.utils.response import get_relative_url
43
from ansible_base.rbac.api.serializers import RoleDefinitionSerializer
54

65

76
@pytest.mark.django_db
87
def test_invalid_content_type(admin_api_client):
98
serializer = RoleDefinitionSerializer(
10-
data=dict(name='foo-role-def', description='bar', permissions=['aap.view_organization'], content_type='aap.foo_does_not_exist_model')
9+
data=dict(name='foo-role-def', description='bar', permissions=['shared.view_organization'], content_type='aap.foo_does_not_exist_model')
1110
)
1211
assert not serializer.is_valid()
1312
assert 'object does not exist' in str(serializer.errors['content_type'])
@@ -22,18 +21,3 @@ def test_invalid_permission(admin_api_client):
2221
assert not serializer.is_valid()
2322
assert 'object does not exist' in str(serializer.errors['permissions'])
2423
assert 'content_type' not in serializer.errors
25-
26-
27-
@pytest.mark.django_db
28-
def test_parity_with_resource_registry(admin_api_client):
29-
types_resp = admin_api_client.get(get_relative_url("resourcetype-list"))
30-
assert types_resp.status_code == 200
31-
res_types = set(r['name'] for r in types_resp.data['results'])
32-
33-
role_types = admin_api_client.options(get_relative_url("roledefinition-list"))
34-
role_types = set(item['value'] for item in role_types.data['actions']['POST']['content_type']['choices'])
35-
36-
# Check the types in both registries
37-
for type_name in ('shared.organization', 'shared.team'):
38-
assert type_name in res_types
39-
assert type_name in role_types

test_app/tests/rbac/api/test_rbac_validation.py

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ def test_custom_roles_for_shared_stuff_not_allowed(self, admin_api_client, allow
4848
url,
4949
data={
5050
'name': 'Alternative Organization Admin Role in Local Server',
51-
'content_type': 'aap.organization',
52-
'permissions': ['aap.view_organization', 'local.change_organization'],
51+
'content_type': 'shared.organization',
52+
'permissions': ['shared.view_organization', 'shared.change_organization'],
5353
},
5454
)
5555
if allowed is False:
@@ -77,8 +77,8 @@ def test_org_resource_roles_creatable(self, admin_api_client):
7777
url,
7878
data={
7979
'name': 'Custom Organization Inventory Admin Role',
80-
'content_type': 'aap.organization',
81-
'permissions': ['aap.view_organization', 'local.change_inventory', 'local.view_inventory'],
80+
'content_type': 'shared.organization',
81+
'permissions': ['shared.view_organization', 'aap.change_inventory', 'aap.view_inventory'],
8282
},
8383
)
8484
assert response.status_code == 201, response.data
@@ -186,22 +186,3 @@ def test_callback_validate_role_team_assignment(admin_api_client, inventory, org
186186
response = admin_api_client.post(url, data={'object_id': inventory.id, 'team': team.id, 'role_definition': inv_rd.id})
187187
assert response.status_code == 403
188188
assert "Role assignment not allowed 403" in str(response.data)
189-
190-
191-
@pytest.mark.django_db
192-
def test_unregistered_model_type_for_assignment(admin_api_client, inventory, inv_rd, user):
193-
url = get_relative_url('roleuserassignment-list')
194-
# force invalid state of role definition, should not really happen
195-
cls = apps.get_model('test_app.secretcolor')
196-
inv_rd.content_type = permission_registry.content_type_model.objects.get_for_model(cls)
197-
inv_rd.save(update_fields=['content_type'])
198-
r = admin_api_client.post(url, data={'object_id': inventory.pk, 'user': user.pk, 'role_definition': inv_rd.pk})
199-
assert r.status_code == 400
200-
assert 'Given role definition is for a model not registered in the permissions system' in str(r.data)
201-
202-
# Temporarily abusing user model to test this via ansible_id reference, this testing method may not work forever
203-
inv_rd.content_type = permission_registry.content_type_model.objects.get_for_model(user)
204-
inv_rd.save(update_fields=['content_type'])
205-
r = admin_api_client.post(url, data={'object_ansible_id': str(user.resource.ansible_id), 'user': user.pk, 'role_definition': inv_rd.pk})
206-
assert r.status_code == 400
207-
assert 'user type is not registered with DAB RBAC' in str(r.data)

test_app/tests/rbac/api/test_rbac_views.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ def test_create_role_definition(admin_api_client):
1919
Test creation of a custom role definition.
2020
"""
2121
url = get_relative_url("roledefinition-list")
22-
data = dict(name='foo-role-def', description='bar', permissions=['aap.view_organization', 'aap.change_organization'], content_type='shared.organization')
22+
data = dict(
23+
name='foo-role-def', description='bar', permissions=['shared.view_organization', 'shared.change_organization'], content_type='shared.organization'
24+
)
2325
response = admin_api_client.post(url, data=data, format="json")
2426
assert response.status_code == 201, response.data
2527
assert response.data['name'] == 'foo-role-def'
@@ -28,7 +30,7 @@ def test_create_role_definition(admin_api_client):
2830
@pytest.mark.django_db
2931
def test_create_global_role_definition(admin_api_client):
3032
url = get_relative_url("roledefinition-list")
31-
data = dict(name='global_view_org', description='bar', permissions=['aap.view_organization'])
33+
data = dict(name='global_view_org', description='bar', permissions=['shared.view_organization'])
3234
response = admin_api_client.post(url, data=data, format="json")
3335
assert response.status_code == 201, response.data
3436
assert response.data['name'] == 'global_view_org'

test_app/tests/rbac/features/test_creator_permission.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ def test_custom_creation_perms(rando, inventory):
6464

6565

6666
@pytest.mark.django_db
67-
def test_creator_permission_for_unregistered_model(admin_api_client, inventory, inv_rd, user):
67+
def test_creator_permission_for_unregistered_model(rando):
6868
prior_ct = DABContentType.objects.count()
6969
prior_assignments = RoleUserAssignment.objects.count()
7070
prior_rds = RoleDefinition.objects.count()
7171

7272
cls = apps.get_model('test_app.secretcolor')
7373
obj = cls.objects.create()
74-
RoleDefinition.objects.give_creator_permissions(user, obj) # should do nothing
74+
RoleDefinition.objects.give_creator_permissions(rando, obj) # should do nothing
7575

7676
assert DABContentType.objects.count() == prior_ct # did not create anything
7777
assert RoleUserAssignment.objects.count() == prior_assignments

test_app/tests/rbac/features/test_public_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def test_org_level_validator_without_view():
4545
@pytest.mark.django_db
4646
def test_custom_role_for_public_model(admin_api_client, rando, public_item):
4747
url = get_relative_url('roledefinition-list')
48-
data = {'name': 'Public data editor', 'permissions': ['local.change_publicdata'], 'content_type': 'local.publicdata'}
48+
data = {'name': 'Public data editor', 'permissions': ['aap.change_publicdata'], 'content_type': 'aap.publicdata'}
4949
response = admin_api_client.post(url, data=data, format="json")
5050
assert response.status_code == 201, response.data
5151

test_app/tests/rbac/models/test_content_type.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,16 +79,15 @@ def test_lookup_cache(self):
7979
DABContentType.objects.get_for_model(Inventory)
8080

8181
@isolate_apps("tests")
82-
def test_get_for_model_create_contenttype(self):
82+
def test_get_for_model_not_registered(self):
8383
class ModelCreatedOnTheFly(models.Model):
8484
name = models.CharField(max_length=10)
8585

8686
class Meta:
8787
app_label = "tests"
8888

89-
ct = DABContentType.objects.get_for_model(ModelCreatedOnTheFly)
90-
assert ct.app_label == "tests"
91-
assert ct.model == "modelcreatedonthefly"
89+
with pytest.raises(RuntimeError):
90+
DABContentType.objects.get_for_model(ModelCreatedOnTheFly)
9291

9392

9493
@pytest.mark.django_db

test_app/tests/rbac/remote/test_remote_assignment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from ansible_base.rbac.models import DABContentType, DABPermission, RoleDefinition, RoleUserAssignment
3+
from ansible_base.rbac.models import RoleUserAssignment
44
from ansible_base.rbac.remote import RemoteObject
55

66

test_app/tests/rbac/test_validators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def test_custom_role_rules_do_not_apply_to_managed_roles():
3030
@override_settings(ANSIBLE_BASE_ALLOW_CUSTOM_ROLES=False)
3131
def test_role_definition_enablement_validation_in_api(admin_api_client):
3232
url = get_relative_url('roledefinition-list')
33-
r = admin_api_client.post(url, data={'name': 'foo', 'permissions': ['view_inventory'], 'content_type': 'aap.inventory'})
33+
r = admin_api_client.post(url, data={'name': 'foo', 'permissions': ['aap.view_inventory'], 'content_type': 'aap.inventory'})
3434
assert r.status_code == 400, r.data
3535
assert 'Creating custom roles is disabled' in str(r.data)
3636

0 commit comments

Comments
 (0)