Skip to content

Commit

Permalink
Merge pull request #9595 from netbox-community/9536-token-last-used
Browse files Browse the repository at this point in the history
Closes #9536: Record last used time for API tokens
  • Loading branch information
jeremystretch authored Jun 23, 2022
2 parents f563ba7 + dc05e62 commit 12bd384
Show file tree
Hide file tree
Showing 9 changed files with 47 additions and 5 deletions.
4 changes: 4 additions & 0 deletions docs/release-notes/version-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/rest-api/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions netbox/netbox/api/authentication.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -40,6 +44,16 @@ def authenticate_credentials(self, key):
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")
Expand Down
4 changes: 4 additions & 0 deletions netbox/netbox/tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ def test_token_authentication(self):
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')
Expand Down
8 changes: 8 additions & 0 deletions netbox/templates/users/api_tokens.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
<span>Never</span>
{% endif %}
</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">
<small class="text-muted">Create/Edit/Delete Operations</small><br />
{% if token.write_enabled %}
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def get_inlines(self, request, obj):
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):
Expand Down
2 changes: 1 addition & 1 deletion netbox/users/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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),
),
]
4 changes: 4 additions & 0 deletions netbox/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 12bd384

Please sign in to comment.