22from django .core .exceptions import ObjectDoesNotExist
33from django .db import transaction
44from django .db .utils import IntegrityError
5- from django .utils .functional import cached_property
65from django .utils .translation import gettext_lazy as _
76from rest_framework import serializers
87from rest_framework .exceptions import PermissionDenied
9- from rest_framework .fields import flatten_choices_dict , to_choices_dict
108from rest_framework .serializers import ValidationError
119
1210from ansible_base .lib .abstract_models .common import get_url_for_object
1614from ansible_base .rbac .policies import check_content_obj_permission , visible_users
1715from 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
12420class 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
15262class RoleDefinitionDetailSerializer (RoleDefinitionSerializer ):
153- content_type = ContentTypeField ( read_only = True )
63+ content_type = serializers . SlugRelatedField ( slug_field = 'api_slug' , read_only = True )
15464
15565
15666class 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.' ),
0 commit comments