auth_backends.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import logging
  2. from django.conf import settings
  3. from django.contrib.auth.backends import ModelBackend, RemoteUserBackend as RemoteUserBackend_
  4. from django.contrib.auth.models import Group, Permission
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.db.models import Q
  7. from users.models import ObjectPermission
  8. class ViewExemptModelBackend(ModelBackend):
  9. """
  10. Custom implementation of Django's stock ModelBackend which allows for the exemption of arbitrary models from view
  11. permission enforcement.
  12. """
  13. def has_perm(self, user_obj, perm, obj=None):
  14. # If this is a view permission, check whether the model has been exempted from enforcement
  15. try:
  16. app, codename = perm.split('.')
  17. action, model = codename.split('_')
  18. if action == 'view':
  19. if (
  20. # All models are exempt from view permission enforcement
  21. '*' in settings.EXEMPT_VIEW_PERMISSIONS
  22. ) or (
  23. # This specific model is exempt from view permission enforcement
  24. '.'.join((app, model)) in settings.EXEMPT_VIEW_PERMISSIONS
  25. ):
  26. return True
  27. except ValueError:
  28. pass
  29. # Fall back to ModelBackend's default behavior, with one exception: Set obj to None. Model-level permissions
  30. # override object-level permissions, so if a user has the model-level permission we can ignore any specified
  31. # object. (By default, ModelBackend will return False if an object is specified.)
  32. return super().has_perm(user_obj, perm, None)
  33. class ObjectPermissionBackend(ModelBackend):
  34. """
  35. Evaluates permission of a user to access or modify a specific object based on the assignment of ObjectPermissions
  36. either to the user directly or to a group of which the user is a member. Model-level permissions supersede this
  37. check: For example, if a user has the dcim.view_site model-level permission assigned, the ViewExemptModelBackend
  38. will grant permission before this backend is evaluated for permission to view a specific site.
  39. """
  40. def _get_all_permissions(self, user_obj):
  41. """
  42. Retrieve all ObjectPermissions assigned to this User (either directly or through a Group) and return the model-
  43. level equivalent codenames.
  44. """
  45. perm_names = set()
  46. for obj_perm in ObjectPermission.objects.filter(
  47. Q(users=user_obj) | Q(groups__user=user_obj)
  48. ).prefetch_related('model'):
  49. for action in ['view', 'add', 'change', 'delete']:
  50. if getattr(obj_perm, f"can_{action}"):
  51. perm_names.add(f"{obj_perm.model.app_label}.{action}_{obj_perm.model.model}")
  52. return perm_names
  53. def get_all_permissions(self, user_obj, obj=None):
  54. """
  55. Get all model-level permissions assigned by this backend. Permissions are cached on the User instance.
  56. """
  57. if not user_obj.is_active or user_obj.is_anonymous:
  58. return set()
  59. if not hasattr(user_obj, '_obj_perm_cache'):
  60. user_obj._obj_perm_cache = self._get_all_permissions(user_obj)
  61. return user_obj._obj_perm_cache
  62. def has_perm(self, user_obj, perm, obj=None):
  63. # If no object is specified, look for any matching ObjectPermissions. If one or more are found, this indicates
  64. # that the user has permission to perform the requested action on at least *some* objects, but not necessarily
  65. # on all of them.
  66. if obj is None:
  67. return perm in self.get_all_permissions(user_obj)
  68. attrs = ObjectPermission.objects.get_attr_constraints(user_obj, perm)
  69. # No ObjectPermissions found for this combination of user, model, and action
  70. if not attrs:
  71. return
  72. model = obj._meta.model
  73. # Check that the requested permission applies to the specified object
  74. app_label, codename = perm.split('.')
  75. action, model_name = codename.split('_')
  76. if model._meta.label_lower != '.'.join((app_label, model_name)):
  77. raise ValueError(f"Invalid permission {perm} for model {model}")
  78. # Attempt to retrieve the model from the database using the attributes defined in the
  79. # ObjectPermission. If we have a match, assert that the user has permission.
  80. if model.objects.filter(pk=obj.pk, **attrs).exists():
  81. return True
  82. class RemoteUserBackend(ViewExemptModelBackend, RemoteUserBackend_):
  83. """
  84. Custom implementation of Django's RemoteUserBackend which provides configuration hooks for basic customization.
  85. """
  86. @property
  87. def create_unknown_user(self):
  88. return settings.REMOTE_AUTH_AUTO_CREATE_USER
  89. def configure_user(self, request, user):
  90. logger = logging.getLogger('netbox.authentication.RemoteUserBackend')
  91. # Assign default groups to the user
  92. group_list = []
  93. for name in settings.REMOTE_AUTH_DEFAULT_GROUPS:
  94. try:
  95. group_list.append(Group.objects.get(name=name))
  96. except Group.DoesNotExist:
  97. logging.error(f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
  98. if group_list:
  99. user.groups.add(*group_list)
  100. logger.debug(f"Assigned groups to remotely-authenticated user {user}: {group_list}")
  101. # Assign default permissions to the user
  102. permissions_list = []
  103. for permission_name in settings.REMOTE_AUTH_DEFAULT_PERMISSIONS:
  104. try:
  105. app_label, codename = permission_name.split('.')
  106. permissions_list.append(
  107. Permission.objects.get(content_type__app_label=app_label, codename=codename)
  108. )
  109. except (ValueError, Permission.DoesNotExist):
  110. logging.error(
  111. "Invalid permission name: '{permission_name}'. Permissions must be in the form "
  112. "<app>.<action>_<model>. (Example: dcim.add_site)"
  113. )
  114. if permissions_list:
  115. user.user_permissions.add(*permissions_list)
  116. logger.debug(f"Assigned permissions to remotely-authenticated user {user}: {permissions_list}")
  117. return user