From f82f084c02f0d1b2aafc6650b7174dbc541686aa Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Oct 2025 16:33:04 -0400 Subject: [PATCH] Misc cleanup --- netbox/users/models/tokens.py | 36 +++++++++++++++++++++++------------ netbox/users/utils.py | 2 +- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/netbox/users/models/tokens.py b/netbox/users/models/tokens.py index 3e8e0f108..e452d2ab7 100644 --- a/netbox/users/models/tokens.py +++ b/netbox/users/models/tokens.py @@ -29,6 +29,8 @@ class Token(models.Model): An API token used for user authentication. This extends the stock model to allow each user to have multiple tokens. It also supports setting an expiration time and toggling write ability. """ + _token = None + version = models.PositiveSmallIntegerField( verbose_name=_('version'), choices=TokenVersionChoices, @@ -136,12 +138,12 @@ class Token(models.Model): def __init__(self, *args, token=None, **kwargs): super().__init__(*args, **kwargs) + # This stores the initial plaintext value (if given) on the creation of a new Token. If not provided, a + # random token value will be generated and assigned immediately prior to saving the Token instance. self.token = token def __str__(self): - if self.v1: - return self.partial - return self.key + return self.key if self.v2 else self.partial def get_absolute_url(self): return reverse('users:token', args=[self.pk]) @@ -156,14 +158,19 @@ class Token(models.Model): @property def partial(self): + """ + Return a sanitized representation of a v1 token. + """ return f'**********************************{self.plaintext[-6:]}' if self.plaintext else '' @property def token(self): - return getattr(self, '_token', None) + return self._token @token.setter def token(self, value): + if not self._state.adding: + raise ValueError("Cannot assign a new plaintext value for an existing token.") self._token = value if value is not None: if self.v1: @@ -173,8 +180,11 @@ class Token(models.Model): self.update_digest() def clean(self): - if self._state.adding and self.v2 and not settings.API_TOKEN_PEPPERS: - raise ValidationError(_("Cannot create v2 tokens: API_TOKEN_PEPPERS is not defined.")) + if self._state.adding: + if self.pepper_id is not None and self.pepper_id not in settings.API_TOKEN_PEPPERS: + raise ValidationError(_( + "Invalid pepper ID: {id}. Check configured API_TOKEN_PEPPERS." + ).format(id=self.pepper_id)) def save(self, *args, **kwargs): # If creating a new Token and no token value has been specified, generate one @@ -201,9 +211,9 @@ class Token(models.Model): """ Recalculate and save the HMAC digest using the currently defined pepper and token values. """ - self.pepper_id, pepper_value = get_current_pepper() + self.pepper_id, pepper = get_current_pepper() self.hmac_digest = hmac.new( - pepper_value.encode('utf-8'), + pepper.encode('utf-8'), self.token.encode('utf-8'), hashlib.sha256 ).hexdigest() @@ -216,12 +226,14 @@ class Token(models.Model): def validate(self, token): """ - Returns true if the given token value validates. + Validate the given plaintext against the token. + + For v1 tokens, check that the given value is equal to the stored plaintext. For v2 tokens, calculate an HMAC + from the Token's pepper ID and the given plaintext value, and check whether the result matches the recorded + digest. """ - if self.is_expired: - return False if self.v1: - return token == self.key + return token == self.token if self.v2: try: pepper = settings.API_TOKEN_PEPPERS[self.pepper_id] diff --git a/netbox/users/utils.py b/netbox/users/utils.py index 045d192c7..5db8cb65e 100644 --- a/netbox/users/utils.py +++ b/netbox/users/utils.py @@ -22,5 +22,5 @@ def get_current_pepper(): """ if len(settings.API_TOKEN_PEPPERS) < 1: raise ImproperlyConfigured("Must define API_TOKEN_PEPPERS to use v2 API tokens") - newest_id = sorted(settings.API_TOKEN_PEPPERS)[-1] + newest_id = sorted(settings.API_TOKEN_PEPPERS.keys())[-1] return newest_id, settings.API_TOKEN_PEPPERS[newest_id]