Skip to content

Commit 7600d7b

Browse files
authored
Closes #13228: Move token management views to primary UI
1 parent 149a496 commit 7600d7b

21 files changed

+482
-167
lines changed

netbox/netbox/navigation/menu.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@
353353
icon_class='mdi mdi-account-multiple',
354354
groups=(
355355
MenuGroup(
356-
label=_('Users'),
356+
label=_('Authentication'),
357357
items=(
358358
# Proxy model for auth.User
359359
MenuItem(
@@ -399,6 +399,7 @@
399399
)
400400
)
401401
),
402+
get_model_item('users', 'token', _('API Tokens')),
402403
get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']),
403404
),
404405
),

netbox/netbox/settings.py

+1
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ def _setting(name, default=None):
469469
('auth', 'group'),
470470
('auth', 'user'),
471471
('users', 'objectpermission'),
472+
('users', 'token'),
472473
)
473474

474475
# All URLs starting with a string listed here are exempt from login enforcement

netbox/templates/inc/profile_button.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
</a>
3535
</li>
3636
<li>
37-
<a class="dropdown-item" href="{% url 'users:token_list' %}">
37+
<a class="dropdown-item" href="{% url 'users:usertoken_list' %}">
3838
<i class="mdi mdi-key"></i> API Tokens
3939
</a>
4040
</li>

netbox/templates/users/account/api_token.html

-58
This file was deleted.

netbox/templates/users/account/base.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</li>
1919
{% endif %}
2020
<li role="presentation" class="nav-item">
21-
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:token_list' %}">{% trans "API Tokens" %}</a>
21+
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:usertoken_list' %}">{% trans "API Tokens" %}</a>
2222
</li>
2323
</ul>
2424
{% endblock %}
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{% extends 'generic/object.html' %}
2+
{% load form_helpers %}
3+
{% load helpers %}
4+
{% load i18n %}
5+
{% load plugins %}
6+
7+
{% block breadcrumbs %}
8+
<li class="breadcrumb-item"><a href="{% url 'users:usertoken_list' %}">{% trans "My API Tokens" %}</a></li>
9+
{% endblock breadcrumbs %}
10+
11+
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
12+
13+
{% block subtitle %}{% endblock %}
14+
15+
{% block content %}
16+
<div class="row">
17+
<div class="col col-md-12">
18+
{% if key and not settings.ALLOW_TOKEN_RETRIEVAL %}
19+
<div class="alert alert-danger" role="alert">
20+
<i class="mdi mdi-alert"></i> Tokens cannot be retrieved at a later time. You must <a href="#" class="copy-content" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
21+
</div>
22+
{% endif %}
23+
<div class="card">
24+
<h5 class="card-header">{% trans "Token" %}</h5>
25+
<div class="card-body">
26+
<table class="table table-hover attr-table">
27+
<tr>
28+
<th scope="row">{% trans "Key" %}</th>
29+
<td>
30+
{% if key %}
31+
<div class="float-end">
32+
{% copy_content "token_id" %}
33+
</div>
34+
<div id="token_id">{{ key }}</div>
35+
{% else %}
36+
{{ object.partial }}
37+
{% endif %}
38+
</td>
39+
</tr>
40+
<tr>
41+
<th scope="row">{% trans "Description" %}</th>
42+
<td>{{ object.description|placeholder }}</td>
43+
</tr>
44+
<tr>
45+
<th scope="row">{% trans "Write enabled" %}</th>
46+
<td>{% checkmark object.write_enabled %}</td>
47+
</tr>
48+
<tr>
49+
<th scope="row">{% trans "Created" %}</th>
50+
<td>{{ object.created|annotated_date }}</td>
51+
</tr>
52+
<tr>
53+
<th scope="row">{% trans "Expires" %}</th>
54+
<td>{{ object.expires|placeholder }}</td>
55+
</tr>
56+
<tr>
57+
<th scope="row">{% trans "Last used" %}</th>
58+
<td>{{ object.last_used|placeholder }}</td>
59+
</tr>
60+
<tr>
61+
<th scope="row">{% trans "Allowed IPs" %}</th>
62+
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
63+
</tr>
64+
</table>
65+
</div>
66+
</div>
67+
</div>
68+
</div>
69+
{% endblock %}

netbox/templates/users/account/api_tokens.html renamed to netbox/templates/users/account/token_list.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
{% load helpers %}
33
{% load render_table from django_tables2 %}
44

5-
{% block title %}API Tokens{% endblock %}
5+
{% block title %}My API Tokens{% endblock %}
66

77
{% block content %}
88
<div class="row">
99
<div class="col col-md-12 text-end">
10-
<a href="{% url 'users:token_add' %}" class="btn btn-sm btn-primary my-3">
10+
<a href="{% url 'users:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
1111
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a Token
1212
</a>
1313
</div>

