authentik.core.api.utils
API Utilities
1"""API Utilities""" 2 3from typing import Any 4 5from django.db import models 6from django.db.models import Model 7from drf_spectacular.extensions import OpenApiSerializerFieldExtension 8from drf_spectacular.plumbing import build_basic_type 9from drf_spectacular.types import OpenApiTypes 10from rest_framework.fields import ( 11 CharField, 12 IntegerField, 13 JSONField, 14 SerializerMethodField, 15) 16from rest_framework.serializers import ModelSerializer as BaseModelSerializer 17from rest_framework.serializers import ( 18 Serializer, 19 ValidationError, 20 model_meta, 21 raise_errors_on_nested_writes, 22) 23 24 25def is_dict(value: Any): 26 """Ensure a value is a dictionary, useful for JSONFields""" 27 if isinstance(value, dict): 28 return 29 raise ValidationError("Value must be a dictionary, and not have any duplicate keys.") 30 31 32class JSONDictField(JSONField): 33 """JSON Field which only allows dictionaries""" 34 35 default_validators = [is_dict] 36 37 38class JSONExtension(OpenApiSerializerFieldExtension): 39 """Generate API Schema for JSON fields as""" 40 41 target_class = "authentik.core.api.utils.JSONDictField" 42 43 def map_serializer_field(self, auto_schema, direction): 44 return build_basic_type(OpenApiTypes.OBJECT) 45 46 47class ModelSerializer(BaseModelSerializer): 48 49 # By default, JSON fields we have are used to store dictionaries 50 serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy() 51 serializer_field_mapping[models.JSONField] = JSONDictField 52 53 def update(self, instance: Model, validated_data): 54 raise_errors_on_nested_writes("update", self, validated_data) 55 info = model_meta.get_field_info(instance) 56 57 # Simply set each attribute on the instance, and then save it. 58 # Note that unlike `.create()` we don't need to treat many-to-many 59 # relationships as being a special case. During updates we already 60 # have an instance pk for the relationships to be associated with. 61 m2m_fields = [] 62 for attr, value in validated_data.items(): 63 if attr in info.relations and info.relations[attr].to_many: 64 m2m_fields.append((attr, value)) 65 else: 66 setattr(instance, attr, value) 67 68 instance.save() 69 70 # Note that many-to-many fields are set after updating instance. 71 # Setting m2m fields triggers signals which could potentially change 72 # updated instance and we do not want it to collide with .update() 73 for attr, value in m2m_fields: 74 field = getattr(instance, attr) 75 # We can't check for inheritance here as m2m managers are generated dynamically 76 if field.__class__.__name__ == "RelatedManager": 77 field.set(value, bulk=False) 78 else: 79 field.set(value) 80 81 return instance 82 83 84class PassiveSerializer(Serializer): 85 """Base serializer class which doesn't implement create/update methods""" 86 87 def create(self, validated_data: dict) -> Model: # pragma: no cover 88 return Model() 89 90 def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover 91 return Model() 92 93 94class PropertyMappingPreviewSerializer(PassiveSerializer): 95 """Preview how the current user is mapped via the property mappings selected in a provider""" 96 97 preview = JSONDictField(read_only=True) 98 99 100class MetaNameSerializer(PassiveSerializer): 101 """Add verbose names to response""" 102 103 verbose_name = SerializerMethodField() 104 verbose_name_plural = SerializerMethodField() 105 meta_model_name = SerializerMethodField() 106 107 def get_verbose_name(self, obj: Model) -> str: 108 """Return object's verbose_name""" 109 return obj._meta.verbose_name 110 111 def get_verbose_name_plural(self, obj: Model) -> str: 112 """Return object's plural verbose_name""" 113 return obj._meta.verbose_name_plural 114 115 def get_meta_model_name(self, obj: Model) -> str: 116 """Return internal model name""" 117 return f"{obj._meta.app_label}.{obj._meta.model_name}" 118 119 120class CacheSerializer(PassiveSerializer): 121 """Generic cache stats for an object""" 122 123 count = IntegerField(read_only=True) 124 125 126class LinkSerializer(PassiveSerializer): 127 """Returns a single link""" 128 129 link = CharField() 130 131 132class ThemedUrlsSerializer(PassiveSerializer): 133 """Themed URLs - maps theme names to URLs for light and dark themes""" 134 135 light = CharField(required=False, allow_null=True) 136 dark = CharField(required=False, allow_null=True)
26def is_dict(value: Any): 27 """Ensure a value is a dictionary, useful for JSONFields""" 28 if isinstance(value, dict): 29 return 30 raise ValidationError("Value must be a dictionary, and not have any duplicate keys.")
Ensure a value is a dictionary, useful for JSONFields
33class JSONDictField(JSONField): 34 """JSON Field which only allows dictionaries""" 35 36 default_validators = [is_dict]
JSON Field which only allows dictionaries
39class JSONExtension(OpenApiSerializerFieldExtension): 40 """Generate API Schema for JSON fields as""" 41 42 target_class = "authentik.core.api.utils.JSONDictField" 43 44 def map_serializer_field(self, auto_schema, direction): 45 return build_basic_type(OpenApiTypes.OBJECT)
Generate API Schema for JSON fields as
48class ModelSerializer(BaseModelSerializer): 49 50 # By default, JSON fields we have are used to store dictionaries 51 serializer_field_mapping = BaseModelSerializer.serializer_field_mapping.copy() 52 serializer_field_mapping[models.JSONField] = JSONDictField 53 54 def update(self, instance: Model, validated_data): 55 raise_errors_on_nested_writes("update", self, validated_data) 56 info = model_meta.get_field_info(instance) 57 58 # Simply set each attribute on the instance, and then save it. 59 # Note that unlike `.create()` we don't need to treat many-to-many 60 # relationships as being a special case. During updates we already 61 # have an instance pk for the relationships to be associated with. 62 m2m_fields = [] 63 for attr, value in validated_data.items(): 64 if attr in info.relations and info.relations[attr].to_many: 65 m2m_fields.append((attr, value)) 66 else: 67 setattr(instance, attr, value) 68 69 instance.save() 70 71 # Note that many-to-many fields are set after updating instance. 72 # Setting m2m fields triggers signals which could potentially change 73 # updated instance and we do not want it to collide with .update() 74 for attr, value in m2m_fields: 75 field = getattr(instance, attr) 76 # We can't check for inheritance here as m2m managers are generated dynamically 77 if field.__class__.__name__ == "RelatedManager": 78 field.set(value, bulk=False) 79 else: 80 field.set(value) 81 82 return instance
A ModelSerializer is just a regular Serializer, except that:
- A set of default fields are automatically populated.
- A set of default validators are automatically populated.
- Default
.create()and.update()implementations are provided.
The process of automatically determining a set of serializer fields based on the model fields is reasonably complex, but you almost certainly don't need to dig into the implementation.
If the ModelSerializer class doesn't generate the set of fields that
you need you should either declare the extra/differing fields explicitly on
the serializer class, or simply use a Serializer class.
54 def update(self, instance: Model, validated_data): 55 raise_errors_on_nested_writes("update", self, validated_data) 56 info = model_meta.get_field_info(instance) 57 58 # Simply set each attribute on the instance, and then save it. 59 # Note that unlike `.create()` we don't need to treat many-to-many 60 # relationships as being a special case. During updates we already 61 # have an instance pk for the relationships to be associated with. 62 m2m_fields = [] 63 for attr, value in validated_data.items(): 64 if attr in info.relations and info.relations[attr].to_many: 65 m2m_fields.append((attr, value)) 66 else: 67 setattr(instance, attr, value) 68 69 instance.save() 70 71 # Note that many-to-many fields are set after updating instance. 72 # Setting m2m fields triggers signals which could potentially change 73 # updated instance and we do not want it to collide with .update() 74 for attr, value in m2m_fields: 75 field = getattr(instance, attr) 76 # We can't check for inheritance here as m2m managers are generated dynamically 77 if field.__class__.__name__ == "RelatedManager": 78 field.set(value, bulk=False) 79 else: 80 field.set(value) 81 82 return instance
85class PassiveSerializer(Serializer): 86 """Base serializer class which doesn't implement create/update methods""" 87 88 def create(self, validated_data: dict) -> Model: # pragma: no cover 89 return Model() 90 91 def update(self, instance: Model, validated_data: dict) -> Model: # pragma: no cover 92 return Model()
Base serializer class which doesn't implement create/update methods
95class PropertyMappingPreviewSerializer(PassiveSerializer): 96 """Preview how the current user is mapped via the property mappings selected in a provider""" 97 98 preview = JSONDictField(read_only=True)
Preview how the current user is mapped via the property mappings selected in a provider
Inherited Members
101class MetaNameSerializer(PassiveSerializer): 102 """Add verbose names to response""" 103 104 verbose_name = SerializerMethodField() 105 verbose_name_plural = SerializerMethodField() 106 meta_model_name = SerializerMethodField() 107 108 def get_verbose_name(self, obj: Model) -> str: 109 """Return object's verbose_name""" 110 return obj._meta.verbose_name 111 112 def get_verbose_name_plural(self, obj: Model) -> str: 113 """Return object's plural verbose_name""" 114 return obj._meta.verbose_name_plural 115 116 def get_meta_model_name(self, obj: Model) -> str: 117 """Return internal model name""" 118 return f"{obj._meta.app_label}.{obj._meta.model_name}"
Add verbose names to response
108 def get_verbose_name(self, obj: Model) -> str: 109 """Return object's verbose_name""" 110 return obj._meta.verbose_name
Return object's verbose_name
112 def get_verbose_name_plural(self, obj: Model) -> str: 113 """Return object's plural verbose_name""" 114 return obj._meta.verbose_name_plural
Return object's plural verbose_name
116 def get_meta_model_name(self, obj: Model) -> str: 117 """Return internal model name""" 118 return f"{obj._meta.app_label}.{obj._meta.model_name}"
Return internal model name
Inherited Members
121class CacheSerializer(PassiveSerializer): 122 """Generic cache stats for an object""" 123 124 count = IntegerField(read_only=True)
Generic cache stats for an object
Inherited Members
127class LinkSerializer(PassiveSerializer): 128 """Returns a single link""" 129 130 link = CharField()
Returns a single link
Inherited Members
133class ThemedUrlsSerializer(PassiveSerializer): 134 """Themed URLs - maps theme names to URLs for light and dark themes""" 135 136 light = CharField(required=False, allow_null=True) 137 dark = CharField(required=False, allow_null=True)
Themed URLs - maps theme names to URLs for light and dark themes