diff --git a/netbox/templates/users/_user.html b/netbox/templates/users/_user.html index 535a14cf2..71eacac6e 100644 --- a/netbox/templates/users/_user.html +++ b/netbox/templates/users/_user.html @@ -9,11 +9,21 @@
diff --git a/netbox/templates/users/api_tokens.html b/netbox/templates/users/api_tokens.html index 2dd4d9813..659b01a70 100644 --- a/netbox/templates/users/api_tokens.html +++ b/netbox/templates/users/api_tokens.html @@ -9,28 +9,36 @@ {% for token in tokens %}
- {% if token.is_expired %} -
- Expired -
- {% endif %} +
+ Edit + Delete +
{{ token.key }} + {% if token.is_expired %} + Expired + {% endif %}
- Created: {{ token.created|date }} + {{ token.created|date }}
+ Created
- Expires: {{ token.expires|default:"Never" }} + {% if token.expires %} + {{ token.expires|date }}
+ {% else %} + Never
+ {% endif %} + Expires
- Write operations: {% if token.write_enabled %} Enabled {% else %} Disabled - {% endif %} + {% endif %}
+ Create/edit/delete operations
{% if token.description %} @@ -38,7 +46,13 @@ {% endif %}
+ {% empty %} +

You do not have any API tokens.

