Răsfoiți Sursa

Merge pull request #9595 from netbox-community/9536-token-last-used

Closes #9536: Record last used time for API tokens
Jeremy Stretch 3 ani în urmă
părinte
comite
12bd3840f9

+ 4 - 0
docs/release-notes/version-3.3.md

@@ -27,6 +27,7 @@
 * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
 * [#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
 * [#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
 * [#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
 * [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
 
 
 ### Other Changes
 ### Other Changes
@@ -55,6 +56,9 @@
 * ipam.IPAddress
 * ipam.IPAddress
     * The `nat_inside` field no longer requires a unique value
     * 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
     * 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
 * virtualization.Cluster
     * Added required `status` field (default value: `active`)
     * Added required `status` field (default value: `active`)
 * virtualization.VirtualMachine
 * virtualization.VirtualMachine

+ 5 - 0
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
 ## 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.
 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.

+ 14 - 0
netbox/netbox/api/authentication.py

@@ -1,7 +1,11 @@
+import logging
+
 from django.conf import settings
 from django.conf import settings
+from django.utils import timezone
 from rest_framework import authentication, exceptions
 from rest_framework import authentication, exceptions
 from rest_framework.permissions import BasePermission, DjangoObjectPermissions, SAFE_METHODS
 from rest_framework.permissions import BasePermission, DjangoObjectPermissions, SAFE_METHODS
 
 
+from netbox.config import get_config
 from users.models import Token
 from users.models import Token
 from utilities.request import get_client_ip
 from utilities.request import get_client_ip
 
 
@@ -40,6 +44,16 @@ class TokenAuthentication(authentication.TokenAuthentication):
         except model.DoesNotExist:
         except model.DoesNotExist:
             raise exceptions.AuthenticationFailed("Invalid token")
             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.
         # Enforce the Token's expiration time, if one has been set.
         if token.is_expired:
         if token.is_expired:
             raise exceptions.AuthenticationFailed("Token expired")
             raise exceptions.AuthenticationFailed("Token expired")

+ 4 - 0
netbox/netbox/tests/test_authentication.py

@@ -31,6 +31,10 @@ class TokenAuthenticationTestCase(APITestCase):
         response = self.client.get(url, HTTP_AUTHORIZATION=f'Token {token.key}')
         response = self.client.get(url, HTTP_AUTHORIZATION=f'Token {token.key}')
         self.assertEqual(response.status_code, 200)
         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=['*'])
     @override_settings(LOGIN_REQUIRED=True, EXEMPT_VIEW_PERMISSIONS=['*'])
     def test_token_expiration(self):
     def test_token_expiration(self):
         url = reverse('dcim-api:site-list')
         url = reverse('dcim-api:site-list')

+ 8 - 0
netbox/templates/users/api_tokens.html

@@ -34,6 +34,14 @@
                                     <span>Never</span>
                                     <span>Never</span>
                                 {% endif %}
                                 {% endif %}
                             </div>
                             </div>
+                            <div class="col col-md-3">
+                                <small class="text-muted">Last Used</small><br />
+                                {% if token.last_used %}
+                                    {{ token.last_used|annotated_date }}
+                                {% else %}
+                                    <span>Never</span>
+                                {% endif %}
+                            </div>
                             <div class="col col-md-3">
                             <div class="col col-md-3">
                                 <small class="text-muted">Create/Edit/Delete Operations</small><br />
                                 <small class="text-muted">Create/Edit/Delete Operations</small><br />
                                 {% if token.write_enabled %}
                                 {% if token.write_enabled %}

+ 1 - 1
netbox/users/admin/__init__.py

@@ -58,7 +58,7 @@ class UserAdmin(UserAdmin_):
 class TokenAdmin(admin.ModelAdmin):
 class TokenAdmin(admin.ModelAdmin):
     form = forms.TokenAdminForm
     form = forms.TokenAdminForm
     list_display = [
     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):
     def list_allowed_ips(self, obj):

+ 1 - 1
netbox/users/api/serializers.py

@@ -74,7 +74,7 @@ class TokenSerializer(ValidatedModelSerializer):
     class Meta:
     class Meta:
         model = Token
         model = Token
         fields = (
         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',
             'allowed_ips',
         )
         )
 
 

+ 6 - 3
netbox/users/migrations/0003_token_allowed_ips.py → 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
 import django.contrib.postgres.fields
-from django.db import migrations
+from django.db import migrations, models
 import ipam.fields
 import ipam.fields
 
 
 
 
@@ -17,4 +15,9 @@ class Migration(migrations.Migration):
             name='allowed_ips',
             name='allowed_ips',
             field=django.contrib.postgres.fields.ArrayField(base_field=ipam.fields.IPNetworkField(), blank=True, null=True, size=None),
             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),
+        ),
     ]
     ]

+ 4 - 0
netbox/users/models.py

@@ -204,6 +204,10 @@ class Token(models.Model):
         blank=True,
         blank=True,
         null=True
         null=True
     )
     )
+    last_used = models.DateTimeField(
+        blank=True,
+        null=True
+    )
     key = models.CharField(
     key = models.CharField(
         max_length=40,
         max_length=40,
         unique=True,
         unique=True,