11import hashlib
22import os
33import re
4+ import secrets
45import uuid
56from typing import Awaitable , Callable , List , Optional , Tuple
67
78from sqlalchemy import delete , select , update
89from sqlalchemy import func as safunc
910from sqlalchemy .ext .asyncio import AsyncSession
11+ from sqlalchemy .orm import load_only
1012
1113from dstack ._internal .core .errors import ResourceExistsError , ServerClientError
1214from dstack ._internal .core .models .users import (
1719 UserTokenCreds ,
1820 UserWithCreds ,
1921)
20- from dstack ._internal .server .models import DecryptedString , UserModel
22+ from dstack ._internal .server .models import DecryptedString , MemberModel , UserModel
2123from dstack ._internal .server .services .permissions import get_default_permissions
2224from dstack ._internal .server .utils .routers import error_forbidden
2325from dstack ._internal .utils import crypto
24- from dstack ._internal .utils .common import run_async
26+ from dstack ._internal .utils .common import get_current_datetime , get_or_error , run_async
2527from dstack ._internal .utils .logging import get_logger
2628
2729logger = get_logger (__name__ )
@@ -53,8 +55,12 @@ async def list_users_for_user(
5355
5456async def list_all_users (
5557 session : AsyncSession ,
58+ include_deleted : bool = False ,
5659) -> List [User ]:
57- res = await session .execute (select (UserModel ))
60+ filters = []
61+ if not include_deleted :
62+ filters .append (UserModel .deleted == False )
63+ res = await session .execute (select (UserModel ).where (* filters ))
5864 user_models = res .scalars ().all ()
5965 user_models = sorted (user_models , key = lambda u : u .created_at )
6066 return [user_model_to_user (u ) for u in user_models ]
@@ -116,7 +122,10 @@ async def update_user(
116122) -> UserModel :
117123 await session .execute (
118124 update (UserModel )
119- .where (UserModel .name == username )
125+ .where (
126+ UserModel .name == username ,
127+ UserModel .deleted == False ,
128+ )
120129 .values (
121130 global_role = global_role ,
122131 email = email ,
@@ -138,7 +147,10 @@ async def refresh_ssh_key(
138147 private_bytes , public_bytes = await run_async (crypto .generate_rsa_key_pair_bytes , username )
139148 await session .execute (
140149 update (UserModel )
141- .where (UserModel .name == username )
150+ .where (
151+ UserModel .name == username ,
152+ UserModel .deleted == False ,
153+ )
142154 .values (
143155 ssh_private_key = private_bytes .decode (),
144156 ssh_public_key = public_bytes .decode (),
@@ -158,7 +170,10 @@ async def refresh_user_token(
158170 new_token = str (uuid .uuid4 ())
159171 await session .execute (
160172 update (UserModel )
161- .where (UserModel .name == username )
173+ .where (
174+ UserModel .name == username ,
175+ UserModel .deleted == False ,
176+ )
162177 .values (
163178 token = DecryptedString (plaintext = new_token ),
164179 token_hash = get_token_hash (new_token ),
@@ -173,7 +188,37 @@ async def delete_users(
173188 user : UserModel ,
174189 usernames : List [str ],
175190):
176- await session .execute (delete (UserModel ).where (UserModel .name .in_ (usernames )))
191+ if _ADMIN_USERNAME in usernames :
192+ raise ServerClientError ("User 'admin' cannot be deleted" )
193+
194+ res = await session .execute (
195+ select (UserModel )
196+ .where (
197+ UserModel .name .in_ (usernames ),
198+ UserModel .deleted == False ,
199+ )
200+ .options (load_only (UserModel .id , UserModel .name ))
201+ )
202+ users = res .scalars ().all ()
203+ if len (users ) != len (usernames ):
204+ raise ServerClientError ("Failed to delete non-existent users" )
205+
206+ user_ids = [u .id for u in users ]
207+ timestamp = str (int (get_current_datetime ().timestamp ()))
208+ updates = []
209+ for u in users :
210+ updates .append (
211+ {
212+ "id" : u .id ,
213+ "name" : f"_deleted_{ timestamp } _{ secrets .token_hex (8 )} " ,
214+ "original_name" : u .name ,
215+ "deleted" : True ,
216+ "active" : False ,
217+ }
218+ )
219+ await session .execute (update (UserModel ), updates )
220+ await session .execute (delete (MemberModel ).where (MemberModel .user_id .in_ (user_ids )))
221+ # Projects are not deleted automatically if owners are deleted.
177222 await session .commit ()
178223 logger .info ("Deleted users %s by user %s" , usernames , user .name )
179224
@@ -183,7 +228,7 @@ async def get_user_model_by_name(
183228 username : str ,
184229 ignore_case : bool = False ,
185230) -> Optional [UserModel ]:
186- filters = []
231+ filters = [UserModel . deleted == False ]
187232 if ignore_case :
188233 filters .append (safunc .lower (UserModel .name ) == safunc .lower (username ))
189234 else :
@@ -192,9 +237,14 @@ async def get_user_model_by_name(
192237 return res .scalar ()
193238
194239
195- async def get_user_model_by_name_or_error (session : AsyncSession , username : str ) -> UserModel :
196- res = await session .execute (select (UserModel ).where (UserModel .name == username ))
197- return res .scalar_one ()
240+ async def get_user_model_by_name_or_error (
241+ session : AsyncSession ,
242+ username : str ,
243+ ignore_case : bool = False ,
244+ ) -> UserModel :
245+ return get_or_error (
246+ await get_user_model_by_name (session = session , username = username , ignore_case = ignore_case )
247+ )
198248
199249
200250async def log_in_with_token (session : AsyncSession , token : str ) -> Optional [UserModel ]:
@@ -203,6 +253,7 @@ async def log_in_with_token(session: AsyncSession, token: str) -> Optional[UserM
203253 select (UserModel ).where (
204254 UserModel .token_hash == token_hash ,
205255 UserModel .active == True ,
256+ UserModel .deleted == False ,
206257 )
207258 )
208259 user = res .scalar ()
0 commit comments