Skip to content

Commit 6adafe3

Browse files
authored
[fix] Fixed validation in change device group admin action #762
Closes #762
1 parent 2d21f42 commit 6adafe3

File tree

2 files changed

+92
-18
lines changed

2 files changed

+92
-18
lines changed

openwisp_controller/config/admin.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ValidationError,
1515
)
1616
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
17+
from django.http.response import HttpResponseForbidden
1718
from django.shortcuts import get_object_or_404
1819
from django.template.loader import get_template
1920
from django.template.response import TemplateResponse
@@ -438,11 +439,20 @@ def get_fields(self, request, obj):
438439

439440
class ChangeDeviceGroupForm(forms.Form):
440441
device_group = forms.ModelChoiceField(
441-
queryset=DeviceGroup.objects.all(),
442+
# The queryset is set in the __init__ method
443+
# after filtering the groups according the
444+
# device's organization
445+
queryset=DeviceGroup.objects.none(),
442446
label=_('Group'),
443447
required=False,
444448
)
445449

450+
def __init__(self, org_id, **kwargs):
451+
super().__init__(**kwargs)
452+
self.fields['device_group'].queryset = DeviceGroup.objects.filter(
453+
organization_id=org_id
454+
)
455+
446456

447457
class DeviceAdmin(MultitenantAdminMixin, BaseConfigAdmin, UUIDAdmin):
448458
recover_form_template = 'admin/config/device_recover_form.html'
@@ -559,28 +569,49 @@ def construct_change_message(self, request, form, formsets, add=False):
559569
return super().construct_change_message(request, form, formsets, add)
560570

561571
def change_group(self, request, queryset):
572+
# Validate all selected devices belong to the same organization
573+
# which is managed by the user.
574+
org_id = None
575+
if queryset:
576+
org_id = queryset[0].organization_id
577+
if (
578+
not request.user.is_superuser
579+
and str(org_id) not in request.user.organizations_managed
580+
):
581+
logger.warning(f'{request.user} does not manage "{org_id}" organization.')
582+
return HttpResponseForbidden()
583+
if len(queryset) != queryset.filter(organization_id=org_id).count():
584+
self.message_user(
585+
request,
586+
_('Select devices from one organization'),
587+
messages.ERROR,
588+
)
589+
return HttpResponseRedirect(request.get_full_path())
590+
562591
if 'apply' in request.POST:
563-
form = ChangeDeviceGroupForm(request.POST)
592+
form = ChangeDeviceGroupForm(data=request.POST, org_id=org_id)
564593
if form.is_valid():
565594
group = form.cleaned_data['device_group']
566-
instances, old_group_ids = map(
567-
list, zip(*queryset.values_list('id', 'group'))
568-
)
595+
# Evaluate queryset to store old group id
596+
old_group_qs = list(queryset)
569597
queryset.update(group=group or None)
570598
group_id = None
571599
if group:
572600
group_id = group.id
573-
Device._send_device_group_changed_signal(
574-
instance=instances, group_id=group_id, old_group_id=old_group_ids
575-
)
601+
for device in old_group_qs:
602+
Device._send_device_group_changed_signal(
603+
instance=device,
604+
group_id=group_id,
605+
old_group_id=device.group_id,
606+
)
576607
self.message_user(
577608
request,
578609
_('Successfully changed group of selected devices.'),
579610
messages.SUCCESS,
580611
)
581612
return HttpResponseRedirect(request.get_full_path())
582613