{% endfor %} + + + Add a token +
{% endblock %} diff --git a/netbox/users/forms.py b/netbox/users/forms.py index b3330ddbf..d84bac0e1 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -1,6 +1,8 @@ from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm as DjangoPasswordChangeForm +from django import forms from utilities.forms import BootstrapMixin +from .models import Token class LoginForm(BootstrapMixin, AuthenticationForm): @@ -14,3 +16,14 @@ class LoginForm(BootstrapMixin, AuthenticationForm): class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm): pass + + +class TokenForm(BootstrapMixin, forms.ModelForm): + key = forms.CharField(required=False, help_text="If no key is provided, one will be generated automatically.") + + class Meta: + model = Token + fields = ['key', 'write_enabled', 'expires', 'description'] + help_texts = { + 'expires': 'YYYY-MM-DD [HH:MM:SS]' + } diff --git a/netbox/users/migrations/0001_api_tokens.py b/netbox/users/migrations/0001_api_tokens.py index 3ab282277..d766b2ef0 100644 --- a/netbox/users/migrations/0001_api_tokens.py +++ b/netbox/users/migrations/0001_api_tokens.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.6 on 2017-03-08 03:52 +# Generated by Django 1.10.6 on 2017-03-08 15:32 from __future__ import unicode_literals from django.conf import settings +import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -22,7 +23,7 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True)), ('expires', models.DateTimeField(blank=True, null=True)), - ('key', models.CharField(max_length=40, unique=True)), + ('key', models.CharField(max_length=40, unique=True, validators=[django.core.validators.MinLengthValidator(40)])), ('write_enabled', models.BooleanField(default=True, help_text=b'Permit create/update/delete operations using this key')), ('description', models.CharField(blank=True, max_length=100)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to=settings.AUTH_USER_MODEL)), diff --git a/netbox/users/models.py b/netbox/users/models.py index 9191b2fd6..0dd303104 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -2,6 +2,7 @@ import binascii import os from django.contrib.auth.models import User +from django.core.validators import MinLengthValidator from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils import timezone @@ -16,7 +17,7 @@ class Token(models.Model): user = models.ForeignKey(User, related_name='tokens', on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) expires = models.DateTimeField(blank=True, null=True) - key = models.CharField(max_length=40, unique=True) + key = models.CharField(max_length=40, unique=True, validators=[MinLengthValidator(40)]) write_enabled = models.BooleanField(default=True, help_text="Permit create/update/delete operations using this key") description = models.CharField(max_length=100, blank=True) @@ -24,7 +25,8 @@ class Token(models.Model): default_permissions = [] def __str__(self): - return u"API key for {}".format(self.user) + # Only display the last 24 bits of the token to avoid accidental exposure. + return u"{} ({})".format(self.key[-6:], self.user) def save(self, *args, **kwargs): if not self.key: diff --git a/netbox/users/urls.py b/netbox/users/urls.py index 31ff11830..e27e61b8d 100644 --- a/netbox/users/urls.py +++ b/netbox/users/urls.py @@ -8,7 +8,10 @@ urlpatterns = [ # User profiles url(r'^profile/$', views.profile, name='profile'), url(r'^profile/password/$', views.change_password, name='change_password'), - url(r'^profile/api-tokens/$', views.TokenList.as_view(), name='api_tokens'), + url(r'^profile/api-tokens/$', views.TokenListView.as_view(), name='token_list'), + url(r'^profile/api-tokens/add/$', views.TokenEditView.as_view(), name='token_add'), + url(r'^profile/api-tokens/(?P\d+)/edit/$', views.TokenEditView.as_view(), name='token_edit'), + url(r'^profile/api-tokens/(?P\d+)/delete/$', views.TokenDeleteView.as_view(), name='token_delete'), url(r'^profile/user-key/$', views.userkey, name='userkey'), url(r'^profile/user-key/edit/$', views.userkey_edit, name='userkey_edit'), url(r'^profile/recent-activity/$', views.recent_activity, name='recent_activity'), diff --git a/netbox/users/views.py b/netbox/users/views.py index 76e73f568..2e14374fd 100644 --- a/netbox/users/views.py +++ b/netbox/users/views.py @@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.utils.http import is_safe_url from django.views.generic import View from secrets.forms import UserKeyForm from secrets.models import UserKey -from .forms import LoginForm, PasswordChangeForm +from utilities.forms import ConfirmationForm +from .forms import LoginForm, PasswordChangeForm, TokenForm from .models import Token @@ -136,7 +137,7 @@ def recent_activity(request): # API tokens # -class TokenList(LoginRequiredMixin, View): +class TokenListView(LoginRequiredMixin, View): def get(self, request): @@ -146,3 +147,74 @@ class TokenList(LoginRequiredMixin, View): 'tokens': tokens, 'active_tab': 'api_tokens', }) + + +class TokenEditView(LoginRequiredMixin, View): + + def get(self, request, pk=None): + + if pk is not None: + token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) + else: + token = Token(user=request.user) + + form = TokenForm(instance=token) + + return render(request, 'utilities/obj_edit.html', { + 'obj': token, + 'obj_type': token._meta.verbose_name, + 'form': form, + 'return_url': reverse('users:token_list'), + }) + + def post(self, request, pk=None): + + if pk is not None: + token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) + form = TokenForm(request.POST, instance=token) + else: + form = TokenForm(request.POST) + + if form.is_valid(): + token = form.save(commit=False) + token.user = request.user + token.save() + + msg = "Token updated" if pk else "New token created" + messages.success(request, msg) + + return redirect('users:token_list') + + +class TokenDeleteView(LoginRequiredMixin, View): + + def get(self, request, pk): + + token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) + initial_data = { + 'return_url': reverse('users:token_list'), + } + form = ConfirmationForm(initial=initial_data) + + return render(request, 'utilities/obj_delete.html', { + 'obj': token, + 'obj_type': token._meta.verbose_name, + 'form': form, + 'return_url': reverse('users:token_list'), + }) + + def post(self, request, pk): + + token = get_object_or_404(Token.objects.filter(user=request.user), pk=pk) + form = ConfirmationForm(request.POST) + if form.is_valid(): + token.delete() + messages.success(request, "Token deleted") + return redirect('users:token_list') + + return render(request, 'utilities/obj_delete.html', { + 'obj': token, + 'obj_type': token._meta.verbose_name, + 'form': form, + 'return_url': reverse('users:token_list'), + })