authentik.core.api.used_by

used_by mixin

  1"""used_by mixin"""
  2
  3from enum import Enum
  4from inspect import getmembers
  5
  6from django.db.models.base import Model
  7from django.db.models.deletion import SET_DEFAULT, SET_NULL
  8from django.db.models.manager import Manager
  9from drf_spectacular.utils import extend_schema
 10from guardian.shortcuts import get_objects_for_user
 11from rest_framework.decorators import action
 12from rest_framework.fields import CharField, ChoiceField
 13from rest_framework.request import Request
 14from rest_framework.response import Response
 15
 16from authentik.core.api.utils import PassiveSerializer
 17from authentik.rbac.filters import ObjectFilter
 18
 19
 20class DeleteAction(Enum):
 21    """Which action a delete will have on a used object"""
 22
 23    CASCADE = "cascade"
 24    CASCADE_MANY = "cascade_many"
 25    SET_NULL = "set_null"
 26    SET_DEFAULT = "set_default"
 27    LEFT_DANGLING = "left_dangling"
 28
 29
 30class UsedBySerializer(PassiveSerializer):
 31    """A list of all objects referencing the queried object"""
 32
 33    app = CharField()
 34    model_name = CharField()
 35    pk = CharField()
 36    name = CharField()
 37    action = ChoiceField(choices=[(x.value, x.name) for x in DeleteAction])
 38
 39
 40def get_delete_action(manager: Manager) -> str:
 41    """Get the delete action from the Foreign key, falls back to cascade"""
 42    if hasattr(manager, "field"):
 43        if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
 44            return DeleteAction.SET_NULL.value
 45        if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
 46            return DeleteAction.SET_DEFAULT.value
 47    if hasattr(manager, "source_field"):
 48        return DeleteAction.CASCADE_MANY.value
 49    return DeleteAction.CASCADE.value
 50
 51
 52class UsedByMixin:
 53    """Mixin to add a used_by endpoint to return a list of all objects using this object"""
 54
 55    @extend_schema(
 56        responses={200: UsedBySerializer(many=True)},
 57    )
 58    @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
 59    def used_by(self, request: Request, *args, **kwargs) -> Response:
 60        """Get a list of all objects that use this object"""
 61        model: Model = self.get_object()
 62        used_by = []
 63        shadows = []
 64        for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
 65            if attr_name == "objects":  # pragma: no cover
 66                continue
 67            manager: Manager
 68            if manager.model._meta.abstract:
 69                continue
 70            app = manager.model._meta.app_label
 71            model_name = manager.model._meta.model_name
 72            delete_action = get_delete_action(manager)
 73
 74            # To make sure we only apply shadows when there are any objects,
 75            # but so we only apply them once, have a simple flag for the first object
 76            first_object = True
 77
 78            # TODO: This will only return the used-by references that the user can see
 79            # Either we have to leak model information here to not make the list
 80            # useless if the user doesn't have all permissions, or we need to double
 81            # query and check if there is a difference between modes the user can see
 82            # and can't see and add a warning
 83            for obj in get_objects_for_user(
 84                request.user, f"{app}.view_{model_name}", manager.all()
 85            ).all():
 86                # Only merge shadows on first object
 87                if first_object:
 88                    shadows += getattr(manager.model._meta, "authentik_used_by_shadows", [])
 89                first_object = False
 90                serializer = UsedBySerializer(
 91                    data={
 92                        "app": app,
 93                        "model_name": model_name,
 94                        "pk": str(obj.pk),
 95                        "name": str(obj),
 96                        "action": delete_action,
 97                    }
 98                )
 99                serializer.is_valid()
100                used_by.append(serializer.data)
101        # Check the shadows map and remove anything that should be shadowed
102        for idx, user in enumerate(used_by):
103            full_model_name = f"{user['app']}.{user['model_name']}"
104            if full_model_name in shadows:
105                del used_by[idx]
106        return Response(used_by)
class DeleteAction(enum.Enum):
21class DeleteAction(Enum):
22    """Which action a delete will have on a used object"""
23
24    CASCADE = "cascade"
25    CASCADE_MANY = "cascade_many"
26    SET_NULL = "set_null"
27    SET_DEFAULT = "set_default"
28    LEFT_DANGLING = "left_dangling"

Which action a delete will have on a used object

CASCADE = <DeleteAction.CASCADE: 'cascade'>
CASCADE_MANY = <DeleteAction.CASCADE_MANY: 'cascade_many'>
SET_NULL = <DeleteAction.SET_NULL: 'set_null'>
SET_DEFAULT = <DeleteAction.SET_DEFAULT: 'set_default'>
LEFT_DANGLING = <DeleteAction.LEFT_DANGLING: 'left_dangling'>
class UsedBySerializer(authentik.core.api.utils.PassiveSerializer):
31class UsedBySerializer(PassiveSerializer):
32    """A list of all objects referencing the queried object"""
33
34    app = CharField()
35    model_name = CharField()
36    pk = CharField()
37    name = CharField()
38    action = ChoiceField(choices=[(x.value, x.name) for x in DeleteAction])

A list of all objects referencing the queried object

