api.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import logging
  2. from django.contrib.contenttypes.fields import GenericForeignKey
  3. from django.core.exceptions import (
  4. FieldDoesNotExist,
  5. FieldError,
  6. MultipleObjectsReturned,
  7. ObjectDoesNotExist,
  8. ValidationError,
  9. )
  10. from django.db.models.fields.related import ManyToOneRel, RelatedField
  11. from django.urls import reverse
  12. from django.utils.module_loading import import_string
  13. from django.utils.translation import gettext_lazy as _
  14. from rest_framework.permissions import BasePermission
  15. from rest_framework.serializers import ListSerializer, Serializer
  16. from rest_framework.views import get_view_name as drf_get_view_name
  17. from extras.constants import HTTP_CONTENT_TYPE_JSON
  18. from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
  19. from netbox.api.fields import RelatedObjectCountField
  20. from netbox.registry import registry
  21. from .query import count_related, dict_to_filter_params
  22. from .string import title
  23. logger = logging.getLogger('netbox.utilities.api')
  24. __all__ = (
  25. 'IsSuperuser',
  26. 'get_annotations_for_serializer',
  27. 'get_graphql_type_for_model',
  28. 'get_prefetches_for_serializer',
  29. 'get_related_object_by_attrs',
  30. 'get_serializer_for_model',
  31. 'get_view_name',
  32. 'is_api_request',
  33. 'is_graphql_request',
  34. )
  35. class IsSuperuser(BasePermission):
  36. """
  37. Allows access only to superusers.
  38. """
  39. def has_permission(self, request, view):
  40. return bool(request.user and request.user.is_superuser)
  41. def get_serializer_for_model(model, prefix=''):
  42. """
  43. Return the appropriate REST API serializer for the given model.
  44. A plugin (or internal app) may register a custom resolver for its own
  45. app via netbox.plugins.register_serializer_resolver() to handle
  46. dynamically generated models or to override serializer resolution. If
  47. a resolver is registered for the model's app and returns a Serializer
  48. subclass, that result is used. Otherwise, the default import-path
  49. lookup runs.
  50. """
  51. app_label, model_name = model._meta.label.split('.')
  52. if resolver := registry['serializer_resolvers'].get(app_label):
  53. try:
  54. serializer = resolver(model, prefix=prefix)
  55. except Exception:
  56. # A buggy resolver must not break serializer lookup for the rest of NetBox.
  57. logger.exception("Serializer resolver %r raised an exception; falling through to default lookup.", resolver)
  58. serializer = None
  59. if serializer is not None:
  60. if isinstance(serializer, type) and issubclass(serializer, Serializer):
  61. return serializer
  62. logger.warning(
  63. "Serializer resolver %r returned %r, which is not a Serializer subclass; "
  64. "falling through to default lookup.",
  65. resolver, serializer,
  66. )
  67. serializer_name = f'{app_label}.api.serializers.{prefix}{model_name}Serializer'
  68. try:
  69. return import_string(serializer_name)
  70. except ImportError:
  71. raise SerializerNotFound(
  72. f"Could not determine serializer for {app_label}.{model_name} with prefix '{prefix}'"
  73. )
  74. def get_graphql_type_for_model(model):
  75. """
  76. Return the GraphQL type class for the given model.
  77. """
  78. app_label, model_name = model._meta.label.split('.')
  79. class_name = f'{app_label}.graphql.types.{model_name}Type'
  80. try:
  81. return import_string(class_name)
  82. except ImportError:
  83. raise GraphQLTypeNotFound(f"Could not find GraphQL type for {app_label}.{model_name}")
  84. def is_api_request(request):
  85. """
  86. Return True of the request is being made via the REST API.
  87. """
  88. return request.path_info.startswith(reverse('api-root'))
  89. def is_graphql_request(request):
  90. """
  91. Return True of the request is being made via the GraphQL API.
  92. """
  93. return request.path_info == reverse('graphql') and request.content_type == HTTP_CONTENT_TYPE_JSON
  94. def get_view_name(view):
  95. """
  96. Derive the view name from its associated model, if it has one. Fall back to DRF's built-in `get_view_name()`.
  97. This function is provided to DRF as its VIEW_NAME_FUNCTION.
  98. """
  99. if hasattr(view, 'queryset') and view.queryset is not None:
  100. # Derive the model name from the queryset.
  101. name = title(view.queryset.model._meta.verbose_name)
  102. if suffix := getattr(view, 'suffix', None):
  103. name = f'{name} {suffix}'
  104. return name
  105. # Fall back to DRF's default behavior
  106. return drf_get_view_name(view)
  107. def _get_nested_serializer(serializer_field):
  108. """
  109. Return the nested serializer instance for a declared serializer field.
  110. """
  111. if isinstance(serializer_field, ListSerializer):
  112. serializer_field = serializer_field.child
  113. if isinstance(serializer_field, Serializer) and hasattr(serializer_field, 'nested'):
  114. return serializer_field
  115. return None
  116. def _get_serializer_fields(serializer: Serializer):
  117. """
  118. Return the effective field names for a serializer instance, honoring any
  119. field-level fields=/omit= overrides.
  120. """
  121. fields = getattr(serializer, '_include_fields', None) or serializer.Meta.fields
  122. omit = getattr(serializer, '_omit_fields', []) or []
  123. return [field_name for field_name in fields if field_name not in omit]
  124. def get_prefetches_for_serializer(serializer_class, fields=None, omit=None):
  125. """
  126. Compile and return a list of fields which should be prefetched on the queryset for a serializer.
  127. """
  128. if fields is not None and omit is not None:
  129. raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
  130. model = serializer_class.Meta.model
  131. # If fields are not specified, default to all
  132. fields_to_include = fields or serializer_class.Meta.fields
  133. fields_to_omit = omit or []
  134. prefetch_fields = []
  135. for field_name in fields_to_include:
  136. if field_name in fields_to_omit:
  137. continue
  138. serializer_field = serializer_class._declared_fields.get(field_name)
  139. # Determine the name of the model field referenced by the serializer field
  140. model_field_name = field_name
  141. if serializer_field and getattr(serializer_field, 'source', None):
  142. model_field_name = serializer_field.source
  143. # If the serializer field does not map to a discrete model field, skip it.
  144. try:
  145. field = model._meta.get_field(model_field_name)
  146. if isinstance(field, (RelatedField, ManyToOneRel, GenericForeignKey)):
  147. prefetch_fields.append(field.name)
  148. except FieldDoesNotExist:
  149. continue
  150. # If this field is represented by a nested serializer, recurse to resolve
  151. # prefetches for the related object, honoring any field-level fields=/omit=
  152. # constraints set on that serializer field instance.
  153. if nested_serializer := _get_nested_serializer(serializer_field):
  154. subfields = _get_serializer_fields(nested_serializer)
  155. for subfield in get_prefetches_for_serializer(type(nested_serializer), fields=subfields):
  156. prefetch_fields.append(f'{field.name}__{subfield}')
  157. return prefetch_fields
  158. def get_annotations_for_serializer(serializer_class, fields=None, omit=None):
  159. """
  160. Return a mapping of field names to annotations to be applied to the queryset for a serializer.
  161. """
  162. if fields is not None and omit is not None:
  163. raise TypeError("Cannot specify both 'fields' and 'omit' parameters.")
  164. model = serializer_class.Meta.model
  165. # If fields are not specified, default to all
  166. fields_to_include = fields or serializer_class.Meta.fields
  167. fields_to_omit = omit or []
  168. annotations = {}
  169. for field_name, field in serializer_class._declared_fields.items():
  170. if field_name in fields_to_omit:
  171. continue
  172. if field_name in fields_to_include and type(field) is RelatedObjectCountField:
  173. related_field = getattr(model, field.relation).field
  174. annotations[field_name] = count_related(related_field.model, related_field.name)
  175. return annotations
  176. def get_related_object_by_attrs(queryset, attrs):
  177. """
  178. Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used
  179. for referencing related objects when creating/updating objects via the REST API.
  180. """
  181. if attrs is None:
  182. return None
  183. # Dictionary of related object attributes
  184. if isinstance(attrs, dict):
  185. params = dict_to_filter_params(attrs)
  186. try:
  187. return queryset.get(**params)
  188. except ObjectDoesNotExist:
  189. raise ValidationError(
  190. _("Related object not found using the provided attributes: {params}").format(params=params))
  191. except MultipleObjectsReturned:
  192. raise ValidationError(
  193. _("Multiple objects match the provided attributes: {params}").format(params=params)
  194. )
  195. except FieldError as e:
  196. raise ValidationError(e)
  197. # Integer PK of related object
  198. try:
  199. # Cast as integer in case a PK was mistakenly sent as a string
  200. pk = int(attrs)
  201. except (TypeError, ValueError):
  202. raise ValidationError(
  203. _(
  204. "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
  205. "unrecognized value: {value}"
  206. ).format(value=attrs)
  207. )
  208. # Look up object by PK
  209. try:
  210. return queryset.get(pk=pk)
  211. except ObjectDoesNotExist:
  212. raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk))