Skip to content

Commit d3df815

Browse files
committed
✨(backend) add user abilities system for mail domain permissions
- Add get_abilities() method to User model to determine user permissions - Add abilities field to UserSerializer to expose permissions via API - Implement permission logic for create_maildomains and view_maildomains - Add comprehensive tests for User model abilities method - Add integration tests for abilities field in users/me endpoint - Support superuser+staff, mail domain access, and regular user scenarios The abilities system provides granular permission control for mail domain operations, allowing the frontend to adapt UI based on user capabilities.
1 parent 8a46381 commit d3df815

File tree

4 files changed

+228
-4
lines changed

4 files changed

+228
-4
lines changed

src/backend/core/api/serializers.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@
1212
class UserSerializer(serializers.ModelSerializer):
1313
"""Serialize users."""
1414

15+
abilities = serializers.SerializerMethodField(read_only=True)
16+
17+
def get_abilities(self, user) -> dict:
18+
"""Return abilities of the logged-in user on the mail domain."""
19+
request = self.context.get("request")
20+
if request:
21+
return user.get_abilities()
22+
return {}
23+
1524
class Meta:
1625
model = models.User
17-
fields = ["id", "email", "full_name", "short_name"]
18-
read_only_fields = ["id", "email", "full_name", "short_name"]
26+
fields = ["id", "email", "full_name", "short_name", "abilities"]
27+
read_only_fields = ["id", "email", "full_name", "short_name", "abilities"]
1928

2029

2130
class MailboxAvailableSerializer(serializers.ModelSerializer):

src/backend/core/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,16 @@ class Meta:
185185
def __str__(self):
186186
return self.email or self.admin_email or str(self.id)
187187

188+
def get_abilities(self):
189+
"""Return abilities of the logged-in user."""
190+
# if user as access to any maildomain, he can view them
191+
has_access = self.maildomain_accesses.exists()
192+
is_super_admin = self.is_superuser and self.is_staff
193+
return {
194+
"create_maildomains": is_super_admin,
195+
"view_maildomains": has_access or is_super_admin,
196+
}
197+
188198

189199
class MailDomain(BaseModel):
190200
"""Mail domain model to store mail domain information."""

src/backend/core/tests/api/test_users.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from rest_framework.test import APIClient
77

8-
from core import factories
8+
from core import factories, models
99

1010
pytestmark = pytest.mark.django_db
1111

@@ -34,9 +34,126 @@ def test_api_users_retrieve_me_authenticated():
3434
)
3535

