models.py 12 KB

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