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)
def is_dict(value: Any):
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

class JSONDictField(rest_framework.fields.JSONField):
33class JSONDictField(JSONField):
34    """JSON Field which only allows dictionaries"""
35
36    default_validators = [is_dict]

JSON Field which only allows dictionaries

default_validators = [<function is_dict>]
class JSONExtension(drf_spectacular.plumbing.OpenApiGeneratorExtension[ForwardRef('OpenApiSerializerFieldExtension')]):
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

target_class = 'JSONDictField'
def map_serializer_field(self, auto_schema, direction):
44    def map_serializer_field(self, auto_schema, direction):
45        return build_basic_type(OpenApiTypes.OBJECT)

override for customized serializer field mapping

class ModelSerializer(rest_framework.serializers.ModelSerializer):
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.

serializer_field_mapping = {<class 'django.db.models.fields.AutoField'>: <class 'rest_framework.fields.IntegerField'>, <class 'django.db.models.fields.BigAutoField'>: <class 'rest_framework.fields.BigIntegerField'>, <class 'django.db.models.fields.BigIntegerField'>: <class 'rest_framework.fields.BigIntegerField'>, <class 'django.db.models.fields.BooleanField'>: <class 'rest_framework.fields.BooleanField'>, <class 'django.db.models.fields.CharField'>: <class 'rest_framework.fields.CharField'>, <class 'django.db.models.fields.CommaSeparatedIntegerField'>: <class 'rest_framework.fields.CharField'>, <class 'django.db.models.fields.DateField'>: <class 'rest_framework.fields.DateField'>, <class 'django.db.models.fields.DateTimeField'>: <class 'rest_framework.fields.DateTimeField'>, <class 'django.db.models.fields.DecimalField'>: <class 'rest_framework.fields.DecimalField'>, <class 'django.db.models.fields.DurationField'>: <class 'rest_framework.fields.DurationField'>, <class 'django.db.models.fields.EmailField'>: <class 'rest_framework.fields.EmailField'>, <class 'django.db.models.fields.Field'>: <class 'rest_framework.fields.ModelField'>, <class 'django.db.models.fields.files.FileField'>: <class 'rest_framework.fields.FileField'>, <class 'django.db.models.fields.FloatField'>: <class 'rest_framework.fields.FloatField'>, <class 'django.db.models.fields.files.ImageField'>: <class 'rest_framework.fields.ImageField'>, <class 'django.db.models.fields.IntegerField'>: <class 'rest_framework.fields.IntegerField'>, <class 'django.db.models.fields.NullBooleanField'>: <class 'rest_framework.fields.BooleanField'>, <class 'django.db.models.fields.PositiveIntegerField'>: <class 'rest_framework.fields.IntegerField'>, <class 'django.db.models.fields.PositiveSmallIntegerField'>: <class 'rest_framework.fields.IntegerField'>, <class 'django.db.models.fields.SlugField'>: <class 'rest_framework.fields.SlugField'>, <class 'django.db.models.fields.SmallIntegerField'>: <class 'rest_framework.fields.IntegerField'>, <class 'django.db.models.fields.TextField'>: <class 'rest_framework.fields.CharField'>, <class 'django.db.models.fields.TimeField'>: <class 'rest_framework.fields.TimeField'>, <class 'django.db.models.fields.URLField'>: <class 'rest_framework.fields.URLField'>, <class 'django.db.models.fields.UUIDField'>: <class 'rest_framework.fields.UUIDField'>, <class 'django.db.models.fields.GenericIPAddressField'>: <class 'rest_framework.fields.IPAddressField'>, <class 'django.db.models.fields.FilePathField'>: <class 'rest_framework.fields.FilePathField'>, <class 'django.db.models.fields.json.JSONField'>: <class 'JSONDictField'>, <class 'django.contrib.postgres.fields.hstore.HStoreField'>: <class 'rest_framework.fields.HStoreField'>, <class 'django.contrib.postgres.fields.array.ArrayField'>: <class 'rest_framework.fields.ListField'>, <class 'django.contrib.postgres.fields.jsonb.JSONField'>: <class 'rest_framework.fields.JSONField'>}
def update(self, instance: django.db.models.base.Model, validated_data):
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
class PassiveSerializer(rest_framework.serializers.Serializer):
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

def create(self, validated_data: dict) -> django.db.models.base.Model:
88    def create(self, validated_data: dict) -> Model:  # pragma: no cover
89        return Model()
def update( self, instance: django.db.models.base.Model, validated_data: dict) -> django.db.models.base.Model:
91    def update(self, instance: Model, validated_data: dict) -> Model:  # pragma: no cover
92        return Model()
class PropertyMappingPreviewSerializer(PassiveSerializer):
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

preview
Inherited Members
PassiveSerializer
create
update
class MetaNameSerializer(PassiveSerializer):
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

verbose_name
verbose_name_plural
meta_model_name
def get_verbose_name(self, obj: django.db.models.base.Model) -> str:
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

def get_verbose_name_plural(self, obj: django.db.models.base.Model) -> str:
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

def get_meta_model_name(self, obj: django.db.models.base.Model) -> str:
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
PassiveSerializer
create
update
class CacheSerializer(PassiveSerializer):
121class CacheSerializer(PassiveSerializer):
122    """Generic cache stats for an object"""
123
124    count = IntegerField(read_only=True)

Generic cache stats for an object

count
Inherited Members
PassiveSerializer
create
update
class LinkSerializer(PassiveSerializer):
127class LinkSerializer(PassiveSerializer):
128    """Returns a single link"""
129
130    link = CharField()

Returns a single link

Inherited Members
PassiveSerializer
create
update
class ThemedUrlsSerializer(PassiveSerializer):
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

light
dark
Inherited Members
PassiveSerializer
create
update