mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
#4969: Remove user and group assignment from SecretRole
This commit is contained in:
parent
aca3ca9d65
commit
e6bc55af85
@ -7,5 +7,3 @@ Each secret is assigned a functional role which indicates what it is used for. S
|
|||||||
* RADIUS/TACACS+ keys
|
* RADIUS/TACACS+ keys
|
||||||
* IKE key strings
|
* IKE key strings
|
||||||
* Routing protocol shared secrets
|
* Routing protocol shared secrets
|
||||||
|
|
||||||
Roles are also used to control access to secrets. Each role is assigned an arbitrary number of groups and/or users. Only the users associated with a role have permission to decrypt the secrets assigned to that role. (A superuser has permission to decrypt all secrets, provided they have an active user key.)
|
|
||||||
|
@ -38,7 +38,7 @@ class SecretRoleViewSet(ModelViewSet):
|
|||||||
|
|
||||||
class SecretViewSet(ModelViewSet):
|
class SecretViewSet(ModelViewSet):
|
||||||
queryset = Secret.objects.prefetch_related(
|
queryset = Secret.objects.prefetch_related(
|
||||||
'device__primary_ip4', 'device__primary_ip6', 'role', 'role__users', 'role__groups', 'tags',
|
'device__primary_ip4', 'device__primary_ip6', 'role', 'tags',
|
||||||
)
|
)
|
||||||
serializer_class = serializers.SecretSerializer
|
serializer_class = serializers.SecretSerializer
|
||||||
filterset_class = filters.SecretFilterSet
|
filterset_class = filters.SecretFilterSet
|
||||||
@ -84,8 +84,8 @@ class SecretViewSet(ModelViewSet):
|
|||||||
|
|
||||||
secret = self.get_object()
|
secret = self.get_object()
|
||||||
|
|
||||||
# Attempt to decrypt the secret if the user is permitted and the master key is known
|
# Attempt to decrypt the secret if the master key is known
|
||||||
if secret.decryptable_by(request.user) and self.master_key is not None:
|
if self.master_key is not None:
|
||||||
secret.decrypt(self.master_key)
|
secret.decrypt(self.master_key)
|
||||||
|
|
||||||
serializer = self.get_serializer(secret)
|
serializer = self.get_serializer(secret)
|
||||||
@ -102,8 +102,6 @@ class SecretViewSet(ModelViewSet):
|
|||||||
if self.master_key is not None:
|
if self.master_key is not None:
|
||||||
secrets = []
|
secrets = []
|
||||||
for secret in page:
|
for secret in page:
|
||||||
# Enforce role permissions
|
|
||||||
if secret.decryptable_by(request.user):
|
|
||||||
secret.decrypt(self.master_key)
|
secret.decrypt(self.master_key)
|
||||||
secrets.append(secret)
|
secrets.append(secret)
|
||||||
serializer = self.get_serializer(secrets, many=True)
|
serializer = self.get_serializer(secrets, many=True)
|
||||||
|
@ -46,13 +46,7 @@ class SecretRoleForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = [
|
fields = ('name', 'slug', 'description')
|
||||||
'name', 'slug', 'description', 'users', 'groups',
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
'users': StaticSelect2Multiple(),
|
|
||||||
'groups': StaticSelect2Multiple(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class SecretRoleCSVForm(CSVModelForm):
|
class SecretRoleCSVForm(CSVModelForm):
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('secrets', '0008_standardize_description'),
|
||||||
|
('users', '0009_replicate_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='secretrole',
|
||||||
|
name='groups',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='secretrole',
|
||||||
|
name='users',
|
||||||
|
),
|
||||||
|
]
|
@ -239,9 +239,6 @@ class SecretRole(ChangeLoggedModel):
|
|||||||
"""
|
"""
|
||||||
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
|
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles
|
||||||
such as "Login Credentials" or "SNMP Communities."
|
such as "Login Credentials" or "SNMP Communities."
|
||||||
|
|
||||||
By default, only superusers will have access to decrypt Secrets. To allow other users to decrypt Secrets, grant them
|
|
||||||
access to the appropriate SecretRoles either individually or by group.
|
|
||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -254,16 +251,6 @@ class SecretRole(ChangeLoggedModel):
|
|||||||
max_length=200,
|
max_length=200,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
users = models.ManyToManyField(
|
|
||||||
to=User,
|
|
||||||
related_name='secretroles',
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
groups = models.ManyToManyField(
|
|
||||||
to=Group,
|
|
||||||
related_name='secretroles',
|
|
||||||
blank=True
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = RestrictedQuerySet.as_manager()
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
@ -285,14 +272,6 @@ class SecretRole(ChangeLoggedModel):
|
|||||||
self.description,
|
self.description,
|
||||||
)
|
)
|
||||||
|
|
||||||
def has_member(self, user):
|
|
||||||
"""
|
|
||||||
Check whether the given user has belongs to this SecretRole. Note that superusers belong to all roles.
|
|
||||||
"""
|
|
||||||
if user.is_superuser:
|
|
||||||
return True
|
|
||||||
return user in self.users.all() or user.groups.filter(pk__in=self.groups.all()).exists()
|
|
||||||
|
|
||||||
|
|
||||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||||
class Secret(ChangeLoggedModel, CustomFieldModel):
|
class Secret(ChangeLoggedModel, CustomFieldModel):
|
||||||
@ -453,9 +432,3 @@ class Secret(ChangeLoggedModel, CustomFieldModel):
|
|||||||
if not self.hash:
|
if not self.hash:
|
||||||
raise Exception("Hash has not been generated for this secret.")
|
raise Exception("Hash has not been generated for this secret.")
|
||||||
return check_password(plaintext, self.hash, preferred=SecretValidationHasher())
|
return check_password(plaintext, self.hash, preferred=SecretValidationHasher())
|
||||||
|
|
||||||
def decryptable_by(self, user):
|
|
||||||
"""
|
|
||||||
Check whether the given user has permission to decrypt this Secret.
|
|
||||||
"""
|
|
||||||
return self.role.has_member(user)
|
|
||||||
|
@ -18,7 +18,7 @@ class SecretRoleTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = SecretRole
|
model = SecretRole
|
||||||
fields = ('pk', 'name', 'secret_count', 'description', 'slug', 'users', 'groups', 'actions')
|
fields = ('pk', 'name', 'secret_count', 'description', 'slug', 'actions')
|
||||||
default_columns = ('pk', 'name', 'secret_count', 'description', 'actions')
|
default_columns = ('pk', 'name', 'secret_count', 'description', 'actions')
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
from django import template
|
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter()
|
|
||||||
def decryptable_by(secret, user):
|
|
||||||
"""
|
|
||||||
Determine whether a given User is permitted to decrypt a Secret.
|
|
||||||
"""
|
|
||||||
return secret.decryptable_by(user)
|
|
@ -25,8 +25,6 @@ class SecretRoleTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
|||||||
'name': 'Secret Role X',
|
'name': 'Secret Role X',
|
||||||
'slug': 'secret-role-x',
|
'slug': 'secret-role-x',
|
||||||
'description': 'A secret role',
|
'description': 'A secret role',
|
||||||
'users': [],
|
|
||||||
'groups': [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
{% load secret_helpers %}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
|
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret.role }}</a></td>
|
||||||
<td>{{ secret.name }}</td>
|
<td>{{ secret.name }}</td>
|
||||||
<td id="secret_{{ secret.pk }}">********</td>
|
<td id="secret_{{ secret.pk }}">********</td>
|
||||||
<td class="text-right noprint">
|
<td class="text-right noprint">
|
||||||
{% if secret|decryptable_by:request.user %}
|
|
||||||
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
<button class="btn btn-xs btn-success unlock-secret" secret-id="{{ secret.pk }}">
|
||||||
<i class="fa fa-lock"></i> Unlock
|
<i class="fa fa-lock"></i> Unlock
|
||||||
</button>
|
</button>
|
||||||
@ -14,10 +12,5 @@
|
|||||||
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
<button class="btn btn-xs btn-danger lock-secret collapse" secret-id="{{ secret.pk }}">
|
||||||
<i class="fa fa-unlock-alt"></i> Lock
|
<i class="fa fa-unlock-alt"></i> Lock
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
|
||||||
<button class="btn btn-xs btn-default" disabled="disabled" title="Permission denied">
|
|
||||||
<i class="fa fa-lock"></i> Unlock
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
{% load buttons %}
|
{% load buttons %}
|
||||||
{% load custom_links %}
|
{% load custom_links %}
|
||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
{% load secret_helpers %}
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
@ -70,7 +69,6 @@
|
|||||||
{% plugin_left_page secret %}
|
{% plugin_left_page secret %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
{% if secret|decryptable_by:request.user %}
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<strong>Secret Data</strong>
|
<strong>Secret Data</strong>
|
||||||
@ -96,12 +94,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<i class="fa fa-warning" aria-hidden="true"></i>
|
|
||||||
You do not have permission to decrypt this secret.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% include 'extras/inc/tags_panel.html' with tags=secret.tags.all url='secrets:secret_list' %}
|
{% include 'extras/inc/tags_panel.html' with tags=secret.tags.all url='secrets:secret_list' %}
|
||||||
{% plugin_right_page secret %}
|
{% plugin_right_page secret %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load form_helpers %}
|
{% load form_helpers %}
|
||||||
{% load secret_helpers %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form action="." method="post" class="form form-horizontal">
|
<form action="." method="post" class="form form-horizontal">
|
||||||
@ -30,7 +29,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading"><strong>Secret Data</strong></div>
|
<div class="panel-heading"><strong>Secret Data</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% if obj.pk and obj|decryptable_by:request.user %}
|
{% if obj.pk %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Current Plaintext</label>
|
<label class="col-md-3 control-label required">Current Plaintext</label>
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
ACTIONS = ['view', 'add', 'change', 'delete']
|
ACTIONS = ['view', 'add', 'change', 'delete']
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ def replicate_permissions(apps, schema_editor):
|
|||||||
"""
|
"""
|
||||||
Permission = apps.get_model('auth', 'Permission')
|
Permission = apps.get_model('auth', 'Permission')
|
||||||
ObjectPermission = apps.get_model('users', 'ObjectPermission')
|
ObjectPermission = apps.get_model('users', 'ObjectPermission')
|
||||||
|
SecretRole = apps.get_model('secrets', 'SecretRole')
|
||||||
|
|
||||||
# TODO: Optimize this iteration so that ObjectPermissions with identical sets of users and groups
|
# TODO: Optimize this iteration so that ObjectPermissions with identical sets of users and groups
|
||||||
# are combined into a single ObjectPermission instance.
|
# are combined into a single ObjectPermission instance.
|
||||||
@ -24,6 +25,27 @@ def replicate_permissions(apps, schema_editor):
|
|||||||
action = perm.codename
|
action = perm.codename
|
||||||
|
|
||||||
if perm.group_set.exists() or perm.user_set.exists():
|
if perm.group_set.exists() or perm.user_set.exists():
|
||||||
|
|
||||||
|
# Handle replication of SecretRole user/group assignments for Secrets
|
||||||
|
if perm.codename == 'view_secret':
|
||||||
|
for secretrole in SecretRole.objects.prefetch_related('users', 'groups'):
|
||||||
|
obj_perm = ObjectPermission(
|
||||||
|
name=f'{perm.content_type.app_label}.{perm.codename} ({secretrole.name})'[:100],
|
||||||
|
actions=[action],
|
||||||
|
constraints={'role__name': secretrole.name}
|
||||||
|
)
|
||||||
|
obj_perm.save()
|
||||||
|
obj_perm.object_types.add(perm.content_type)
|
||||||
|
# Assign only users/groups who both a) are assigned to the SecretRole and b) have the view_secret
|
||||||
|
# permission
|
||||||
|
obj_perm.groups.add(
|
||||||
|
*list(secretrole.groups.filter(permissions=perm))
|
||||||
|
)
|
||||||
|
obj_perm.users.add(*list(secretrole.users.filter(
|
||||||
|
Q(user_permissions=perm) | Q(groups__permissions=perm)
|
||||||
|
)))
|
||||||
|
|
||||||
|
else:
|
||||||
obj_perm = ObjectPermission(
|
obj_perm = ObjectPermission(
|
||||||
# Copy name from original Permission object
|
# Copy name from original Permission object
|
||||||
name=f'{perm.content_type.app_label}.{perm.codename}'[:100],
|
name=f'{perm.content_type.app_label}.{perm.codename}'[:100],
|
||||||
@ -31,6 +53,7 @@ def replicate_permissions(apps, schema_editor):
|
|||||||
)
|
)
|
||||||
obj_perm.save()
|
obj_perm.save()
|
||||||
obj_perm.object_types.add(perm.content_type)
|
obj_perm.object_types.add(perm.content_type)
|
||||||
|
|
||||||
if perm.group_set.exists():
|
if perm.group_set.exists():
|
||||||
obj_perm.groups.add(*list(perm.group_set.all()))
|
obj_perm.groups.add(*list(perm.group_set.all()))
|
||||||
if perm.user_set.exists():
|
if perm.user_set.exists():
|
||||||
|
Loading…
Reference in New Issue
Block a user