From cb65278931eb9cb665c7e2874aeed2cb137d833d Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Fri, 17 Jun 2022 14:45:56 +0200 Subject: [PATCH 1/8] Add last_used to Token model and update when used --- netbox/netbox/api/authentication.py | 5 +++++ netbox/users/admin/__init__.py | 2 +- .../users/migrations/0003_token_last_used.py | 18 ++++++++++++++++++ netbox/users/models.py | 4 ++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 netbox/users/migrations/0003_token_last_used.py diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 5e177bfcb..f40141de4 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.utils import timezone from rest_framework import authentication, exceptions from rest_framework.permissions import BasePermission, DjangoObjectPermissions, SAFE_METHODS @@ -18,6 +19,10 @@ class TokenAuthentication(authentication.TokenAuthentication): except model.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid token") + # Update last used. + token.last_used = timezone.now() + token.save() + # Enforce the Token's expiration time, if one has been set. if token.is_expired: raise exceptions.AuthenticationFailed("Token expired") diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py index 1b163ed06..320c28df2 100644 --- a/netbox/users/admin/__init__.py +++ b/netbox/users/admin/__init__.py @@ -58,7 +58,7 @@ class UserAdmin(UserAdmin_): class TokenAdmin(admin.ModelAdmin): form = forms.TokenAdminForm list_display = [ - 'key', 'user', 'created', 'expires', 'write_enabled', 'description' + 'key', 'user', 'created', 'expires', 'last_used', 'write_enabled', 'description' ] diff --git a/netbox/users/migrations/0003_token_last_used.py b/netbox/users/migrations/0003_token_last_used.py new file mode 100644 index 000000000..cc014e59c --- /dev/null +++ b/netbox/users/migrations/0003_token_last_used.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-06-16 15:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_standardize_id_fields'), + ] + + operations = [ + migrations.AddField( + model_name='token', + name='last_used', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/netbox/users/models.py b/netbox/users/models.py index 23068442e..a0055914b 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -203,6 +203,10 @@ class Token(models.Model): blank=True, null=True ) + last_used = models.DateTimeField( + blank=True, + null=True + ) key = models.CharField( max_length=40, unique=True, From 83255a70d59adce7ab9d89f5ad803c476973957b Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Fri, 17 Jun 2022 14:57:19 +0200 Subject: [PATCH 2/8] Only update every 60 seconds --- netbox/netbox/api/authentication.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index f40141de4..3f223cf98 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -19,9 +19,11 @@ class TokenAuthentication(authentication.TokenAuthentication): except model.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid token") - # Update last used. - token.last_used = timezone.now() - token.save() + # Update last used, but only once a minute. This reduces the write load on the db + timediff = timezone.now() - token.last_used + if timediff.total_seconds() > 60: + token.last_used = timezone.now() + token.save() # Enforce the Token's expiration time, if one has been set. if token.is_expired: From 56f155fd7f2518f0035f6018676fc6991db999e2 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Fri, 17 Jun 2022 14:58:20 +0200 Subject: [PATCH 3/8] Rename timediff to lasted_used_diff --- netbox/netbox/api/authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 3f223cf98..fce9036aa 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -20,8 +20,8 @@ class TokenAuthentication(authentication.TokenAuthentication): raise exceptions.AuthenticationFailed("Invalid token") # Update last used, but only once a minute. This reduces the write load on the db - timediff = timezone.now() - token.last_used - if timediff.total_seconds() > 60: + last_used_diff = timezone.now() - token.last_used + if last_used_diff.total_seconds() > 60: token.last_used = timezone.now() token.save() From 09ca661dd93bd2612925ff29ded1bac23ef9ddfb Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Fri, 17 Jun 2022 15:52:12 +0200 Subject: [PATCH 4/8] Fix last_used=None error --- netbox/netbox/api/authentication.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index fce9036aa..39008f366 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -20,8 +20,7 @@ class TokenAuthentication(authentication.TokenAuthentication): raise exceptions.AuthenticationFailed("Invalid token") # Update last used, but only once a minute. This reduces the write load on the db - last_used_diff = timezone.now() - token.last_used - if last_used_diff.total_seconds() > 60: + if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 60: token.last_used = timezone.now() token.save() From 4b06390ada10a9a727925d32a38f79c375a39c4d Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Sun, 19 Jun 2022 12:40:52 +0200 Subject: [PATCH 5/8] Disable token last_used update when in Maint mode --- netbox/netbox/api/authentication.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 39008f366..e98376899 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -1,8 +1,11 @@ +import logging + from django.conf import settings from django.utils import timezone from rest_framework import authentication, exceptions from rest_framework.permissions import BasePermission, DjangoObjectPermissions, SAFE_METHODS +from netbox.config import get_config from users.models import Token @@ -20,9 +23,15 @@ class TokenAuthentication(authentication.TokenAuthentication): raise exceptions.AuthenticationFailed("Invalid token") # Update last used, but only once a minute. This reduces the write load on the db - if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 60: - token.last_used = timezone.now() - token.save() + if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 6: + # If maintenance mode is enabled, assume the database is read-only, and disable updating the token's + # last_used time upon authentication. + if get_config().MAINTENANCE_MODE: + logger = logging.getLogger('netbox.auth.login') + logger.warning("Maintenance mode enabled: disabling update of token's last used timestamp") + else: + token.last_used = timezone.now() + token.save() # Enforce the Token's expiration time, if one has been set. if token.is_expired: From d5ad599ed4ed1f032a4cb3e16ae381e5c9dd4f16 Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Sun, 19 Jun 2022 12:41:44 +0200 Subject: [PATCH 6/8] Correct delay time from 6 to 60 seconds --- netbox/netbox/api/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index e98376899..847bcbfd9 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -23,7 +23,7 @@ class TokenAuthentication(authentication.TokenAuthentication): raise exceptions.AuthenticationFailed("Invalid token") # Update last used, but only once a minute. This reduces the write load on the db - if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 6: + if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 60: # If maintenance mode is enabled, assume the database is read-only, and disable updating the token's # last_used time upon authentication. if get_config().MAINTENANCE_MODE: From 4fdce6a348ca254d7afb3be65b94734ff4f59c5e Mon Sep 17 00:00:00 2001 From: Pieter Lambrecht Date: Sun, 19 Jun 2022 13:03:03 +0200 Subject: [PATCH 7/8] Show LastUsed in /user/api-tokens/ --- netbox/templates/users/api_tokens.html | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index 01ffec23a..a019cbd1f 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -22,11 +22,11 @@
-
+
Created
{{ token.created|annotated_date }}
-
+
Expires
{% if token.expires %} {{ token.expires|annotated_date }} @@ -34,7 +34,15 @@ Never {% endif %}
-
+
+ Last Used
+ {% if token.last_used %} + {{ token.last_used|annotated_date }} + {% else %} + Never + {% endif %} +
+
Create/Edit/Delete Operations
{% if token.write_enabled %} Enabled From f9937758f9e4e6f5ecc5a738e66c45116b65da38 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Thu, 23 Jun 2022 12:09:00 -0400 Subject: [PATCH 8/8] Documentation & clenaup for #9536 --- docs/release-notes/version-3.3.md | 4 ++++ docs/rest-api/authentication.md | 5 +++++ netbox/netbox/api/authentication.py | 7 +++---- netbox/netbox/tests/test_authentication.py | 4 ++++ netbox/users/api/serializers.py | 2 +- ....py => 0003_token_allowed_ips_last_used.py} | 9 ++++++--- .../users/migrations/0003_token_last_used.py | 18 ------------------ 7 files changed, 23 insertions(+), 26 deletions(-) rename netbox/users/migrations/{0003_token_allowed_ips.py => 0003_token_allowed_ips_last_used.py} (68%) delete mode 100644 netbox/users/migrations/0003_token_last_used.py diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 81125451e..f76cab4e2 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -27,6 +27,7 @@ * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results * [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields +* [#9536](https://github.com/netbox-community/netbox/issues/9536) - Track API token usage times * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location ### Other Changes @@ -55,6 +56,9 @@ * ipam.IPAddress * The `nat_inside` field no longer requires a unique value * The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses +* users.Token + * Added the `allowed_ips` array field + * Added the read-only `last_used` datetime field * virtualization.Cluster * Added required `status` field (default value: `active`) * virtualization.VirtualMachine diff --git a/docs/rest-api/authentication.md b/docs/rest-api/authentication.md index 11b8cd6bf..18b6bc4f8 100644 --- a/docs/rest-api/authentication.md +++ b/docs/rest-api/authentication.md @@ -29,6 +29,11 @@ $ curl https://netbox/api/dcim/sites/ } ``` +When a token is used to authenticate a request, its `last_updated` time updated to the current time if its last use was recorded more than 60 seconds ago (or was never recorded). This allows users to determine which tokens have been active recently. + +!!! note + The "last used" time for tokens will not be updated while maintenance mode is enabled. + ## Initial Token Provisioning Ideally, each user should provision his or her own REST API token(s) via the web UI. However, you may encounter where a token must be created by a user via the REST API itself. NetBox provides a special endpoint to provision tokens using a valid username and password combination. diff --git a/netbox/netbox/api/authentication.py b/netbox/netbox/api/authentication.py index 1b7def3a3..b8607a0bb 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -44,16 +44,15 @@ class TokenAuthentication(authentication.TokenAuthentication): except model.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid token") - # Update last used, but only once a minute. This reduces the write load on the db + # Update last used, but only once per minute at most. This reduces write load on the database if not token.last_used or (timezone.now() - token.last_used).total_seconds() > 60: # If maintenance mode is enabled, assume the database is read-only, and disable updating the token's # last_used time upon authentication. if get_config().MAINTENANCE_MODE: logger = logging.getLogger('netbox.auth.login') - logger.warning("Maintenance mode enabled: disabling update of token's last used timestamp") + logger.debug("Maintenance mode enabled: Disabling update of token's last used timestamp") else: - token.last_used = timezone.now() - token.save() + Token.objects.filter(pk=token.pk).update(last_used=timezone.now()) # Enforce the Token's expiration time, if one has been set. if token.is_expired: diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py index 6597684fb..ef4554b4b 100644 --- a/netbox/netbox/tests/test_authentication.py +++ b/netbox/netbox/tests/test_authentication.py @@ -31,6 +31,10 @@ class TokenAuthenticationTestCase(APITestCase): response = self.client.get(url, HTTP_AUTHORIZATION=f'Token {token.key}') self.assertEqual(response.status_code, 200) + # Check that the token's last_used time has been updated + token.refresh_from_db() + self.assertIsNotNone(token.last_used) + @override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*']) def test_token_expiration(self): url = reverse('dcim-api:site-list') diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 177cce39c..d05f6c7da 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -74,7 +74,7 @@ class TokenSerializer(ValidatedModelSerializer): class Meta: model = Token fields = ( - 'id', 'url', 'display', 'user', 'created', 'expires', 'key', 'write_enabled', 'description', + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', 'allowed_ips', ) diff --git a/netbox/users/migrations/0003_token_allowed_ips.py b/netbox/users/migrations/0003_token_allowed_ips_last_used.py similarity index 68% rename from netbox/users/migrations/0003_token_allowed_ips.py rename to netbox/users/migrations/0003_token_allowed_ips_last_used.py index f4eaa9f96..946226f75 100644 --- a/netbox/users/migrations/0003_token_allowed_ips.py +++ b/netbox/users/migrations/0003_token_allowed_ips_last_used.py @@ -1,7 +1,5 @@ -# Generated by Django 3.2.12 on 2022-04-19 12:37 - import django.contrib.postgres.fields -from django.db import migrations +from django.db import migrations, models import ipam.fields @@ -17,4 +15,9 @@ class Migration(migrations.Migration): name='allowed_ips', field=django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None), ), + migrations.AddField( + model_name='token', + name='last_used', + field=models.DateTimeField(blank=True, null=True), + ), ] diff --git a/netbox/users/migrations/0003_token_last_used.py b/netbox/users/migrations/0003_token_last_used.py deleted file mode 100644 index cc014e59c..000000000 --- a/netbox/users/migrations/0003_token_last_used.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.0.4 on 2022-06-16 15:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('users', '0002_standardize_id_fields'), - ] - - operations = [ - migrations.AddField( - model_name='token', - name='last_used', - field=models.DateTimeField(blank=True, null=True), - ), - ]