mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
8853 Prevent the retrieval of API tokens after creation (#10645)
* 8853 hide api token * 8853 hide key on edit * 8853 add key display * 8853 cleanup html * 8853 make token view accessible only once on POST * Clean up display of tokens in views * Honor ALLOW_TOKEN_RETRIEVAL in API serializer * Add docs & tweak default setting * Include token key when provisioning with user credentials Co-authored-by: jeremystretch <jstretch@ns1.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
@@ -63,7 +64,13 @@ class GroupSerializer(ValidatedModelSerializer):
|
||||
|
||||
class TokenSerializer(ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
|
||||
key = serializers.CharField(min_length=40, max_length=40, allow_blank=True, required=False)
|
||||
key = serializers.CharField(
|
||||
min_length=40,
|
||||
max_length=40,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
write_only=not settings.ALLOW_TOKEN_RETRIEVAL
|
||||
)
|
||||
user = NestedUserSerializer()
|
||||
allowed_ips = serializers.ListField(
|
||||
child=IPNetworkSerializer(),
|
||||
|
@@ -88,6 +88,8 @@ class TokenProvisionView(APIView):
|
||||
token = Token(user=user)
|
||||
token.save()
|
||||
data = serializers.TokenSerializer(token, context={'request': request}).data
|
||||
# Manually append the token key, which is normally write-only
|
||||
data['key'] = token.key
|
||||
|
||||
return Response(data, status=HTTP_201_CREATED)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm
|
||||
from django.contrib.postgres.forms import SimpleArrayField
|
||||
from django.utils.html import mark_safe
|
||||
@@ -117,3 +118,10 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
|
||||
widgets = {
|
||||
'expires': DateTimePicker(),
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Omit the key field if token retrieval is not permitted
|
||||
if self.instance.pk and not settings.ALLOW_TOKEN_RETRIEVAL:
|
||||
del self.fields['key']
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import binascii
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -230,12 +231,12 @@ class Token(models.Model):
|
||||
'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
# Only display the last 24 bits of the token to avoid accidental exposure.
|
||||
return f"{self.key[-6:]} ({self.user})"
|
||||
return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial
|
||||
|
||||
@property
|
||||
def partial(self):
|
||||
return f'**********************************{self.key[-6:]}' if self.key else ''
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.key:
|
||||
|
@@ -6,14 +6,16 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ value }}</span></samp>"""
|
||||
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
|
||||
|
||||
ALLOWED_IPS = """{{ value|join:", " }}"""
|
||||
|
||||
COPY_BUTTON = """
|
||||
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
|
||||
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
|
||||
<i class="mdi mdi-content-copy"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
||||
@@ -38,5 +40,5 @@ class TokenTable(NetBoxTable):
|
||||
class Meta(NetBoxTable.Meta):
|
||||
model = Token
|
||||
fields = (
|
||||
'pk', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', 'description',
|
||||
'pk', 'description', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
|
||||
)
|
||||
|
@@ -273,6 +273,7 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
form = TokenForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
|
||||
token = form.save(commit=False)
|
||||
token.user = request.user
|
||||
token.save()
|
||||
@@ -280,7 +281,13 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
msg = f"Modified token {token}" if pk else f"Created token {token}"
|
||||
messages.success(request, msg)
|
||||
|
||||
if '_addanother' in request.POST:
|
||||
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
|
||||
return render(request, 'users/api_token.html', {
|
||||
'object': token,
|
||||
'key': token.key,
|
||||
'return_url': reverse('users:token_list'),
|
||||
})
|
||||
elif '_addanother' in request.POST:
|
||||
return redirect(request.path)
|
||||
else:
|
||||
return redirect('users:token_list')
|
||||
@@ -289,6 +296,7 @@ class TokenEditView(LoginRequiredMixin, View):
|
||||
'object': token,
|
||||
'form': form,
|
||||
'return_url': reverse('users:token_list'),
|
||||
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
|
||||
})
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user