Improve SMB credential handling for AI structure
CI / python (push) Has been cancelled
CI / rust (push) Has been cancelled

This commit is contained in:
2026-05-22 00:20:04 +03:00
parent afb455a2c6
commit 23800dea71
2 changed files with 69 additions and 8 deletions
@@ -77,13 +77,21 @@ def _smbclient() -> Any:
def _register_session(smbclient: Any, *, server: str, username: str, password: str, domain: str | None) -> None: def _register_session(smbclient: Any, *, server: str, username: str, password: str, domain: str | None) -> None:
qualified_user = f"{domain}\\{username}" if domain else username effective_domain, effective_username = _normalize_credentials(username, domain)
qualified_user = f"{effective_domain}\\{effective_username}" if effective_domain else effective_username
try:
smbclient.register_session(server, username=qualified_user, password=password) smbclient.register_session(server, username=qualified_user, password=password)
except Exception as error: # pragma: no cover - depends on smb backend details
raise RuntimeError(_translate_smb_error(error, server=server, username=qualified_user)) from error
def _copy_smb_directory(smbclient: Any, source: str, target: Path) -> None: def _copy_smb_directory(smbclient: Any, source: str, target: Path) -> None:
target.mkdir(parents=True, exist_ok=True) target.mkdir(parents=True, exist_ok=True)
for item in smbclient.scandir(source): try:
items = list(smbclient.scandir(source))
except Exception as error: # pragma: no cover - depends on smb backend details
raise RuntimeError(_translate_smb_error(error, path=source)) from error
for item in items:
destination = target / item.name destination = target / item.name
child_source = f"{source}\\{item.name}" child_source = f"{source}\\{item.name}"
try: try:
@@ -98,9 +106,12 @@ def _copy_smb_directory(smbclient: Any, source: str, target: Path) -> None:
def _copy_local_file_to_smb(smbclient: Any, source: Path, target: str) -> None: def _copy_local_file_to_smb(smbclient: Any, source: Path, target: str) -> None:
try:
with source.open("rb") as local_file: with source.open("rb") as local_file:
with smbclient.open_file(target, mode="wb") as remote_file: with smbclient.open_file(target, mode="wb") as remote_file:
shutil.copyfileobj(local_file, remote_file, length=1024 * 1024) shutil.copyfileobj(local_file, remote_file, length=1024 * 1024)
except Exception as error: # pragma: no cover - depends on smb backend details
raise RuntimeError(_translate_smb_error(error, path=target)) from error
def _ensure_smb_directory(smbclient: Any, path: str) -> None: def _ensure_smb_directory(smbclient: Any, path: str) -> None:
@@ -115,9 +126,9 @@ def _ensure_smb_directory(smbclient: Any, path: str) -> None:
_ensure_smb_directory(smbclient, parent) _ensure_smb_directory(smbclient, parent)
try: try:
smbclient.mkdir(normalized) smbclient.mkdir(normalized)
except OSError: except OSError as error:
if not smbclient.path.isdir(normalized): if not smbclient.path.isdir(normalized):
raise raise RuntimeError(_translate_smb_error(error, path=normalized)) from error
def _unc_parent_path(path: str) -> str | None: def _unc_parent_path(path: str) -> str | None:
@@ -129,3 +140,31 @@ def _unc_parent_path(path: str) -> str | None:
return f"\\\\{server}\\{share}" return f"\\\\{server}\\{share}"
parent_relative = "\\".join(parts[:-1]) parent_relative = "\\".join(parts[:-1])
return f"\\\\{server}\\{share}\\{parent_relative}" return f"\\\\{server}\\{share}\\{parent_relative}"
def _normalize_credentials(username: str, domain: str | None) -> tuple[str | None, str]:
raw_username = username.strip()
raw_domain = (domain or "").strip() or None
if "\\" in raw_username:
embedded_domain, embedded_username = raw_username.split("\\", 1)
return embedded_domain.strip() or raw_domain, embedded_username.strip()
if "@" in raw_username and not raw_domain:
embedded_username, embedded_domain = raw_username.split("@", 1)
return embedded_domain.strip() or None, embedded_username.strip()
return raw_domain, raw_username
def _translate_smb_error(error: Exception, *, server: str | None = None, path: str | None = None, username: str | None = None) -> str:
message = str(error).strip()
lowered = message.casefold()
target = path or server or "сетевой ресурс"
if any(token in lowered for token in ["logon failure", "access denied", "authentication", "STATUS_LOGON_FAILURE".casefold(), "STATUS_ACCESS_DENIED".casefold()]):
user_part = f" для пользователя {username}" if username else ""
return f"Ошибка авторизации SMB{user_part}. Проверьте логин, пароль, домен и права доступа к {target}."
if any(token in lowered for token in ["bad network name", "object name not found", "path not found", "no such file", "STATUS_OBJECT_PATH_NOT_FOUND".casefold(), "STATUS_BAD_NETWORK_NAME".casefold()]):
return f"Сетевой путь не найден или недоступен: {target}."
if any(token in lowered for token in ["connection reset", "connection refused", "timed out", "host is down", "network name deleted", "network path was not found"]):
return f"Не удалось подключиться к сетевому ресурсу {target}. Проверьте доступность сервера и сети."
if message:
return f"Ошибка SMB при обращении к {target}: {message}"
return f"Неизвестная ошибка SMB при обращении к {target}."
+22
View File
@@ -1310,6 +1310,28 @@ def test_server_smb_browse_validates_unc_path():
assert "UNC путь" in response.json()["error"] assert "UNC путь" in response.json()["error"]
def test_smb_credentials_embedded_domain_format_is_normalized():
from api_server import smb_paths
domain, username = smb_paths._normalize_credentials("MST\\m", None)
assert domain == "MST"
assert username == "m"
def test_smb_error_translation_returns_russian_auth_message():
from api_server import smb_paths
message = smb_paths._translate_smb_error(
RuntimeError("STATUS_LOGON_FAILURE: logon failure"),
server="192.168.220.200",
username="MST\\m",
)
assert "Ошибка авторизации SMB" in message
assert "Проверьте логин, пароль, домен" in message
def test_project_setup_recovers_indexed_status_from_stored_snapshot(tmp_path: Path): def test_project_setup_recovers_indexed_status_from_stored_snapshot(tmp_path: Path):
(tmp_path / "metadata.xml").write_text( (tmp_path / "metadata.xml").write_text(
""" """