app
model_name
pk
name
action
def get_delete_action(manager: django.db.models.manager.Manager) -> str:
41def get_delete_action(manager: Manager) -> str:
42    """Get the delete action from the Foreign key, falls back to cascade"""
43    if hasattr(manager, "field"):
44        if manager.field.remote_field.on_delete.__name__ == SET_NULL.__name__:
45            return DeleteAction.SET_NULL.value
46        if manager.field.remote_field.on_delete.__name__ == SET_DEFAULT.__name__:
47            return DeleteAction.SET_DEFAULT.value
48    if hasattr(manager, "source_field"):
49        return DeleteAction.CASCADE_MANY.value
50    return DeleteAction.CASCADE.value

Get the delete action from the Foreign key, falls back to cascade

class UsedByMixin:
 53class UsedByMixin:
 54    """Mixin to add a used_by endpoint to return a list of all objects using this object"""
 55
 56    @extend_schema(
 57        responses={200: UsedBySerializer(many=True)},
 58    )
 59    @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
 60    def used_by(self, request: Request, *args, **kwargs) -> Response:
 61        """Get a list of all objects that use this object"""
 62        model: Model = self.get_object()
 63        used_by = []
 64        shadows = []
 65        for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
 66            if attr_name == "objects":  # pragma: no cover
 67                continue
 68            manager: Manager
 69            if manager.model._meta.abstract:
 70                continue
 71            app = manager.model._meta.app_label
 72            model_name = manager.model._meta.model_name
 73            delete_action = get_delete_action(manager)
 74
 75            # To make sure we only apply shadows when there are any objects,
 76            # but so we only apply them once, have a simple flag for the first object
 77            first_object = True
 78
 79            # TODO: This will only return the used-by references that the user can see
 80            # Either we have to leak model information here to not make the list
 81            # useless if the user doesn't have all permissions, or we need to double
 82            # query and check if there is a difference between modes the user can see
 83            # and can't see and add a warning
 84            for obj in get_objects_for_user(
 85                request.user, f"{app}.view_{model_name}", manager.all()
 86            ).all():
 87                # Only merge shadows on first object
 88                if first_object:
 89                    shadows += getattr(manager.model._meta, "authentik_used_by_shadows", [])
 90                first_object = False
 91                serializer = UsedBySerializer(
 92                    data={
 93                        "app": app,
 94                        "model_name": model_name,
 95                        "pk": str(obj.pk),
 96                        "name": str(obj),
 97                        "action": delete_action,
 98                    }
 99                )
100                serializer.is_valid()
101                used_by.append(serializer.data)
102        # Check the shadows map and remove anything that should be shadowed
103        for idx, user in enumerate(used_by):
104            full_model_name = f"{user['app']}.{user['model_name']}"
105            if full_model_name in shadows:
106                del used_by[idx]
107        return Response(used_by)

Mixin to add a used_by endpoint to return a list of all objects using this object

@extend_schema(responses={200: UsedBySerializer(many=True)})
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
def used_by( self, request: rest_framework.request.Request, *args, **kwargs) -> rest_framework.response.Response:
 56    @extend_schema(
 57        responses={200: UsedBySerializer(many=True)},
 58    )
 59    @action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
 60    def used_by(self, request: Request, *args, **kwargs) -> Response:
 61        """Get a list of all objects that use this object"""
 62        model: Model = self.get_object()
 63        used_by = []
 64        shadows = []
 65        for attr_name, manager in getmembers(model, lambda x: isinstance(x, Manager)):
 66            if attr_name == "objects":  # pragma: no cover
 67                continue
 68            manager: Manager
 69            if manager.model._meta.abstract:
 70                continue
 71            app = manager.model._meta.app_label
 72            model_name = manager.model._meta.model_name
 73            delete_action = get_delete_action(manager)
 74
 75            # To make sure we only apply shadows when there are any objects,
 76            # but so we only apply them once, have a simple flag for the first object
 77            first_object = True
 78
 79            # TODO: This will only return the used-by references that the user can see
 80            # Either we have to leak model information here to not make the list
 81            # useless if the user doesn't have all permissions, or we need to double
 82            # query and check if there is a difference between modes the user can see
 83            # and can't see and add a warning
 84            for obj in get_objects_for_user(
 85                request.user, f"{app}.view_{model_name}", manager.all()
 86            ).all():
 87                # Only merge shadows on first object
 88                if first_object:
 89                    shadows += getattr(manager.model._meta, "authentik_used_by_shadows", [])
 90                first_object = False
 91                serializer = UsedBySerializer(
 92                    data={
 93                        "app": app,
 94                        "model_name": model_name,
 95                        "pk": str(obj.pk),
 96                        "name": str(obj),
 97                        "action": delete_action,
 98                    }
 99                )
100                serializer.is_valid()
101                used_by.append(serializer.data)
102        # Check the shadows map and remove anything that should be shadowed
103        for idx, user in enumerate(used_by):
104            full_model_name = f"{user['app']}.{user['model_name']}"
105            if full_model_name in shadows:
106                del used_by[idx]
107        return Response(used_by)

Get a list of all objects that use this object