netbox/templates/users/token.html

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{% extends 'generic/object.html' %}
2+
{% load i18n %}
3+
{% load helpers %}
4+
{% load render_table from django_tables2 %}
5+
6+
{% block title %}{% trans "Token" %} {{ object }}{% endblock %}
7+
8+
{% block subtitle %}{% endblock %}
9+
10+
{% block content %}
11+
<div class="row mb-3">
12+
<div class="col-md-6">
13+
<div class="card">
14+
<h5 class="card-header">{% trans "Token" %}</h5>
15+
<div class="card-body">
16+
<table class="table table-hover attr-table">
17+
<tr>
18+
<th scope="row">{% trans "Key" %}</th>
19+
<td>{% if settings.ALLOW_TOKEN_RETRIEVAL %}{{ object.key }}{% else %}{{ object.partial }}{% endif %}</td>
20+
</tr>
21+
<tr>
22+
<th scope="row">{% trans "User" %}</th>
23+
<td>
24+
<a href="{% url 'users:netboxuser' pk=object.user.pk %}">{{ object.user }}</a>
25+
</td>
26+
</tr>
27+
<tr>
28+
<th scope="row">{% trans "Description" %}</th>
29+
<td>{{ object.description|placeholder }}</td>
30+
</tr>
31+
<tr>
32+
<th scope="row">{% trans "Write enabled" %}</th>
33+
<td>{% checkmark object.write_enabled %}</td>
34+
</tr>
35+
<tr>
36+
<th scope="row">{% trans "Created" %}</th>
37+
<td>{{ object.created|annotated_date }}</td>
38+
</tr>
39+
<tr>
40+
<th scope="row">{% trans "Expires" %}</th>
41+
<td>{{ object.expires|placeholder }}</td>
42+
</tr>
43+
<tr>
44+
<th scope="row">{% trans "Last used" %}</th>
45+
<td>{{ object.last_used|placeholder }}</td>
46+
</tr>
47+
<tr>
48+
<th scope="row">{% trans "Allowed IPs" %}</th>
49+
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
50+
</tr>
51+
</table>
52+
</div>
53+
</div>
54+
</div>
55+
</div>
56+
{% endblock %}

netbox/users/admin/__init__.py

-21
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,10 @@
11
from django.contrib import admin
2-
from django.contrib.auth.admin import UserAdmin as UserAdmin_
32
from django.contrib.auth.models import Group, User
43

5-
from users.models import ObjectPermission, Token
6-
from . import filters, forms, inlines
7-
8-
94
#
105
# Users & groups
116
#
127

138
# Unregister the built-in GroupAdmin and UserAdmin classes so that we can use our custom admin classes below
149
admin.site.unregister(Group)
1510
admin.site.unregister(User)
16-
17-
18-
#
19-
# REST API tokens
20-
#
21-
22-
@admin.register(Token)
23-
class TokenAdmin(admin.ModelAdmin):
24-
form = forms.TokenAdminForm
25-
list_display = [
26-
'key', 'user', 'created', 'expires', 'last_used', 'write_enabled', 'description', 'list_allowed_ips'
27-
]
28-
29-
def list_allowed_ips(self, obj):
30-
return obj.allowed_ips or 'Any'
31-
list_allowed_ips.short_description = "Allowed IPs"

netbox/users/admin/forms.py

-21
This file was deleted.

netbox/users/filtersets.py

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
__all__ = (
1111
'GroupFilterSet',
1212
'ObjectPermissionFilterSet',
13+
'TokenFilterSet',
1314
'UserFilterSet',
1415
)
1516

netbox/users/forms/bulk_edit.py

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
from django import forms
2+
from django.contrib.postgres.forms import SimpleArrayField
23
from django.utils.translation import gettext_lazy as _
34

5+
from ipam.formfields import IPNetworkFormField
6+
from ipam.validators import prefix_validator
47
from users.models import *
5-
from utilities.forms import BootstrapMixin
6-
from utilities.forms.widgets import BulkEditNullBooleanSelect
8+
from utilities.forms import BootstrapMixin, BulkEditForm
9+
from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker
710

811
__all__ = (
912
'ObjectPermissionBulkEditForm',
1013
'UserBulkEditForm',
14+
'TokenBulkEditForm',
1115
)
1216

1317

@@ -70,3 +74,38 @@ class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form):
7074
(None, ('enabled', 'description')),
7175
)
7276
nullable_fields = ('description',)
77+
78+
79+
class TokenBulkEditForm(BulkEditForm):
80+
pk = forms.ModelMultipleChoiceField(
81+
queryset=Token.objects.all(),
82+
widget=forms.MultipleHiddenInput
83+
)
84+
write_enabled = forms.NullBooleanField(
85+
required=False,
86+
widget=BulkEditNullBooleanSelect,
87+
label=_('Write enabled')
88+
)
89+
description = forms.CharField(
90+
max_length=200,
91+
required=False,
92+
label=_('Description')
93+
)
94+
expires = forms.DateTimeField(
95+
required=False,
96+
widget=DateTimePicker(),
97+
label=_('Expires')
98+
)
99+
allowed_ips = SimpleArrayField(
100+
base_field=IPNetworkFormField(validators=[prefix_validator]),
101+
required=False,
102+
label=_('Allowed IPs')
103+
)
104+
105+
model = Token
106+
fieldsets = (
107+
(None, ('write_enabled', 'description', 'expires', 'allowed_ips')),
108+
)
109+
nullable_fields = (
110+
'expires', 'description', 'allowed_ips',
111+
)

netbox/users/forms/bulk_import.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from users.models import NetBoxGroup, NetBoxUser
1+
from django import forms
2+
from django.utils.translation import gettext as _
3+
from users.models import *
24
from utilities.forms import CSVModelForm
35

6+
47
__all__ = (
58
'GroupImportForm',
69
'UserImportForm',
10+
'TokenImportForm',
711
)
812

913

@@ -30,3 +34,15 @@ def save(self, *args, **kwargs):
3034
self.instance.set_password(self.cleaned_data.get('password'))
3135

3236
return super().save(*args, **kwargs)
37+
38+
39+
class TokenImportForm(CSVModelForm):
40+
key = forms.CharField(
41+
label=_('Key'),
42+
required=False,
43+
help_text=_("If no key is provided, one will be generated automatically.")
44+
)
45+
46+
class Meta:
47+
model = Token
48+
fields = ('user', 'key', 'write_enabled', 'expires', 'description',)

0 commit comments

Comments
 (0)