2
0

models.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. import binascii
  2. import os
  3. from django.conf import settings
  4. from django.contrib.auth.models import Group, GroupManager, User, UserManager
  5. from django.contrib.contenttypes.models import ContentType
  6. from django.contrib.postgres.fields import ArrayField
  7. from django.core.exceptions import ValidationError
  8. from django.core.validators import MinLengthValidator
  9. from django.db import models
  10. from django.db.models.signals import post_save
  11. from django.dispatch import receiver
  12. from django.urls import reverse
  13. from django.utils import timezone
  14. from django.utils.translation import gettext_lazy as _
  15. from netaddr import IPNetwork
  16. from ipam.fields import IPNetworkField
  17. from netbox.config import get_config
  18. from utilities.querysets import RestrictedQuerySet
  19. from utilities.utils import flatten_dict
  20. from .constants import *
  21. __all__ = (
  22. 'NetBoxGroup',
  23. 'NetBoxUser',
  24. 'ObjectPermission',
  25. 'Token',
  26. 'UserConfig',
  27. )
  28. #
  29. # Proxies for Django's User and Group models
  30. #
  31. class NetBoxUserManager(UserManager.from_queryset(RestrictedQuerySet)):
  32. pass
  33. class NetBoxGroupManager(GroupManager.from_queryset(RestrictedQuerySet)):
  34. pass
  35. class NetBoxUser(User):
  36. """
  37. Proxy contrib.auth.models.User for the UI
  38. """
  39. objects = NetBoxUserManager()
  40. class Meta:
  41. proxy = True
  42. ordering = ('username',)
  43. verbose_name = _('user')
  44. verbose_name_plural = _('users')
  45. def get_absolute_url(self):
  46. return reverse('users:netboxuser', args=[self.pk])
  47. def clean(self):
  48. super().clean()
  49. # Check for any existing Users with names that differ only in case
  50. model = self._meta.model
  51. if model.objects.exclude(pk=self.pk).filter(username__iexact=self.username).exists():
  52. raise ValidationError(_("A user with this username already exists."))
  53. class NetBoxGroup(Group):
  54. """
  55. Proxy contrib.auth.models.User for the UI
  56. """
  57. objects = NetBoxGroupManager()
  58. class Meta:
  59. proxy = True
  60. ordering = ('name',)
  61. verbose_name = _('group')
  62. verbose_name_plural = _('groups')
  63. def get_absolute_url(self):
  64. return reverse('users:netboxgroup', args=[self.pk])
  65. #
  66. # User preferences
  67. #
  68. class UserConfig(models.Model):
  69. """
  70. This model stores arbitrary user-specific preferences in a JSON data structure.
  71. """
  72. user = models.OneToOneField(
  73. to=User,
  74. on_delete=models.CASCADE,
  75. related_name='config'
  76. )
  77. data = models.JSONField(
  78. default=dict
  79. )
  80. class Meta:
  81. ordering = ['user']
  82. verbose_name = _('user preferences')
  83. verbose_name_plural = _('user preferences')
  84. def get(self, path, default=None):
  85. """
  86. Retrieve a configuration parameter specified by its dotted path. Example:
  87. userconfig.get('foo.bar.baz')
  88. :param path: Dotted path to the configuration key. For example, 'foo.bar' returns self.data['foo']['bar'].
  89. :param default: Default value to return for a nonexistent key (default: None).
  90. """
  91. d = self.data
  92. keys = path.split('.')
  93. # Iterate down the hierarchy, returning the default value if any invalid key is encountered
  94. try:
  95. for key in keys:
  96. d = d[key]
  97. return d
  98. except (TypeError, KeyError):
  99. pass
  100. # If the key is not found in the user's config, check for an application-wide default
  101. config = get_config()
  102. d = config.DEFAULT_USER_PREFERENCES
  103. try:
  104. for key in keys:
  105. d = d[key]
  106. return d
  107. except (TypeError, KeyError):
  108. pass
  109. # Finally, return the specified default value (if any)
  110. return default
  111. def all(self):
  112. """
  113. Return a dictionary of all defined keys and their values.
  114. """
  115. return flatten_dict(self.data)
  116. def set(self, path, value, commit=False):
  117. """
  118. Define or overwrite a configuration parameter. Example:
  119. userconfig.set('foo.bar.baz', 123)
  120. Leaf nodes (those which are not dictionaries of other nodes) cannot be overwritten as dictionaries. Similarly,
  121. branch nodes (dictionaries) cannot be overwritten as single values. (A TypeError exception will be raised.) In
  122. both cases, the existing key must first be cleared. This safeguard is in place to help avoid inadvertently
  123. overwriting the wrong key.
  124. :param path: Dotted path to the configuration key. For example, 'foo.bar' sets self.data['foo']['bar'].
  125. :param value: The value to be written. This can be any type supported by JSON.
  126. :param commit: If true, the UserConfig instance will be saved once the new value has been applied.
  127. """
  128. d = self.data
  129. keys = path.split('.')
  130. # Iterate through the hierarchy to find the key we're setting. Raise TypeError if we encounter any
  131. # interim leaf nodes (keys which do not contain dictionaries).
  132. for i, key in enumerate(keys[:-1]):
  133. if key in d and type(d[key]) is dict:
  134. d = d[key]
  135. elif key in d:
  136. err_path = '.'.join(path.split('.')[:i + 1])
  137. raise TypeError(
  138. _("Key '{err_path}' is a leaf node; cannot assign new keys").format(err_path=err_path)
  139. )
  140. else:
  141. d = d.setdefault(key, {})
  142. # Set a key based on the last item in the path. Raise TypeError if attempting to overwrite a non-leaf node.
  143. key = keys[-1]
  144. if key in d and type(d[key]) is dict:
  145. if type(value) is dict:
  146. d[key].update(value)
  147. else:
  148. raise TypeError(
  149. _("Key '{path}' is a dictionary; cannot assign a non-dictionary value").format(path=path)
  150. )
  151. else:
  152. d[key] = value
  153. if commit:
  154. self.save()
  155. def clear(self, path, commit=False):
  156. """
  157. Delete a configuration parameter specified by its dotted path. The key and any child keys will be deleted.
  158. Example:
  159. userconfig.clear('foo.bar.baz')
  160. Invalid keys will be ignored silently.
  161. :param path: Dotted path to the configuration key. For example, 'foo.bar' deletes self.data['foo']['bar'].
  162. :param commit: If true, the UserConfig instance will be saved once the new value has been applied.
  163. """
  164. d = self.data
  165. keys = path.split('.')
  166. for key in keys[:-1]:
  167. if key not in d:
  168. break
  169. if type(d[key]) is dict:
  170. d = d[key]
  171. key = keys[-1]
  172. d.pop(key, None) # Avoid a KeyError on invalid keys
  173. if commit:
  174. self.save()
  175. @receiver(post_save, sender=User)
  176. def create_userconfig(instance, created, raw=False, **kwargs):
  177. """
  178. Automatically create a new UserConfig when a new User is created. Skip this if importing a user from a fixture.
  179. """
  180. if created and not raw:
  181. config = get_config()
  182. UserConfig(user=instance, data=config.DEFAULT_USER_PREFERENCES).save()
  183. #
  184. # REST API
  185. #
  186. class Token(models.Model):
  187. """
  188. An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens.
  189. It also supports setting an expiration time and toggling write ability.
  190. """
  191. user = models.ForeignKey(
  192. to=User,
  193. on_delete=models.CASCADE,
  194. related_name='tokens'
  195. )
  196. created = models.DateTimeField(
  197. verbose_name=_('created'),
  198. auto_now_add=True
  199. )
  200. expires = models.DateTimeField(
  201. verbose_name=_('expires'),
  202. blank=True,
  203. null=True
  204. )
  205. last_used = models.DateTimeField(
  206. verbose_name=_('last used'),
  207. blank=True,
  208. null=True
  209. )
  210. key = models.CharField(
  211. verbose_name=_('key'),
  212. max_length=40,
  213. unique=True,
  214. validators=[MinLengthValidator(40)]
  215. )
  216. write_enabled = models.BooleanField(
  217. verbose_name=_('write enabled'),
  218. default=True,
  219. help_text=_('Permit create/update/delete operations using this key')
  220. )
  221. description = models.CharField(
  222. verbose_name=_('description'),
  223. max_length=200,
  224. blank=True
  225. )
  226. allowed_ips = ArrayField(
  227. base_field=IPNetworkField(),
  228. blank=True,
  229. null=True,
  230. verbose_name=_('allowed IPs'),
  231. help_text=_(
  232. 'Allowed IPv4/IPv6 networks from where the token can be used. Leave blank for no restrictions. '
  233. 'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"'
  234. ),
  235. )
  236. objects = RestrictedQuerySet.as_manager()
  237. class Meta:
  238. verbose_name = _('token')
  239. verbose_name_plural = _('tokens')
  240. def __str__(self):
  241. return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial
  242. def get_absolute_url(self):
  243. return reverse('users:token', args=[self.pk])
  244. @property
  245. def partial(self):
  246. return f'**********************************{self.key[-6:]}' if self.key else ''
  247. def save(self, *args, **kwargs):
  248. if not self.key:
  249. self.key = self.generate_key()
  250. return super().save(*args, **kwargs)
  251. @staticmethod
  252. def generate_key():
  253. # Generate a random 160-bit key expressed in hexadecimal.
  254. return binascii.hexlify(os.urandom(20)).decode()
  255. @property
  256. def is_expired(self):
  257. if self.expires is None or timezone.now() < self.expires:
  258. return False
  259. return True
  260. def validate_client_ip(self, client_ip):
  261. """
  262. Validate the API client IP address against the source IP restrictions (if any) set on the token.
  263. """
  264. if not self.allowed_ips:
  265. return True
  266. for ip_network in self.allowed_ips:
  267. if client_ip in IPNetwork(ip_network):
  268. return True
  269. return False
  270. #
  271. # Permissions
  272. #
  273. class ObjectPermission(models.Model):
  274. """
  275. A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
  276. identified by ORM query parameters.
  277. """
  278. name = models.CharField(
  279. verbose_name=_('name'),
  280. max_length=100
  281. )
  282. description = models.CharField(
  283. verbose_name=_('description'),
  284. max_length=200,
  285. blank=True
  286. )
  287. enabled = models.BooleanField(
  288. verbose_name=_('enabled'),
  289. default=True
  290. )
  291. object_types = models.ManyToManyField(
  292. to=ContentType,
  293. limit_choices_to=OBJECTPERMISSION_OBJECT_TYPES,
  294. related_name='object_permissions'
  295. )
  296. groups = models.ManyToManyField(
  297. to=Group,
  298. blank=True,
  299. related_name='object_permissions'
  300. )
  301. users = models.ManyToManyField(
  302. to=User,
  303. blank=True,
  304. related_name='object_permissions'
  305. )
  306. actions = ArrayField(
  307. base_field=models.CharField(max_length=30),
  308. help_text=_("The list of actions granted by this permission")
  309. )
  310. constraints = models.JSONField(
  311. blank=True,
  312. null=True,
  313. verbose_name=_('constraints'),
  314. help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
  315. )
  316. objects = RestrictedQuerySet.as_manager()
  317. class Meta:
  318. ordering = ['name']
  319. verbose_name = _('permission')
  320. verbose_name_plural = _('permissions')
  321. def __str__(self):
  322. return self.name
  323. @property
  324. def can_view(self):
  325. return 'view' in self.actions
  326. @property
  327. def can_add(self):
  328. return 'add' in self.actions
  329. @property
  330. def can_change(self):
  331. return 'change' in self.actions
  332. @property
  333. def can_delete(self):
  334. return 'delete' in self.actions
  335. def list_constraints(self):
  336. """
  337. Return all constraint sets as a list (even if only a single set is defined).
  338. """
  339. if type(self.constraints) is not list:
  340. return [self.constraints]
  341. return self.constraints
  342. def get_absolute_url(self):
  343. return reverse('users:objectpermission', args=[self.pk])