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 ea66dc5a6..b8607a0bb 100644 --- a/netbox/netbox/api/authentication.py +++ b/netbox/netbox/api/authentication.py @@ -1,7 +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 from utilities.request import get_client_ip @@ -40,6 +44,16 @@ class TokenAuthentication(authentication.TokenAuthentication): except model.DoesNotExist: raise exceptions.AuthenticationFailed("Invalid token") + # 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.debug("Maintenance mode enabled: Disabling update of token's last used timestamp") + else: + 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: raise exceptions.AuthenticationFailed("Token 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/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index 360e65a67..24b32cc9b 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -34,6 +34,14 @@ Never {% endif %} +
+ Last Used
+ {% if token.last_used %} + {{ token.last_used|annotated_date }} + {% else %} + Never + {% endif %} +
Create/Edit/Delete Operations
{% if token.write_enabled %} diff --git a/netbox/users/admin/__init__.py b/netbox/users/admin/__init__.py index ede26cd1b..2db822cfe 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', 'list_allowed_ips' + 'key', 'user', 'created', 'expires', 'last_used', 'write_enabled', 'description', 'list_allowed_ips' ] def list_allowed_ips(self, obj): 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/models.py b/netbox/users/models.py index 704516c71..4ee4dce6b 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -204,6 +204,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,