Skip to content

13044 token admin #13228

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion netbox/netbox/navigation/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@
icon_class='mdi mdi-account-multiple',
groups=(
MenuGroup(
label=_('Users'),
label=_('Authentication'),
items=(
# Proxy model for auth.User
MenuItem(
Expand Down Expand Up @@ -399,6 +399,7 @@
)
)
),
get_model_item('users', 'token', _('API Tokens')),
get_model_item('users', 'objectpermission', _('Permissions'), actions=['add']),
),
),
Expand Down
1 change: 1 addition & 0 deletions netbox/netbox/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ def _setting(name, default=None):
('auth', 'group'),
('auth', 'user'),
('users', 'objectpermission'),
('users', 'token'),
)

# All URLs starting with a string listed here are exempt from login enforcement
Expand Down
2 changes: 1 addition & 1 deletion netbox/templates/inc/profile_button.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
</a>
</li>
<li>
<a class="dropdown-item" href="{% url 'users:token_list' %}">
<a class="dropdown-item" href="{% url 'users:usertoken_list' %}">
<i class="mdi mdi-key"></i> API Tokens
</a>
</li>
Expand Down
58 changes: 0 additions & 58 deletions netbox/templates/users/account/api_token.html

This file was deleted.

2 changes: 1 addition & 1 deletion netbox/templates/users/account/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</li>
{% endif %}
<li role="presentation" class="nav-item">
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:token_list' %}">{% trans "API Tokens" %}</a>
<a class="nav-link{% if active_tab == 'api-tokens' %} active{% endif %}" href="{% url 'users:usertoken_list' %}">{% trans "API Tokens" %}</a>
</li>
</ul>
{% endblock %}
Expand Down
69 changes: 69 additions & 0 deletions netbox/templates/users/account/token.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{% extends 'generic/object.html' %}
{% load form_helpers %}
{% load helpers %}
{% load i18n %}
{% load plugins %}

{% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'users:usertoken_list' %}">{% trans "My API Tokens" %}</a></li>
{% endblock breadcrumbs %}

{% block title %}{% trans "Token" %} {{ object }}{% endblock %}

{% block subtitle %}{% endblock %}

