Skip to content

Commit 265647b

Browse files
committed
Dissalow adding member twice
1 parent c376500 commit 265647b

2 files changed

Lines changed: 76 additions & 4 deletions

File tree

src/dstack/_internal/server/services/projects.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ async def set_project_members(
197197
project: ProjectModel,
198198
members: List[MemberSetting],
199199
):
200+
usernames = {m.username for m in members}
201+
if len(usernames) != len(members):
202+
raise ServerClientError("Cannot add same user multiple times")
203+
200204
project = await get_project_model_by_name_or_error(
201205
session=session,
202206
project_name=project.name,
@@ -245,6 +249,10 @@ async def add_project_members(
245249
members: List[MemberSetting],
246250
):
247251
"""Add multiple members to a project."""
252+
usernames = {m.username for m in members}
253+
if len(usernames) != len(members):
254+
raise ServerClientError("Cannot add same user multiple times")
255+
248256
project = await get_project_model_by_name_or_error(
249257
session=session,
250258
project_name=project.name,
@@ -259,7 +267,10 @@ async def add_project_members(
259267
)
260268

261269
if not is_self_join_to_public:
262-
if requesting_user_role not in [ProjectRole.ADMIN, ProjectRole.MANAGER]:
270+
if user.global_role != GlobalRole.ADMIN and requesting_user_role not in [
271+
ProjectRole.ADMIN,
272+
ProjectRole.MANAGER,
273+
]:
263274
raise ForbiddenError("Access denied: insufficient permissions to add members")
264275

265276
if user.global_role != GlobalRole.ADMIN and requesting_user_role == ProjectRole.MANAGER:
@@ -272,8 +283,6 @@ async def add_project_members(
272283
if members[0].project_role != ProjectRole.USER:
273284
raise ForbiddenError("Access denied: can only join public projects as user role")
274285

275-
usernames = [member.username for member in members]
276-
277286
res = await session.execute(
278287
select(UserModel).where((UserModel.name.in_(usernames)) | (UserModel.email.in_(usernames)))
279288
)
@@ -628,7 +637,10 @@ async def remove_project_members(
628637
)
629638

630639
if not is_self_leave:
631-
if requesting_user_role not in [ProjectRole.ADMIN, ProjectRole.MANAGER]:
640+
if user.global_role != GlobalRole.ADMIN and requesting_user_role not in [
641+
ProjectRole.ADMIN,
642+
ProjectRole.MANAGER,
643+
]:
632644
raise ForbiddenError("Access denied: insufficient permissions to remove members")
633645

634646
res = await session.execute(

src/tests/_internal/server/routers/test_projects.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,37 @@ async def test_global_admin_manager_can_set_project_admins(
989989
members = res.scalars().all()
990990
assert len(members) == 2
991991

992+
@pytest.mark.asyncio
993+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
994+
async def test_cannot_set_same_user_twice(
995+
self, test_db, session: AsyncSession, client: AsyncClient
996+
):
997+
project = await create_project(session=session)
998+
user = await create_user(session=session, global_role=GlobalRole.ADMIN)
999+
user1 = await create_user(session=session, name="user1")
1000+
members = [
1001+
{
1002+
"username": user1.name,
1003+
"project_role": ProjectRole.ADMIN,
1004+
},
1005+
{
1006+
"username": user1.name,
1007+
"project_role": ProjectRole.ADMIN,
1008+
},
1009+
]
1010+
body = {"members": members}
1011+
response = await client.post(
1012+
f"/api/projects/{project.name}/set_members",
1013+
headers=get_auth_headers(user.token),
1014+
json=body,
1015+
)
1016+
assert response.status_code == 400
1017+
res = await session.execute(select(MemberModel))
1018+
members = res.scalars().all()
1019+
assert len(members) == 0
1020+
1021+
1022+
class TestAddProjectMembers:
9921023
@pytest.mark.asyncio
9931024
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
9941025
async def test_add_member_errors_on_nonexistent_user(
@@ -1053,6 +1084,35 @@ async def test_add_member_manager_cannot_add_admin_without_global_admin(
10531084

10541085
assert response.status_code == 403
10551086

1087+
@pytest.mark.asyncio
1088+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
1089+
async def test_cannot_add_same_user_twice(
1090+
self, test_db, session: AsyncSession, client: AsyncClient
1091+
):
1092+
project = await create_project(session=session)
1093+
user = await create_user(session=session, global_role=GlobalRole.ADMIN)
1094+
user1 = await create_user(session=session, name="user1")
1095+
members = [
1096+
{
1097+
"username": user1.name,
1098+
"project_role": ProjectRole.ADMIN,
1099+
},
1100+
{
1101+
"username": user1.name,
1102+
"project_role": ProjectRole.ADMIN,
1103+
},
1104+
]
1105+
body = {"members": members}
1106+
response = await client.post(
1107+
f"/api/projects/{project.name}/add_members",
1108+
headers=get_auth_headers(user.token),
1109+
json=body,
1110+
)
1111+
assert response.status_code == 400, response.json()
1112+
res = await session.execute(select(MemberModel))
1113+
members = res.scalars().all()
1114+
assert len(members) == 0
1115+
10561116

10571117
class TestUpdateProjectVisibility:
10581118
@pytest.mark.asyncio

0 commit comments

Comments
 (0)