mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
* Introduce the DEFAULT_PERMISSIONS config parameter * Establish default permissions for user token management
This commit is contained in:
@ -68,8 +68,13 @@ When defining a permission constraint, administrators may use the special token
|
||||
|
||||
The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes.
|
||||
|
||||
### Default Permissions
|
||||
|
||||
#### Example Constraint Definitions
|
||||
!!! info "This feature was introduced in NetBox v3.6."
|
||||
|
||||
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to _all_ authenticated users. This is done using the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter. Note that statically configuring permissions for specific users or groups is **not** supported.
|
||||
|
||||
### Example Constraint Definitions
|
||||
|
||||
| Constraints | Description |
|
||||
| ----------- | ----------- |
|
||||
|
@ -90,6 +90,38 @@ CSRF_TRUSTED_ORIGINS = (
|
||||
|
||||
---
|
||||
|
||||
## DEFAULT_PERMISSIONS
|
||||
|
||||
!!! info "This parameter was introduced in NetBox v3.6."
|
||||
|
||||
Default:
|
||||
|
||||
```python
|
||||
{
|
||||
'users.view_token': ({'user': '$user'},),
|
||||
'users.add_token': ({'user': '$user'},),
|
||||
'users.change_token': ({'user': '$user'},),
|
||||
'users.delete_token': ({'user': '$user'},),
|
||||
}
|
||||
```
|
||||
|
||||
This parameter defines object permissions that are applied automatically to _any_ authenticated user, regardless of what permissions have been defined in the database. By default, this parameter is defined to allow all users to manage their own API tokens, however it can be overriden for any purpose.
|
||||
|
||||
For example, to allow all users to create a device role beginning with the word "temp," you could configure the following:
|
||||
|
||||
```python
|
||||
DEFAULT_PERMISSIONS = {
|
||||
'dcim.add_devicerole': (
|
||||
{'name__startswith': 'temp'},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
!!! warning
|
||||
Setting a custom value for this parameter will overwrite the default permission mapping shown above. If you want to retain the default mapping, be sure to reproduce it in your custom configuration.
|
||||
|
||||
---
|
||||
|
||||
## EXEMPT_VIEW_PERMISSIONS
|
||||
|
||||
Default: Empty list
|
||||
|
@ -76,6 +76,18 @@ class ObjectPermissionMixin:
|
||||
"""
|
||||
Return all permissions granted to the user by an ObjectPermission.
|
||||
"""
|
||||
# Initialize a dictionary mapping permission names to sets of constraints
|
||||
perms = defaultdict(list)
|
||||
|
||||
# Collect any configured default permissions
|
||||
for perm_name, constraints in settings.DEFAULT_PERMISSIONS.items():
|
||||
constraints = constraints or tuple()
|
||||
if type(constraints) not in (list, tuple):
|
||||
raise ImproperlyConfigured(
|
||||
f"Constraints for default permission {perm_name} must be defined as a list or tuple."
|
||||
)
|
||||
perms[perm_name].extend(constraints)
|
||||
|
||||
# Retrieve all assigned and enabled ObjectPermissions
|
||||
object_permissions = ObjectPermission.objects.filter(
|
||||
self.get_permission_filter(user_obj),
|
||||
@ -83,7 +95,6 @@ class ObjectPermissionMixin:
|
||||
).order_by('id').distinct('id').prefetch_related('object_types')
|
||||
|
||||
# Create a dictionary mapping permissions to their constraints
|
||||
perms = defaultdict(list)
|
||||
for obj_perm in object_permissions:
|
||||
for object_type in obj_perm.object_types.all():
|
||||
for action in obj_perm.actions:
|
||||
|
@ -39,6 +39,8 @@ REDIS = {
|
||||
|
||||
SECRET_KEY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
|
||||
DEFAULT_PERMISSIONS = {}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True
|
||||
|
@ -99,6 +99,13 @@ DATE_FORMAT = getattr(configuration, 'DATE_FORMAT', 'N j, Y')
|
||||
DATETIME_FORMAT = getattr(configuration, 'DATETIME_FORMAT', 'N j, Y g:i a')
|
||||
DEBUG = getattr(configuration, 'DEBUG', False)
|
||||
DEFAULT_DASHBOARD = getattr(configuration, 'DEFAULT_DASHBOARD', None)
|
||||
DEFAULT_PERMISSIONS = getattr(configuration, 'DEFAULT_PERMISSIONS', {
|
||||
# Permit users to manage their own API tokens
|
||||
'users.view_token': ({'user': '$user'},),
|
||||
'users.add_token': ({'user': '$user'},),
|
||||
'users.change_token': ({'user': '$user'},),
|
||||
'users.delete_token': ({'user': '$user'},),
|
||||
})
|
||||
DEVELOPER = getattr(configuration, 'DEVELOPER', False)
|
||||
DOCS_ROOT = getattr(configuration, 'DOCS_ROOT', os.path.join(os.path.dirname(BASE_DIR), 'docs'))
|
||||
EMAIL = getattr(configuration, 'EMAIL', {})
|
||||
|
@ -1,6 +1,5 @@
|
||||
from django.urls import include, path
|
||||
from django.urls import path
|
||||
|
||||
from utilities.urls import get_model_urls
|
||||
from . import views
|
||||
|
||||
app_name = 'account'
|
||||
@ -13,6 +12,8 @@ urlpatterns = [
|
||||
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
|
||||
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
|
||||
path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'),
|
||||
path('api-tokens/<int:pk>/', include(get_model_urls('users', 'usertoken'))),
|
||||
path('api-tokens/<int:pk>/', views.UserTokenView.as_view(), name='usertoken'),
|
||||
path('api-tokens/<int:pk>/edit/', views.UserTokenEditView.as_view(), name='usertoken_edit'),
|
||||
path('api-tokens/<int:pk>/delete/', views.UserTokenDeleteView.as_view(), name='usertoken_delete'),
|
||||
|
||||
]
|
||||
|
@ -49,21 +49,10 @@ class GroupViewSet(NetBoxModelViewSet):
|
||||
#
|
||||
|
||||
class TokenViewSet(NetBoxModelViewSet):
|
||||
queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
|
||||
queryset = Token.objects.prefetch_related('user')
|
||||
serializer_class = serializers.TokenSerializer
|
||||
filterset_class = filtersets.TokenFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Limit the non-superusers to their own Tokens.
|
||||
"""
|
||||
queryset = super().get_queryset()
|
||||
if not self.request.user.is_authenticated:
|
||||
return queryset.none()
|
||||
if self.request.user.is_superuser:
|
||||
return queryset
|
||||
return queryset.filter(user=self.request.user)
|
||||
|
||||
|
||||
class TokenProvisionView(APIView):
|
||||
"""
|
||||
|
@ -23,14 +23,6 @@ COPY_BUTTON = """
|
||||
"""
|
||||
|
||||
|
||||
class TokenActionsColumn(columns.ActionsColumn):
|
||||
# Subclass ActionsColumn to disregard permissions for edit & delete buttons
|
||||
actions = {
|
||||
'edit': columns.ActionsItem('Edit', 'pencil', None, 'warning'),
|
||||
'delete': columns.ActionsItem('Delete', 'trash-can-outline', None, 'danger'),
|
||||
}
|
||||
|
||||
|
||||
class UserTokenTable(NetBoxTable):
|
||||
"""
|
||||
Table for users to manager their own API tokens under account views.
|
||||
@ -55,7 +47,8 @@ class UserTokenTable(NetBoxTable):
|
||||
verbose_name=_('Allowed IPs'),
|
||||
template_code=ALLOWED_IPS
|
||||
)
|
||||
actions = TokenActionsColumn(
|
||||
# TODO: Fix permissions evaluation & viewname resolution
|
||||
actions = columns.ActionsColumn(
|
||||
actions=('edit', 'delete'),
|
||||
extra_buttons=COPY_BUTTON
|
||||
)
|
||||
|
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
from users.models import ObjectPermission, Token
|
||||
from utilities.testing import APIViewTestCases, APITestCase
|
||||
from utilities.testing import APIViewTestCases, APITestCase, create_test_user
|
||||
from utilities.utils import deepmerge
|
||||
|
||||
|
||||
@ -105,24 +105,35 @@ class TokenTest(
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Apply grant_token permission to enable the creation of Tokens for other Users
|
||||
self.add_permissions('users.grant_token')
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
users = (
|
||||
create_test_user('User 1'),
|
||||
create_test_user('User 2'),
|
||||
create_test_user('User 3'),
|
||||
)
|
||||
|
||||
tokens = (
|
||||
# We already start with one Token, created by the test class
|
||||
Token(user=self.user),
|
||||
Token(user=self.user),
|
||||
Token(user=users[0]),
|
||||
Token(user=users[1]),
|
||||
Token(user=users[2]),
|
||||
)
|
||||
# Use save() instead of bulk_create() to ensure keys get automatically generated
|
||||
for token in tokens:
|
||||
token.save()
|
||||
|
||||
self.create_data = [
|
||||
cls.create_data = [
|
||||
{
|
||||
'user': self.user.pk,
|
||||
'user': users[0].pk,
|
||||
},
|
||||
{
|
||||
'user': self.user.pk,
|
||||
'user': users[1].pk,
|
||||
},
|
||||
{
|
||||
'user': self.user.pk,
|
||||
'user': users[2].pk,
|
||||
},
|
||||
]
|
||||
|
||||
@ -161,6 +172,9 @@ class TokenTest(
|
||||
"""
|
||||
Test provisioning a Token for a different User with & without the grant_token permission.
|
||||
"""
|
||||
# Clear grant_token permission assigned by setUpTestData
|
||||
ObjectPermission.objects.filter(users=self.user).delete()
|
||||
|
||||
self.add_permissions('users.add_token')
|
||||
user2 = User.objects.create_user(username='testuser2')
|
||||
data = {
|
||||
|
@ -279,83 +279,20 @@ class UserTokenView(LoginRequiredMixin, View):
|
||||
})
|
||||
|
||||
|
||||
@register_model_view(UserToken, 'edit')
|
||||
class UserTokenEditView(LoginRequiredMixin, View):
|
||||
class UserTokenEditView(generic.ObjectEditView):
|
||||
queryset = UserToken.objects.all()
|
||||
form = forms.UserTokenForm
|
||||
default_return_url = 'account:usertoken_list'
|
||||
|
||||
def get(self, request, pk=None):
|
||||
if pk:
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
else:
|
||||
token = UserToken(user=request.user)
|
||||
form = forms.UserTokenForm(instance=token)
|
||||
|
||||
return render(request, 'generic/object_edit.html', {
|
||||
'object': token,
|
||||
'form': form,
|
||||
'return_url': reverse('account:usertoken_list'),
|
||||
})
|
||||
|
||||
def post(self, request, pk=None):
|
||||
if pk:
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
form = forms.UserTokenForm(request.POST, instance=token)
|
||||
else:
|
||||
token = UserToken(user=request.user)
|
||||
form = forms.UserTokenForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
token = form.save(commit=False)
|
||||
token.user = request.user
|
||||
token.save()
|
||||
|
||||
msg = f"Modified token {token}" if pk else f"Created token {token}"
|
||||
messages.success(request, msg)
|
||||
|
||||
if not pk and not settings.ALLOW_TOKEN_RETRIEVAL:
|
||||
return render(request, 'users/account/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('account:usertoken_list')
|
||||
|
||||
return render(request, 'generic/object_edit.html', {
|
||||
'object': token,
|
||||
'form': form,
|
||||
'return_url': reverse('account:usertoken_list'),
|
||||
'disable_addanother': not settings.ALLOW_TOKEN_RETRIEVAL
|
||||
})
|
||||
def alter_object(self, obj, request, url_args, url_kwargs):
|
||||
if not obj.pk:
|
||||
obj.user = request.user
|
||||
return obj
|
||||
|
||||
|
||||
@register_model_view(UserToken, 'delete')
|
||||
class UserTokenDeleteView(LoginRequiredMixin, View):
|
||||
|
||||
def get(self, request, pk):
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
|
||||
return render(request, 'generic/object_delete.html', {
|
||||
'object': token,
|
||||
'form': ConfirmationForm(),
|
||||
'return_url': reverse('account:usertoken_list'),
|
||||
})
|
||||
|
||||
def post(self, request, pk):
|
||||
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
|
||||
form = ConfirmationForm(request.POST)
|
||||
|
||||
if form.is_valid():
|
||||
token.delete()
|
||||
messages.success(request, "Token deleted")
|
||||
return redirect('account:usertoken_list')
|
||||
|
||||
return render(request, 'generic/object_delete.html', {
|
||||
'object': token,
|
||||
'form': form,
|
||||
'return_url': reverse('account:usertoken_list'),
|
||||
})
|
||||
class UserTokenDeleteView(generic.ObjectDeleteView):
|
||||
queryset = UserToken.objects.all()
|
||||
default_return_url = 'account:usertoken_list'
|
||||
|
||||
|
||||
#
|
||||
|
Reference in New Issue
Block a user