Skip to content

Commit ade9399

Browse files
committed
[fix] Add org specific group and add tests
1 parent 9fcee51 commit ade9399

File tree

2 files changed

+100
-43
lines changed

2 files changed

+100
-43
lines changed

openwisp_controller/geo/channels/consumers.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import swapper
2+
from asgiref.sync import async_to_sync
23
from django_loci.channels.base import BaseCommonLocationBroadcast, BaseLocationBroadcast
34

45
Location = swapper.load_model("geo", "Location")
@@ -22,12 +23,16 @@ def is_authorized(self, user, location):
2223
class CommonLocationBroadcast(BaseCommonLocationBroadcast):
2324
model = Location
2425

25-
def is_autherized(self, user, location):
26-
result = super().is_authorized(user, location)
27-
if (
28-
result
29-
and not user.is_superuser
30-
and not user.is_manager(location.organization)
31-
):
32-
return False
33-
return result
26+
def join_groups(self, user):
27+
"""
28+
Subscribe user to all organizations they manage or bypass if superuser.
29+
"""
30+
if user.is_superuser:
31+
super().join_groups(user)
32+
return
33+
34+
self.group_names = []
35+
for org in user.organizations_managed:
36+
group = f"loci.mobile-location.organization.{org}"
37+
self.group_names.append(group)
38+
async_to_sync(self.channel_layer.group_add)(group, self.channel_name)
Lines changed: 86 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1+
import asyncio
12
import os
3+
from contextlib import suppress
24
from unittest import skipIf
35

46
import pytest
57
from channels.db import database_sync_to_async
8+
from channels.layers import get_channel_layer
69
from channels.routing import ProtocolTypeRouter
710
from django.conf import settings
811
from django.contrib.auth import get_user_model
912
from django.contrib.auth.models import Permission
1013
from django.utils.module_loading import import_string
11-
from django_loci.tests.base.test_channels import BaseTestChannels
14+
from django_loci.tests import TestChannelsMixin
1215
from swapper import load_model
1316

17+
from openwisp_controller.geo.channels.consumers import (
18+
CommonLocationBroadcast,
19+
LocationBroadcast,
20+
)
21+
1422
from .utils import TestGeoMixin
1523

1624
Device = load_model("config", "Device")
@@ -21,7 +29,9 @@
2129

2230