{% block content %}
<div class="row">
<div class="col col-md-12">
{% if key and 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-content" data-clipboard-target="#token_id" title="Copy to clipboard">copy the token value</a> below and store it securely.
</div>
{% endif %}
<div class="card">
<h5 class="card-header">{% trans "Token" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Key" %}</th>
<td>
{% if key %}
<div class="float-end">
{% copy_content "token_id" %}
</div>
<div id="token_id">{{ key }}</div>
{% else %}
{{ object.partial }}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Write enabled" %}</th>
<td>{% checkmark object.write_enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|annotated_date }}</td>
</tr>
<tr>
<th scope="row">{% trans "Expires" %}</th>
<td>{{ object.expires|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last used" %}</th>
<td>{{ object.last_used|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Allowed IPs" %}</th>
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
{% load helpers %}
{% load render_table from django_tables2 %}

{% block title %}API Tokens{% endblock %}
{% block title %}My API Tokens{% endblock %}

{% block content %}
<div class="row">
<div class="col col-md-12 text-end">
<a href="{% url 'users:token_add' %}" class="btn btn-sm btn-primary my-3">
<a href="{% url 'users:usertoken_add' %}" class="btn btn-sm btn-primary my-3">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add a Token
</a>
</div>
Expand Down
56 changes: 56 additions & 0 deletions netbox/templates/users/token.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{% extends 'generic/object.html' %}
{% load i18n %}
{% load helpers %}
{% load render_table from django_tables2 %}

{% block title %}{% trans "Token" %} {{ object }}{% endblock %}

{% block subtitle %}{% endblock %}

{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<div class="card">
<h5 class="card-header">{% trans "Token" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Key" %}</th>
<td>{% if settings.ALLOW_TOKEN_RETRIEVAL %}{{ object.key }}{% else %}{{ object.partial }}{% endif %}</td>
</tr>
<tr>
<th scope="row">{% trans "User" %}</th>
<td>
<a href="{% url 'users:netboxuser' pk=object.user.pk %}">{{ object.user }}</a>
</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Write enabled" %}</th>
<td>{% checkmark object.write_enabled %}</td>
</tr>
<tr>
<th scope="row">{% trans "Created" %}</th>
<td>{{ object.created|annotated_date }}</td>
</tr>
<tr>
<th scope="row">{% trans "Expires" %}</th>
<td>{{ object.expires|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Last used" %}</th>
<td>{{ object.last_used|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Allowed IPs" %}</th>
<td>{{ object.allowed_ips|join:", "|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
</div>
{% endblock %}
21 changes: 0 additions & 21 deletions netbox/users/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,10 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as UserAdmin_
from django.contrib.auth.models import Group, User

from users.models import ObjectPermission, Token
from . import filters, forms, inlines


#
# Users & groups
#

# Unregister the built-in GroupAdmin and UserAdmin classes so that we can use our custom admin classes below
admin.site.unregister(Group)
admin.site.unregister(User)


#
# REST API tokens
#

@admin.register(Token)
class TokenAdmin(admin.ModelAdmin):
form = forms.TokenAdminForm
list_display = [
'key', 'user', 'created', 'expires', 'last_used', 'write_enabled', 'description', 'list_allowed_ips'
]

def list_allowed_ips(self, obj):
return obj.allowed_ips or 'Any'
list_allowed_ips.short_description = "Allowed IPs"
21 changes: 0 additions & 21 deletions netbox/users/admin/forms.py

This file was deleted.

1 change: 1 addition & 0 deletions netbox/users/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
__all__ = (
'GroupFilterSet',
'ObjectPermissionFilterSet',
'TokenFilterSet',
'UserFilterSet',
)

Expand Down
43 changes: 41 additions & 2 deletions netbox/users/forms/bulk_edit.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from django import forms
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.translation import gettext_lazy as _

from ipam.formfields import IPNetworkFormField
from ipam.validators import prefix_validator
from users.models import *
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import BulkEditNullBooleanSelect
from utilities.forms import BootstrapMixin, BulkEditForm
from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker

__all__ = (
'ObjectPermissionBulkEditForm',
'UserBulkEditForm',
'TokenBulkEditForm',
)


Expand Down Expand Up @@ -70,3 +74,38 @@ class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form):
(None, ('enabled', 'description')),
)
nullable_fields = ('description',)


class TokenBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Token.objects.all(),
widget=forms.MultipleHiddenInput
)
write_enabled = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect,
label=_('Write enabled')
)
description = forms.CharField(
max_length=200,
required=False,
label=_('Description')
)
expires = forms.DateTimeField(
required=False,
widget=DateTimePicker(),
label=_('Expires')
)
allowed_ips = SimpleArrayField(
base_field=IPNetworkFormField(validators=[prefix_validator]),
required=False,
label=_('Allowed IPs')
)

model = Token
fieldsets = (
(None, ('write_enabled', 'description', 'expires', 'allowed_ips')),
)
nullable_fields = (
'expires', 'description', 'allowed_ips',
)
18 changes: 17 additions & 1 deletion netbox/users/forms/bulk_import.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from users.models import NetBoxGroup, NetBoxUser
from django import forms
from django.utils.translation import gettext as _
from users.models import *
from utilities.forms import CSVModelForm


__all__ = (
'GroupImportForm',
'UserImportForm',
'TokenImportForm',
)


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

return super().save(*args, **kwargs)


class TokenImportForm(CSVModelForm):
key = forms.CharField(
label=_('Key'),
required=False,
help_text=_("If no key is provided, one will be generated automatically.")
)

class Meta:
model = Token
fields = ('user', 'key', 'write_enabled', 'expires', 'description',)
Loading