Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 48 additions & 6 deletions openwisp_notifications/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,58 @@
import swapper
from rest_framework.permissions import BasePermission

User = swapper.load_model("openwisp_users", "User")
Organization = swapper.load_model("openwisp_users", "Organization")


class PreferencesPermission(BasePermission):
"""
Permission class for the notification preferences.
Custom permission for accessing notification preferences via the API.

Access rules:
1. Superusers can always access any user's preferences.
2. Users can access their own preferences.
3. Staff users can access another user's preferences if all of the following are true:
- They have the 'change_notificationsetting' permission.
- They manage at least one organization that the target user belongs to.
- If an organization_id is provided in the URL, it must be among the shared organizations.

Permission is granted only in these two cases:
1. Superusers can change the notification preferences of any user.
2. Regular users can only change their own preferences.
Returns:
bool: True if access is allowed, False otherwise.
"""

def has_permission(self, request, view):
return request.user.is_superuser or request.user.id == view.kwargs.get(
"user_id"
user = request.user
target_user_id = view.kwargs.get("user_id")
target_user = User.objects.get(id=target_user_id) if target_user_id else None
target_org_id = view.kwargs.get("organization_id")
target_org = (
Organization.objects.get(id=target_org_id) if target_org_id else None
)

# Superusers always have access
if user.is_superuser:
return True

# Users can access their own preferences
if target_user_id and user.id == target_user_id:
return True

# Staff users with proper permission can access users in their managed orgs
if user.is_staff and user.has_perm(
"openwisp_notifications.change_notificationsetting"
):
admin_orgs = set(user.organizations_managed)
target_orgs = set(target_user.organizations_dict.keys())
shared_orgs = admin_orgs.intersection(target_orgs)

# If an organization_id is provided, validate it’s one of the shared ones
if target_org and target_org not in shared_orgs:
return False

# Allow if there’s at least one shared organization
if shared_orgs:
return True

# Otherwise, deny access
return False
9 changes: 6 additions & 3 deletions openwisp_notifications/templates/admin/base_site.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
<span class="toggle-btn" id="ow-mark-all-read" tabindex="0" role="button">
{% trans 'Mark all as read' %}
</span>
<a class="toggle-btn" href="{% url 'notifications:notification_preference' %}">
{% trans 'Notification Preferences' %}
</a>
{% can_change_notifications as can_change_notifications %}
{% if can_change_notifications %}
<a class="toggle-btn" href="{% url 'notifications:notification_preference' %}">
{% trans 'Notification Preferences' %}
</a>
{% endif %}
</div>
<div class="ow-notification-wrapper ow-round-bottom-border">
<div id="ow-notifications-loader" class="ow-hide"><div class="loader"></div></div>
Expand Down
31 changes: 28 additions & 3 deletions openwisp_notifications/templatetags/notification_tags.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# -*- coding: utf-8 -*-
import swapper
from django.core.cache import cache
from django.template import Library
from django.utils.html import format_html

from openwisp_notifications.swapper import load_model
from openwisp_notifications.utils import normalize_unread_count

Notification = load_model("Notification")
Notification = swapper.load_model("openwisp_notifications", "Notification")
User = swapper.load_model("openwisp_users", "User")
Organization = swapper.load_model("openwisp_users", "Organization")

register = Library()

Expand All @@ -26,6 +28,7 @@ def get_notifications_count(context):
return count


@register.simple_tag(takes_context=True)
def unread_notifications(context):
count = get_notifications_count(context)
output = ""
Expand All @@ -44,4 +47,26 @@ def should_load_notifications_widget(request):
)


register.simple_tag(takes_context=True)(unread_notifications)
@register.simple_tag(takes_context=True)
def can_change_notifications(context):
"""
Template tag to determine whether the "Notification Preferences" button
should be rendered in the UI.

The button is shown if:
- The user is a superuser, OR
- The user is viewing their own preferences (matches 'user_id' in context), OR
- The user is staff AND has the 'change_notificationsetting' permission.

Returns:
bool: True if the button should be displayed, False otherwise.
"""
user = context["request"].user
return (
user.is_superuser
or user.id == context.get("user_id")
or (
user.is_staff
and user.has_perm("openwisp_notifications.change_notificationsetting")
)
)
201 changes: 201 additions & 0 deletions openwisp_notifications/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from datetime import datetime
from unittest.mock import patch

from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache
from django.test import TransactionTestCase
Expand Down Expand Up @@ -1287,6 +1288,206 @@ def test_organization_setting_inactive_organization(self):
response = self.client.get(url)
self.assertEqual(response.status_code, 404)

def _add_permission(self, user, perm_codename):
perm = Permission.objects.get(codename=perm_codename)
user.user_permissions.add(perm)
user.save()

def _permissions_setUp(self):
self.staff_with_perm = self._create_user(
is_staff=True,
username="staff_with_perm",
email="[email protected]",
)
self.staff_without_perm = self._create_user(
is_staff=True,
username="staff_without_perm",
email="[email protected]",
)

# Give permission only to one staff user
self._add_permission(self.staff_with_perm, "change_notificationsetting")