2331
@skipIf(os.environ.get("SAMPLE_APP", False), "Running tests on SAMPLE_APP")
24-
class TestChannels(TestGeoMixin, BaseTestChannels):
32+
class TestChannels(TestGeoMixin, TestChannelsMixin):
33+
location_consumer = LocationBroadcast
34+
common_location_consumer = CommonLocationBroadcast
2535
application = import_string(getattr(settings, "ASGI_APPLICATION"))
2636
object_model = Device
2737
location_model = Location
@@ -37,35 +47,32 @@ async def test_consumer_staff_but_no_change_permission(self):
3747
location = await database_sync_to_async(self._create_location)(is_mobile=True)
3848
await database_sync_to_async(self._create_object_location)(location=location)
3949
pk = location.pk
40-
request_vars = await self._get_specific_location_request_dict(user=user, pk=pk)
50+
request_vars = await self._get_specific_location_request_dict(pk=pk, user=user)
4151
communicator = self._get_specific_location_communicator(request_vars, user)
4252
connected, _ = await communicator.connect()
4353
assert not connected
4454
await communicator.disconnect()
4555
# add permission to change location and repeat
46-
perm = await database_sync_to_async(
47-
(
48-
await database_sync_to_async(Permission.objects.filter)(
49-
name="Can change location"
50-
)
51-
).first
52-
)()
56+
perm = await Permission.objects.filter(
57+
codename=f"change_{self.location_model._meta.model_name}",
58+
content_type__app_label=self.location_model._meta.app_label,
59+
).afirst()
5360
await database_sync_to_async(user.user_permissions.add)(perm)
5461
user = await database_sync_to_async(User.objects.get)(pk=user.pk)
55-
request_vars = await self._get_specific_location_request_dict(user=user, pk=pk)
62+
request_vars = await self._get_specific_location_request_dict(pk=pk, user=user)
5663
communicator = self._get_specific_location_communicator(request_vars, user)
5764
connected, _ = await communicator.connect()
5865
assert not connected
5966
await communicator.disconnect()
6067
# add user to organization
6168
await database_sync_to_async(OrganizationUser.objects.create)(
62-
organization=location.organization, user=user, is_admin=True
69+
organization=location.organization,
70+
user=user,
71+
is_admin=True,
6372
)
6473
await database_sync_to_async(location.organization.save)()
6574
user = await database_sync_to_async(User.objects.get)(pk=user.pk)
66-
request_vars = await self._ge_get_specific_location_request_dictt_request_dict(
67-
user=user, pk=pk
68-
)
75+
request_vars = await self._get_specific_location_request_dict(pk=pk, user=user)
6976
communicator = self._get_specific_location_communicator(request_vars, user)
7077
connected, _ = await communicator.connect()
7178
assert connected
@@ -80,37 +87,82 @@ async def test_common_location_consumer_staff_but_no_change_permission(self):
8087
location = await database_sync_to_async(self._create_location)(is_mobile=True)
8188
await database_sync_to_async(self._create_object_location)(location=location)
8289
pk = location.pk
83-
request_vars = await self._get_common_location_request_dict(user=user, pk=pk)
90+
request_vars = await self._get_common_location_request_dict(pk=pk, user=user)
8491
communicator = self._get_common_location_communicator(request_vars, user)
8592
connected, _ = await communicator.connect()
8693
assert not connected
8794
await communicator.disconnect()
88-
# add permission to change location and repeat
89-
perm = await database_sync_to_async(
90-
(
91-
await database_sync_to_async(Permission.objects.filter)(
92-
name="Can change location"
93-
)
94-
).first
95-
)()
95+
# After granting change permission, the user can connect to the common
96+
# location endpoint, but must receive updates only for locations
97+
# belonging to their organization.
98+
perm = await Permission.objects.filter(
99+
codename=f"change_{self.location_model._meta.model_name}",
100+
content_type__app_label=self.location_model._meta.app_label,
101+
).afirst()
96102
await database_sync_to_async(user.user_permissions.add)(perm)
97103
user = await database_sync_to_async(User.objects.get)(pk=user.pk)
98-
request_vars = await self._get_common_location_request_dict(user=user, pk=pk)
104+
request_vars = await self._get_common_location_request_dict(pk=pk, user=user)
99105
communicator = self._get_common_location_communicator(request_vars, user)
100106
connected, _ = await communicator.connect()
101-
assert not connected
107+
assert connected
102108
await communicator.disconnect()
103-
# add user to organization
109+
110+
@pytest.mark.asyncio
111+
@pytest.mark.django_db(transaction=True)
112+
async def test_common_location_org_isolation(self):
113+
org1 = await database_sync_to_async(self._create_organization)(name="test1")
114+
org2 = await database_sync_to_async(self._create_organization)(name="test2")
115+
location1 = await database_sync_to_async(self._create_location)(
116+
is_mobile=True, organization=org1
117+
)
118+
location2 = await database_sync_to_async(self._create_location)(
119+
is_mobile=True, organization=org2
120+
)
121+
user1 = await database_sync_to_async(User.objects.create_user)(
122+
username="user1", password="password", email="[email protected]", is_staff=True
123+
)
124+
user2 = await database_sync_to_async(User.objects.create_user)(
125+
username="user2", password="password", email="[email protected]", is_staff=True
126+
)
127+
perm = await Permission.objects.filter(
128+
codename=f"change_{self.location_model._meta.model_name}",
129+
content_type__app_label=self.location_model._meta.app_label,
130+
).afirst()
131+
await database_sync_to_async(user1.user_permissions.add)(perm)
132+
await database_sync_to_async(user2.user_permissions.add)(perm)
104133
await database_sync_to_async(OrganizationUser.objects.create)(
105-
organization=location.organization, user=user, is_admin=True
134+
organization=org1, user=user1, is_admin=True
106135
)
107-
await database_sync_to_async(location.organization.save)()
108-
user = await database_sync_to_async(User.objects.get)(pk=user.pk)
109-
request_vars = await self._get_common_location_request_dict(user=user, pk=pk)
110-
communicator = self._get_common_location_communicator(request_vars, user)
111-
connected, _ = await communicator.connect()
136+
await database_sync_to_async(OrganizationUser.objects.create)(
137+
organization=org2, user=user2, is_admin=True
138+
)
139+
user1 = await database_sync_to_async(User.objects.get)(pk=user1.pk)
140+
user2 = await database_sync_to_async(User.objects.get)(pk=user2.pk)
141+
channel_layer = get_channel_layer()
142+
communicator1 = self._get_common_location_communicator(
143+
await self._get_common_location_request_dict(pk=location1.pk, user=user1),
144+
user1,
145+
)
146+
communicator2 = self._get_common_location_communicator(
147+
await self._get_common_location_request_dict(pk=location2.pk, user=user2),
148+
user2,
149+
)
150+
connected, _ = await communicator1.connect()
112151
assert connected
113-
await communicator.disconnect()
152+
connected, _ = await communicator2.connect()
153+
assert connected
154+
await channel_layer.group_send(
155+
f"loci.mobile-location.organization.{org1.pk}",
156+
{"type": "send.message", "message": {"id": str(location1.pk)}},
157+
)
158+
response = await communicator1.receive_json_from(timeout=2)
159+
assert response["id"] == str(location1.pk)
160+
with pytest.raises(asyncio.TimeoutError):
161+
await communicator2.receive_json_from(timeout=2)
162+
# The task is been cancelled if not completed in the given timeout
163+
await communicator1.disconnect()
164+
with suppress(asyncio.CancelledError):
165+
await communicator2.disconnect()
114166

115167
def test_asgi_application_router(self):
116168
assert isinstance(self.application, ProtocolTypeRouter)

0 commit comments

Comments
 (0)