Skip to content

Commit ccc3e05

Browse files
authored
Validate usernames (#2514)
* Validate usernames * Fix tests
1 parent ddf53a7 commit ccc3e05

File tree

3 files changed

+64
-1
lines changed

3 files changed

+64
-1
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import hashlib
22
import os
3+
import re
34
import uuid
45
from datetime import timezone
56
from typing import Awaitable, Callable, List, Optional, Tuple
@@ -8,7 +9,7 @@
89
from sqlalchemy import func as safunc
910
from sqlalchemy.ext.asyncio import AsyncSession
1011

11-
from dstack._internal.core.errors import ResourceExistsError
12+
from dstack._internal.core.errors import ResourceExistsError, ServerClientError
1213
from dstack._internal.core.models.users import (
1314
GlobalRole,
1415
User,
@@ -78,6 +79,7 @@ async def create_user(
7879
active: bool = True,
7980
token: Optional[str] = None,
8081
) -> UserModel:
82+
validate_username(username)
8183
user_model = await get_user_model_by_name(session=session, username=username, ignore_case=True)
8284
if user_model is not None:
8385
raise ResourceExistsError()
@@ -225,6 +227,15 @@ def get_user_permissions(user_model: UserModel) -> UserPermissions:
225227
)
226228

227229

230+
def validate_username(username: str):
231+
if not is_valid_username(username):
232+
raise ServerClientError("Username should match regex '^[a-zA-Z0-9-_]{1,60}$'")
233+
234+
235+
def is_valid_username(username: str) -> bool:
236+
return re.match("^[a-zA-Z0-9-_]{1,60}$", username) is not None
237+
238+
228239
_CREATE_USER_HOOKS = []
229240

230241

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,29 @@ async def test_return_400_if_username_taken(
236236
)
237237
assert len(res.scalars().all()) == 1
238238

239+
@pytest.mark.asyncio
240+
@pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
241+
@freeze_time(datetime(2023, 1, 2, 3, 4, tzinfo=timezone.utc))
242+
async def test_returns_400_if_username_invalid(
243+
self,
244+
test_db,
245+
session: AsyncSession,
246+
client: AsyncClient,
247+
):
248+
user = await create_user(
249+
name="admin",
250+
session=session,
251+
)
252+
response = await client.post(
253+
"/api/users/create",
254+
headers=get_auth_headers(user.token),
255+
json={
256+
"username": "Invalid#$username",
257+
"global_role": GlobalRole.USER,
258+
},
259+
)
260+
assert response.status_code == 400
261+
239262

240263
class TestDeleteUsers:
241264
@pytest.mark.asyncio
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import pytest
2+
3+
from dstack._internal.server.services.users import is_valid_username
4+
5+
6+
class TestIsValidUsername:
7+
@pytest.mark.parametrize(
8+
"username",
9+
[
10+
"special#$symbols",
11+
"A,B",
12+
"",
13+
"a" * 61,
14+
],
15+
)
16+
def test_valid(self, username: str):
17+
assert not is_valid_username(username)
18+
19+
@pytest.mark.parametrize(
20+
"username",
21+
[
22+
"regularusername",
23+
"CaseUsername",
24+
"username_with_underscores-and-dashes1234",
25+
"a" * 60,
26+
],
27+
)
28+
def test_invalid(self, username: str):
29+
assert is_valid_username(username)

0 commit comments

Comments
 (0)