def test_staff_user_access_own_notifications(self):
"""Test that a staff user can access their own notification settings if they have permission"""

self._permissions_setUp()
user = self.staff_with_perm

url_list = reverse(
"notifications:user_notification_setting_list",
kwargs={
"user_id": self.staff_with_perm.id,
},
)

notification_setting_staff_with_perm = NotificationSetting.objects.create(
user=self.staff_with_perm, type="default", web=True, email=True
)

url_setting = reverse(
"notifications:user_notification_setting",
kwargs={
"user_id": self.staff_with_perm.id,
"pk": notification_setting_staff_with_perm.id,
},
)

self.client.force_login(user)

# The user should be able to access the list and detail views
response = self.client.get(url_list)
self.assertEqual(response.status_code, 200)

response = self.client.get(url_setting)
self.assertEqual(response.status_code, 200)

def test_staff_user_access_own_notifications_no_perm(self):
"""Test that a staff user can access their own notification
settings even without explicit permission"""

self._permissions_setUp()
user = self.staff_without_perm

url_list = reverse(
"notifications:user_notification_setting_list",
kwargs={
"user_id": self.staff_without_perm.id,
},
)

notification_setting_staff_without_perm = NotificationSetting.objects.create(
user=self.staff_without_perm, type="default", web=True, email=True
)

url_setting = reverse(
"notifications:user_notification_setting",
kwargs={
"user_id": self.staff_without_perm.id,
"pk": notification_setting_staff_without_perm.id,
},
)

self.client.force_login(user)

# The user should be able to access their own list and detail views
response = self.client.get(url_list)
self.assertEqual(response.status_code, 200)

response = self.client.get(url_setting)
self.assertEqual(response.status_code, 200)

def test_staff_user_access_managed_org_user_notifications(self):
"""Test that a staff user with permission can access
notification settings of users in organizations they manage"""

self._permissions_setUp()
user_of_org = self._create_user(
is_staff=False, username="user_of_org", email="[email protected]"
)

org = self._create_org(name="test1-org", slug="default")
org.add_user(self.staff_with_perm, is_admin=True)
org.add_user(user_of_org, is_admin=False)

url_list = reverse(
"notifications:user_notification_setting_list",
kwargs={"user_id": user_of_org.id},
)

notification_setting_user_of_org = NotificationSetting.objects.create(
user=user_of_org, type="default", web=True, email=True
)

url_setting = reverse(
"notifications:user_notification_setting",
kwargs={
"user_id": user_of_org.id,
"pk": notification_setting_user_of_org.id,
},
)

self.client.force_login(self.staff_with_perm)

# Staff user should have access because they manage the organization
response = self.client.get(url_list)
self.assertEqual(response.status_code, 200)

response = self.client.get(url_setting)
self.assertEqual(response.status_code, 200)

def test_staff_user_no_access_to_unmanaged_org_user_notifications(self):
"""Test that staff users without permission or not managing the
organization cannot access notification settings of other users"""

self._permissions_setUp()
# Case 1: Staff user without permission trying to access user in their org
user_of_org1 = self._create_user(
is_staff=False, username="user_of_org1", email="[email protected]"
)

org1 = self._create_org(name="test1-org", slug="default")
org1.add_user(self.staff_without_perm, is_admin=True)
org1.add_user(user_of_org1, is_admin=False)

url = reverse(
"notifications:user_notification_setting_list",
kwargs={"user_id": user_of_org1.id},
)

self.client.force_login(self.staff_without_perm)
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

# Case 2: Staff user with permission but not managing the
# organization trying to access user in that org
user_of_org2 = self._create_user(
is_staff=False, username="user_of_org2", email="[email protected]"
)

staff_with_perm_but_not_owner_of_org2 = self._create_user(
is_staff=True,
username="staff_with_perm_but_now_owner_of_org2",
email="[email protected]",
)

self._add_permission(
staff_with_perm_but_not_owner_of_org2, "change_notificationsetting"
)

org2 = self._create_org(name="test2-org", slug="default")
org2.add_user(self.staff_with_perm, is_admin=True)
org2.add_user(staff_with_perm_but_not_owner_of_org2, is_admin=False)
org2.add_user(user_of_org2, is_admin=False)

url = reverse(
"notifications:user_notification_setting_list",
kwargs={"user_id": user_of_org2.id},
)

self.client.force_login(staff_with_perm_but_not_owner_of_org2)
response = self.client.get(url)
self.assertEqual(response.status_code, 403)

# Case 3: Staff user with permission trying to access user not in any managed org
not_user_of_org3 = self._create_user(
is_staff=False,
username="not_user_of_org3",
email="[email protected]",
)

org3 = self._create_org(name="test3-org", slug="default")
org3.add_user(self.staff_with_perm, is_admin=True)

url = reverse(
"notifications:user_notification_setting_list",
kwargs={"user_id": not_user_of_org3.id},
)

self.client.force_login(self.staff_with_perm)
response = self.client.get(url)
self.assertEqual(response.status_code, 403)


class TestMultitenancyApi(
TestNotificationMixin,
Expand Down
Loading