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'>
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
Inherited Members
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