3636
assert response.status_code == 200
37-
assert response.json() == {
37+
data = response.json()
38+
assert data == {
3839
"id": str(user.id),
3940
"email": user.email,
4041
"full_name": user.full_name,
4142
"short_name": user.short_name,
43+
"abilities": {
44+
"create_maildomains": False,
45+
"view_maildomains": False,
46+
},
4247
}
48+
49+
50+
def test_api_users_retrieve_me_with_abilities_regular_user():
51+
"""Test abilities for regular user without mail domain access."""
52+
user = factories.UserFactory()
53+
54+
client = APIClient()
55+
client.force_login(user)
56+
57+
response = client.get("/api/v1.0/users/me/")
58+
59+
assert response.status_code == 200
60+
data = response.json()
61+
abilities = data["abilities"]
62+
assert abilities["create_maildomains"] is False
63+
assert abilities["view_maildomains"] is False
64+
65+
66+
def test_api_users_retrieve_me_with_abilities_user_with_access():
67+
"""Test abilities for user with mail domain access."""
68+
user = factories.UserFactory()
69+
maildomain = factories.MailDomainFactory()
70+
71+
# Give user access to a mail domain
72+
models.MailDomainAccess.objects.create(
73+
maildomain=maildomain,
74+
user=user,
75+
role=models.MailDomainAccessRoleChoices.ADMIN,
76+
)
77+
78+
client = APIClient()
79+
client.force_login(user)
80+
81+
response = client.get("/api/v1.0/users/me/")
82+
83+
assert response.status_code == 200
84+
data = response.json()
85+
abilities = data["abilities"]
86+
assert abilities["create_maildomains"] is False
87+
assert abilities["view_maildomains"] is True
88+
89+
90+
def test_api_users_retrieve_me_with_abilities_superuser_staff():
91+
"""Test abilities for superuser and staff user."""
92+
user = factories.UserFactory(is_superuser=True, is_staff=True)
93+
94+
client = APIClient()
95+
client.force_login(user)
96+
97+
response = client.get("/api/v1.0/users/me/")
98+
99+
assert response.status_code == 200
100+
data = response.json()
101+
abilities = data["abilities"]
102+
assert abilities["create_maildomains"] is True
103+
assert abilities["view_maildomains"] is True
104+
105+
106+
def test_api_users_retrieve_me_with_abilities_superuser_not_staff():
107+
"""Test abilities for superuser without staff status."""
108+
user = factories.UserFactory(is_superuser=True, is_staff=False)
109+
110+
client = APIClient()
111+
client.force_login(user)
112+
113+
response = client.get("/api/v1.0/users/me/")
114+
115+
assert response.status_code == 200
116+
data = response.json()
117+
abilities = data["abilities"]
118+
assert abilities["create_maildomains"] is False
119+
assert abilities["view_maildomains"] is False
120+
121+
122+
def test_api_users_retrieve_me_with_abilities_staff_not_superuser():
123+
"""Test abilities for staff user without superuser status."""
124+
user = factories.UserFactory(is_superuser=False, is_staff=True)
125+
126+
client = APIClient()
127+
client.force_login(user)
128+
129+
response = client.get("/api/v1.0/users/me/")
130+
131+
assert response.status_code == 200
132+
data = response.json()
133+
abilities = data["abilities"]
134+
assert abilities["create_maildomains"] is False
135+
assert abilities["view_maildomains"] is False
136+
137+
138+
def test_api_users_retrieve_me_with_abilities_superuser_staff_with_access():
139+
"""Test abilities for superuser/staff with mail domain access."""
140+
user = factories.UserFactory(is_superuser=True, is_staff=True)
141+
maildomain = factories.MailDomainFactory()
142+
143+
# Give user access to a mail domain
144+
models.MailDomainAccess.objects.create(
145+
maildomain=maildomain,
146+
user=user,
147+
role=models.MailDomainAccessRoleChoices.ADMIN,
148+
)
149+
150+
client = APIClient()
151+
client.force_login(user)
152+
153+
response = client.get("/api/v1.0/users/me/")
154+
155+
assert response.status_code == 200
156+
data = response.json()
157+
abilities = data["abilities"]
158+
assert abilities["create_maildomains"] is True
159+
assert abilities["view_maildomains"] is True
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Tests for User model get_abilities method."""
2+
3+
import pytest
4+
5+
from core import models
6+
from core.factories import MailDomainFactory, UserFactory
7+
8+
pytestmark = pytest.mark.django_db
9+
10+
11+
@pytest.mark.django_db
12+
class TestUserGetAbilities:
13+
"""Test the get_abilities method on User model."""
14+
15+
def test_abilities_superuser_staff(self):
16+
"""Test abilities when user is superuser and staff."""
17+
user = UserFactory(is_superuser=True, is_staff=True)
18+
19+
abilities = user.get_abilities()
20+
21+
assert abilities["create_maildomains"] is True
22+
assert abilities["view_maildomains"] is True
23+
24+
def test_abilities_superuser_not_staff(self):
25+
"""Test abilities when user is superuser but not staff."""
26+
user = UserFactory(is_superuser=True, is_staff=False)
27+
28+
abilities = user.get_abilities()
29+
30+
assert abilities["create_maildomains"] is False
31+
assert abilities["view_maildomains"] is False
32+
33+
def test_abilities_staff_not_superuser(self):
34+
"""Test abilities when user is staff but not superuser."""
35+
user = UserFactory(is_superuser=False, is_staff=True)
36+
37+
abilities = user.get_abilities()
38+
39+
assert abilities["create_maildomains"] is False
40+
assert abilities["view_maildomains"] is False
41+
42+
def test_abilities_staff_not_superuser_with_maildomain_access(self):
43+
"""Test abilities when user is staff but not superuser and has mail domain access."""
44+
user = UserFactory(is_superuser=False, is_staff=True)
45+
maildomain = MailDomainFactory()
46+
models.MailDomainAccess.objects.create(
47+
maildomain=maildomain,
48+
user=user,
49+
role=models.MailDomainAccessRoleChoices.ADMIN,
50+
)
51+
52+
abilities = user.get_abilities()
53+
assert abilities["create_maildomains"] is False
54+
assert abilities["view_maildomains"] is True
55+
56+
def test_abilities_regular_user(self):
57+
"""Test abilities when user is regular user."""
58+
user = UserFactory(is_superuser=False, is_staff=False)
59+
60+
abilities = user.get_abilities()
61+
62+
assert abilities["create_maildomains"] is False
63+
assert abilities["view_maildomains"] is False
64+
65+
def test_abilities_with_maildomain_access(self):
66+
"""Test abilities when user has mail domain access."""
67+
user = UserFactory()
68+
maildomain = MailDomainFactory()
69+
70+
# Give user access to a mail domain
71+
models.MailDomainAccess.objects.create(
72+
maildomain=maildomain,
73+
user=user,
74+
role=models.MailDomainAccessRoleChoices.ADMIN,
75+
)
76+
77+
abilities = user.get_abilities()
78+
79+
assert abilities["view_maildomains"] is True
80+
assert abilities["create_maildomains"] is False
81+
82+
def test_abilities_without_maildomain_access(self):
83+
"""Test abilities when user has no mail domain access."""
84+
user = UserFactory()
85+
abilities = user.get_abilities()
86+
87+
assert abilities["view_maildomains"] is False
88+
assert abilities["create_maildomains"] is False

0 commit comments

Comments
 (0)