Skip to content

Commit 792b8ec

Browse files
Fix project deletion permissions and member race conditions
1 parent 5ca77f9 commit 792b8ec

2 files changed

Lines changed: 10 additions & 28 deletions

File tree

src/dstack/_internal/server/security/permissions.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,8 @@ async def __call__(
207207
if user.global_role == GlobalRole.ADMIN:
208208
return user, project
209209

210-
# Project managers can remove members
210+
# Any project member can access (managers can remove others, members can leave)
211211
project_role = get_user_project_role(user=user, project=project)
212-
if project_role in [ProjectRole.ADMIN, ProjectRole.MANAGER]:
213-
return user, project
214-
215-
# Any project member can leave (will be validated in service layer)
216212
if project_role is not None:
217213
return user, project
218214

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

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -297,45 +297,35 @@ async def add_project_members(
297297
username_to_user = {user.name: user for user in users_found}
298298
email_to_user = {user.email: user for user in users_found if user.email}
299299

300-
# Build a set of current member IDs so we can quickly check if users are already members
301-
member_ids = {m.user_id for m in project.members}
302300
# Build a map from user_id to member for efficient existing member updates
303301
member_by_user_id = {m.user_id: m for m in project.members}
304302

305-
# Get current max member_num
306-
max_member_num = 0
307-
for member in project.members:
308-
if member.member_num is not None and member.member_num > max_member_num:
309-
max_member_num = member.member_num
310-
311303
# Process each member to add
312304
for member_setting in members:
313305
user_to_add = username_to_user.get(member_setting.username) or email_to_user.get(
314306
member_setting.username
315307
)
316308
if user_to_add is None:
317-
# Error on adding non-existing users instead of silently skipping
318309
raise ServerClientError(f"User not found: {member_setting.username}")
319310

320-
# Check if user is already a member using our fast lookup set
321-
if user_to_add.id in member_ids:
322-
# Update existing member role if different using our efficient lookup
311+
# Check if user is already a member
312+
if user_to_add.id in member_by_user_id:
313+
# Update existing member role if different
323314
existing_member = member_by_user_id[user_to_add.id]
324315
if existing_member.project_role != member_setting.project_role:
325316
existing_member.project_role = member_setting.project_role
326317
else:
327-
# Add new member
328-
max_member_num += 1
318+
# Add new member (let database handle member_num to avoid race conditions)
329319
await add_project_member(
330320
session=session,
331321
project=project,
332322
user=user_to_add,
333323
project_role=member_setting.project_role,
334-
member_num=max_member_num,
324+
member_num=None, # Let database auto-assign to avoid race conditions
335325
commit=False,
336326
)
337-
# Keep track of newly added members for subsequent iterations
338-
member_ids.add(user_to_add.id)
327+
# Update our local tracking for subsequent iterations
328+
member_by_user_id[user_to_add.id] = None # Placeholder to track addition
339329

340330
await session.commit()
341331

@@ -663,8 +653,6 @@ async def remove_project_members(
663653
username_to_user = {user.name: user for user in users_found}
664654
email_to_user = {user.email: user for user in users_found if user.email}
665655

666-
# Build a set of member IDs for faster membership checks
667-
member_user_ids = {m.user_id for m in project.members}
668656
# Build a map from user_id to member for efficient member lookups
669657
member_by_user_id = {m.user_id: m for m in project.members}
670658

@@ -675,15 +663,13 @@ async def remove_project_members(
675663
for username in usernames:
676664
user_to_remove = username_to_user.get(username) or email_to_user.get(username)
677665
if user_to_remove is None:
678-
# Error on removing non-existing users instead of silently skipping
679666
raise ServerClientError(f"User not found: {username}")
680667

681668
# Check if user is actually a member before trying to remove them
682-
if user_to_remove.id not in member_user_ids:
683-
# Error on removing non-members instead of silently skipping
669+
if user_to_remove.id not in member_by_user_id:
684670
raise ServerClientError(f"User is not a member of this project: {username}")
685671

686-
# Get the member to remove using our efficient lookup
672+
# Get the member to remove
687673
member_to_remove = member_by_user_id[user_to_remove.id]
688674

689675
# Check if trying to remove project admin

0 commit comments

Comments
 (0)