Clean up display of tokens in views

This commit is contained in:
jeremystretch 2022-11-02 10:00:57 -04:00
parent 08b965b55e
commit 9f4135a23a
7 changed files with 60 additions and 52 deletions

View File

@ -72,6 +72,9 @@ ADMINS = [
# ('John Doe', 'jdoe@example.com'), # ('John Doe', 'jdoe@example.com'),
] ]
# Permit the retrieval of API tokens after their creation.
ALLOW_TOKEN_RETRIEVAL = False
# Enable any desired validators for local account passwords below. For a list of included validators, please see the # Enable any desired validators for local account passwords below. For a list of included validators, please see the
# Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation. # Django documentation at https://docs.djangoproject.com/en/stable/topics/auth/passwords/#password-validation.
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
@ -224,7 +227,3 @@ TIME_FORMAT = 'g:i a'
SHORT_TIME_FORMAT = 'H:i:s' SHORT_TIME_FORMAT = 'H:i:s'
DATETIME_FORMAT = 'N j, Y g:i a' DATETIME_FORMAT = 'N j, Y g:i a'
SHORT_DATETIME_FORMAT = 'Y-m-d H:i' SHORT_DATETIME_FORMAT = 'Y-m-d H:i'
# Allow API Tokens to be viewed after creation. Before NetBox 3.4 the default was to allow viewing of the tokens
# so this flag was created for backwards compatability.
ALLOW_TOKEN_RETRIEVAL = False

View File

@ -109,11 +109,9 @@ Context:
<button type="submit" name="_create" class="btn btn-primary"> <button type="submit" name="_create" class="btn btn-primary">
Create Create
</button> </button>
{% if not disable_addanother %}
<button type="submit" name="_addanother" class="btn btn-outline-primary"> <button type="submit" name="_addanother" class="btn btn-outline-primary">
Create & Add Another Create & Add Another
</button> </button>
{% endif %}
{% endif %} {% endif %}
<a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a> <a class="btn btn-outline-danger" href="{{ return_url }}">Cancel</a>
{% endblock buttons %} {% endblock buttons %}

View File

@ -6,31 +6,55 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col col-md-12"> <div class="col col-md-12">
{% if not settings.ALLOW_TOKEN_RETRIEVAL %}
<div class="alert alert-danger" role="alert">
<i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-token" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
</div>
{% endif %}
<div class="card"> <div class="card">
<h5 class="card-header">Token Created</h5> <h5 class="card-header">Token</h5>
<div class="card-body"> <div class="card-body">
<p>The token has been created, you will need to copy the key as it will no longer be displayed for security purposes.</p>
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
<td>Key</td> <th scope="row">Key</th>
<td>{{ object.key }}</td> <td>
<div class="float-end">
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_id" title="Copy to clipboard">
<i class="mdi mdi-content-copy"></i>
</a>
</div>
<div id="token_id">{{ key }}</div>
</td>
</tr>
<tr>
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">User</th>
<td>{{ object.user }}</td>
</tr>
<tr>
<th scope="row">Created</th>
<td>{{ object.created|annotated_date }}</td>
</tr>
<tr>
<th scope="row">Expires</th>
<td>
{% if object.expires %}
{{ object.expires|annotated_date }}
{% else %}
<span>Never</span>
{% endif %}
</td>
</tr> </tr>
</table> </table>
<form method="post">
{% csrf_token %}
{% render_form form %}
<div class="row my-3">
<div class="col col-md-12 text-center">
<button type="submit" name="_addanother" class="btn btn-outline-primary">
Add Another
</button>
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
</div>
</div>
</form>
</div> </div>
</div> </div>
{% plugin_left_page object %} <div class="col col-md-12 text-center">
<a href="{% url 'users:token_add' %}" class="btn btn-outline-primary">Add Another</a>
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -121,13 +121,7 @@ class TokenForm(BootstrapMixin, forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id and not settings.ALLOW_TOKEN_RETRIEVAL:
keyfield = self.fields['key']
keyfield.disabled = True
keyfield.required = False
keyfield.widget = forms.HiddenInput()
# Omit the key field if token retrieval is not permitted
class TokenViewForm(BootstrapMixin, forms.Form): if self.instance.pk and not settings.ALLOW_TOKEN_RETRIEVAL:
view_token = forms.BooleanField(widget=forms.HiddenInput(), required=False) del self.fields['key']

View File

@ -1,6 +1,7 @@
import binascii import binascii
import os import os
from django.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField 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"', 'Ex: "10.1.1.0/24, 192.168.10.16/32, 2001:DB8:1::/64"',
) )
class Meta:
pass
def __str__(self): def __str__(self):
# Only display the last 24 bits of the token to avoid accidental exposure. return self.key if settings.ALLOW_TOKEN_RETRIEVAL else self.partial
return f"{self.description or self.key[-6:]} ({self.user})"
@property
def partial(self):
return f'**********************************{self.key[-6:]}' if self.key else ''
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.key: if not self.key:

View File

@ -1,4 +1,3 @@
from django.conf import settings
from .models import Token from .models import Token
from netbox.tables import NetBoxTable, columns from netbox.tables import NetBoxTable, columns
@ -7,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:", " }}""" ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """ COPY_BUTTON = """
<a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard"> {% if settings.ALLOW_TOKEN_RETRIEVAL %}
<i class="mdi mdi-content-copy"></i> <a class="btn btn-sm btn-success copy-token" data-clipboard-target="#token_{{ record.pk }}" title="Copy to clipboard">
</a> <i class="mdi mdi-content-copy"></i>
</a>
{% endif %}
""" """
@ -41,9 +42,3 @@ class TokenTable(NetBoxTable):
fields = ( fields = (
'pk', 'description', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips', 'pk', 'description', 'key', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
) )
def render_key(self, value):
if settings.ALLOW_TOKEN_RETRIEVAL:
return value
else:
return "****************************************"

View File

@ -20,7 +20,7 @@ from extras.tables import ObjectChangeTable
from netbox.authentication import get_auth_backend_display, get_saml_idps from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config from netbox.config import get_config
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from .forms import LoginForm, PasswordChangeForm, TokenForm, TokenViewForm, UserConfigForm from .forms import LoginForm, PasswordChangeForm, TokenForm, UserConfigForm
from .models import Token, UserConfig from .models import Token, UserConfig
from .tables import TokenTable from .tables import TokenTable
@ -261,7 +261,6 @@ class TokenEditView(LoginRequiredMixin, View):
'object': token, 'object': token,
'form': form, 'form': form,
'return_url': reverse('users:token_list'), 'return_url': reverse('users:token_list'),
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
}) })
def post(self, request, pk=None): def post(self, request, pk=None):
@ -288,10 +287,8 @@ class TokenEditView(LoginRequiredMixin, View):
messages.success(request, msg) messages.success(request, msg)
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL: if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
form = TokenViewForm(initial={'view_token': True})
return render(request, 'users/api_token.html', { return render(request, 'users/api_token.html', {
'object': token, 'object': token,
'form': form,
'key': token.key, 'key': token.key,
'return_url': reverse('users:token_list'), 'return_url': reverse('users:token_list'),
}) })