Skip to content

Commit 5bec55a

Browse files
author
Ilyas Gasanov
committed
[DOP-19926] Provide guest role when changing group owner
1 parent fad630c commit 5bec55a

File tree

8 files changed

+151
-10
lines changed

8 files changed

+151
-10
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Grant read-only rights for the previous group owner when ownership is transferred

syncmaster/backend/api/v1/groups.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
from syncmaster.backend.services import UnitOfWork, get_user
66
from syncmaster.db.models import User
7+
from syncmaster.db.models.group import GroupMemberRole
78
from syncmaster.db.utils import Permission
89
from syncmaster.errors.registration import get_error_responses
910
from syncmaster.exceptions import ActionNotAllowedError
10-
from syncmaster.exceptions.group import GroupNotFoundError
11+
from syncmaster.exceptions.group import AlreadyIsGroupOwnerError, GroupNotFoundError
1112
from syncmaster.schemas.v1.groups import (
1213
AddUserSchema,
1314
CreateGroupSchema,
@@ -104,12 +105,29 @@ async def update_group(
104105
raise ActionNotAllowedError
105106

106107
async with unit_of_work:
108+
group = await unit_of_work.group.read_by_id(group_id=group_id)
109+
previous_owner_id = group.owner_id
110+
107111
group = await unit_of_work.group.update(
108112
group_id=group_id,
109113
owner_id=group_data.owner_id,
110114
name=group_data.name,
111115
description=group_data.description,
112116
)
117+
118+
if previous_owner_id != group_data.owner_id:
119+
new_owner_user_group = await unit_of_work.group.get_user_group(
120+
group_id=group_id,
121+
user_id=group_data.owner_id,
122+
)
123+
if new_owner_user_group:
124+
await unit_of_work.group.delete_user(group_id=group_id, target_user_id=group_data.owner_id)
125+
126+
await unit_of_work.group.add_user(
127+
group_id=group_id,
128+
new_user_id=previous_owner_id,
129+
role=GroupMemberRole.Guest,
130+
)
113131
return ReadGroupSchema.from_orm(group)
114132

115133

@@ -195,6 +213,10 @@ async def add_user_to_group(
195213
if resource_rule < Permission.DELETE:
196214
raise ActionNotAllowedError
197215

216+
group = await unit_of_work.group.read_by_id(group_id=group_id)
217+
if group.owner_id == user_id:
218+
raise AlreadyIsGroupOwnerError
219+
198220
async with unit_of_work:
199221
await unit_of_work.group.add_user(
200222
group_id=group_id,

syncmaster/backend/handler.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from syncmaster.exceptions.credentials import AuthDataNotFoundError
2323
from syncmaster.exceptions.group import (
2424
AlreadyIsGroupMemberError,
25+
AlreadyIsGroupOwnerError,
2526
AlreadyIsNotGroupMemberError,
2627
GroupAdminNotFoundError,
2728
GroupAlreadyExistsError,
@@ -194,6 +195,14 @@ async def syncmsater_exception_handler(request: Request, exc: SyncmasterError):
194195
content=content,
195196
)
196197

198+
if isinstance(exc, AlreadyIsGroupOwnerError):
199+
content.code = "conflict"
200+
content.message = "User already is group owner"
201+
return exception_json_response(
202+
status=status.HTTP_409_CONFLICT,
203+
content=content,
204+
)
205+
197206
if isinstance(exc, UserNotFoundError):
198207
content.code = "not_found"
199208
content.message = "User not found"

syncmaster/db/repositories/group.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,15 @@ async def get_group_permission(self, user: User, group_id: int) -> Permission:
337337

338338
return Permission.READ
339339

340+
async def get_user_group(self, group_id: int, user_id: int) -> UserGroup | None:
341+
return await self._session.get(
342+
UserGroup,
343+
{
344+
"group_id": group_id,
345+
"user_id": user_id,
346+
},
347+
)
348+
340349
async def delete_user(
341350
self,
342351
group_id: int,

syncmaster/exceptions/group.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,9 @@ class AlreadyIsNotGroupMemberError(SyncmasterError):
2323
pass
2424

2525

26+
class AlreadyIsGroupOwnerError(SyncmasterError):
27+
pass
28+
29+
2630
class GroupNotFoundError(SyncmasterError):
2731
pass

tests/test_unit/test_connections/connection_fixtures/group_connections_fixture.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ async def group_connections(
4141
)
4242
elif conn_type in [
4343
ConnectionType.POSTGRES,
44-
ConnectionType.ORACLE,
4544
ConnectionType.CLICKHOUSE,
4645
ConnectionType.MSSQL,
4746
ConnectionType.MYSQL,

tests/test_unit/test_groups/test_add_user_to_group.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,34 @@ async def test_owner_add_unknown_user_to_group_error(
337337
}
338338

339339

340+
async def test_add_exiting_owner_as_a_group_member(
341+
client: AsyncClient,
342+
empty_group: MockGroup,
343+
role_maintainer_or_below: UserTestRoles,
344+
):
345+
# Arrange
346+
user = empty_group.get_member_of_role(UserTestRoles.Owner)
347+
348+
# Act
349+
result = await client.post(
350+
f"v1/groups/{empty_group.id}/users/{empty_group.owner_id}",
351+
headers={"Authorization": f"Bearer {user.token}"},
352+
json={
353+
"role": role_maintainer_or_below,
354+
},
355+
)
356+
357+
# Assert
358+
assert result.status_code == 409
359+
assert result.json() == {
360+
"error": {
361+
"code": "conflict",
362+
"message": "User already is group owner",
363+
"details": None,
364+
},
365+
}
366+
367+
340368
async def test_superuser_add_user_to_unknown_group_error(
341369
client: AsyncClient,
342370
superuser: MockUser,

tests/test_unit/test_groups/test_update_group_by_id.py

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,93 @@ async def test_validation_on_update_group(
192192

193193

194194
async def test_owner_change_group_owner(client: AsyncClient, empty_group: MockGroup, simple_user: MockUser):
195-
# Act
196-
result = await client.patch(
195+
previous_owner = empty_group.owner
196+
new_owner = simple_user
197+
user = empty_group.get_member_of_role(UserTestRoles.Owner)
198+
199+
# Change a group owner
200+
patch_result = await client.patch(
197201
f"v1/groups/{empty_group.id}",
198-
headers={"Authorization": f"Bearer {empty_group.get_member_of_role(UserTestRoles.Owner).token}"},
202+
headers={"Authorization": f"Bearer {user.token}"},
199203
json={
200204
"name": empty_group.name,
201-
"owner_id": simple_user.id,
205+
"owner_id": new_owner.id,
202206
"description": empty_group.description,
203207
},
204208
)
205-
# Assert
206-
assert result.json() == {
209+
group_users_result = await client.get(
210+
f"v1/groups/{empty_group.id}/users",
211+
headers={"Authorization": f"Bearer {user.token}"},
212+
)
213+
214+
assert patch_result.status_code == 200
215+
assert patch_result.json() == {
207216
"id": empty_group.id,
208217
"name": empty_group.name,
209-
"owner_id": simple_user.id,
218+
"owner_id": new_owner.id,
210219
"description": empty_group.description,
211220
}
212-
assert result.status_code == 200
221+
# Make sure previous owner became a guest in group
222+
assert group_users_result.status_code == 200
223+
assert group_users_result.json()["items"] == [
224+
{
225+
"id": previous_owner.id,
226+
"username": previous_owner.username,
227+
"role": UserTestRoles.Guest,
228+
},
229+
]
230+
231+
232+
async def test_owner_change_group_owner_with_existing_role(
233+
client: AsyncClient,
234+
empty_group: MockGroup,
235+
simple_user: MockUser,
236+
role_maintainer_or_below: UserTestRoles,
237+
):
238+
previous_owner = empty_group.owner
239+
new_owner = simple_user
240+
user = empty_group.get_member_of_role(UserTestRoles.Owner)
241+
242+
# Make user a group member
243+
await client.post(
244+
f"v1/groups/{empty_group.id}/users/{new_owner.id}",
245+
headers={"Authorization": f"Bearer {user.token}"},
246+
json={
247+
"role": role_maintainer_or_below,
248+
},
249+
)
250+
# Upgrade user to a group owner
251+
patch_result = await client.patch(
252+
f"v1/groups/{empty_group.id}",
253+
headers={"Authorization": f"Bearer {user.token}"},
254+
json={
255+
"name": empty_group.name,
256+
"owner_id": new_owner.id,
257+
"description": empty_group.description,
258+
},
259+
)
260+
group_users_result = await client.get(
261+
f"v1/groups/{empty_group.id}/users",
262+
headers={"Authorization": f"Bearer {user.token}"},
263+
)
264+
265+
assert patch_result.status_code == 200
266+
assert patch_result.json() == {
267+
"id": empty_group.id,
268+
"name": empty_group.name,
269+
"owner_id": new_owner.id,
270+
"description": empty_group.description,
271+
}
272+
# Make sure previous owner became a guest in group
273+
# As well as upgraded owner is no longer considered a group member
274+
assert group_users_result.status_code == 200
275+
assert group_users_result.json()["items"] == [
276+
{
277+
"id": previous_owner.id,
278+
"username": previous_owner.username,
279+
"role": UserTestRoles.Guest,
280+
},
281+
]
213282

214283

215284
async def test_maintainer_or_below_cannot_change_group_owner(

0 commit comments

Comments
 (0)