583-
form = ChangeDeviceGroupForm()
614+
form = ChangeDeviceGroupForm(org_id=org_id)
584615
context = {
585616
'title': _('Change group'),
586617
'queryset': queryset,

openwisp_controller/config/tests/test_admin.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,40 +1727,83 @@ def test_group_templates_apply(self):
17271727

17281728
def test_change_device_group_action_changes_templates(self):
17291729
path = reverse(f'admin:{self.app_label}_device_changelist')
1730-
org = self._get_org(org_name='default')
1730+
org1 = self._create_org(name='org1', slug='org1')
1731+
org2 = self._create_org(name='org2', slug='org2')
17311732
t1 = self._create_template(name='t1')
17321733
t2 = self._create_template(name='t2')
1733-
dg1 = self._create_device_group(name='test-group-1', organization=org)
1734+
dg1 = self._create_device_group(name='test-group-1', organization=org1)
17341735
dg1.templates.add(t1)
1735-
dg2 = self._create_device_group(name='test-group-2', organization=org)
1736+
dg2 = self._create_device_group(name='test-group-2', organization=org1)
17361737
dg2.templates.add(t2)
1737-
device = self._create_device(organization=org, group=dg1)
1738-
templates = device.config.templates.all()
1738+
device1 = self._create_device(organization=org1, group=dg1)
1739+
device2 = self._create_device_config(
1740+
device_opts={'organization': org1, 'mac_address': '11:22:33:44:55:66'}
1741+
)
1742+
templates = device1.config.templates.all()
17391743
self.assertNotIn(t2, templates)
17401744
self.assertIn(t1, templates)
17411745
post_data = {
1742-
'_selected_action': [device.pk],
1746+
'_selected_action': [device1.pk],
17431747
'action': 'change_group',
17441748
'csrfmiddlewaretoken': 'test',
1749+
'apply': True,
17451750
}
1751+
17461752
with self.subTest('change group'):
17471753
post_data['device_group'] = str(dg2.pk)
1748-
post_data['apply'] = True
17491754
response = self.client.post(path, post_data, follow=True)
17501755
self.assertEqual(response.status_code, 200)
17511756
self.assertContains(
17521757
response, 'Successfully changed group of selected devices.'
17531758
)
1754-
templates = device.config.templates.all()
1759+
templates = device1.config.templates.all()
17551760
self.assertIn(t2, templates)
17561761
self.assertNotIn(t1, templates)
1762+
17571763
with self.subTest('unassign group'):
17581764
post_data['device_group'] = ''
17591765
response = self.client.post(path, post_data, follow=True)
17601766
self.assertEqual(response.status_code, 200)
1761-
templates = list(device.config.templates.all())
1767+
templates = list(device1.config.templates.all())
17621768
self.assertEqual(templates, [])
17631769

1770+
with self.subTest('Change group for multiple devices'):
1771+
data = post_data.copy()
1772+
data['_selected_action'] = [device1.pk, device2.pk]
1773+
data['device_group'] = str(dg2.pk)
1774+
with patch.object(Device, '_send_device_group_changed_signal') as mocked:
1775+
response = self.client.post(path, data, follow=True)
1776+
self.assertEqual(response.status_code, 200)
1777+
self.assertEqual(len(mocked.call_args_list), 2)
1778+
1779+
device2.organization = org2
1780+
device2.save()
1781+
1782+
with self.subTest('Select devices from different organization'):
1783+
data = post_data.copy()
1784+
data['_selected_action'] = [device1.pk, device2.pk]
1785+
response = self.client.post(path, data, follow=True)
1786+
self.assertContains(response, 'Select devices from one organization')
1787+
1788+
data.pop('apply')
1789+
data.pop('device_group')
1790+
response = self.client.post(path, data, follow=True)
1791+
self.assertContains(response, 'Select devices from one organization')
1792+
1793+
org_user = self._create_administrator(organizations=[org1])
1794+
self.client.force_login(org_user)
1795+
1796+
with self.subTest('Select devices from org not managed by user'):
1797+
data = post_data.copy()
1798+
data['_selected_action'] = [device2.pk]
1799+
response = self.client.post(path, data, follow=True)
1800+
self.assertEqual(response.status_code, 403)
1801+
1802+
data.pop('apply')
1803+
data.pop('device_group')
1804+
response = self.client.post(path, data, follow=True)
1805+
self.assertEqual(response.status_code, 403)
1806+
17641807
def test_change_device_group_changes_templates(self):
17651808
org = self._get_org(org_name='default')
17661809
t1 = self._create_template(name='t1')

0 commit comments

Comments
 (0)