Sur une machine sans tkinter — tout serveur — `get_kdbx()` rendait None en journalisant « pykeepass is not installed », alors que pykeepass était là. Les deux imports partageaient un seul `try` : l'absence de tkinter mettait aussi `PyKeePass` à None, et le coffre restait inouvrable même avec chemin et mot de passe configurés. tkinter ne sert qu'au sélecteur de fichier, quand aucun chemin n'est configuré ; deux blocs séparés le rendent à ce seul rôle. --- EN --- On a machine without tkinter — every server — `get_kdbx()` returned None while logging "pykeepass is not installed", though pykeepass was present. Both imports shared one `try`: a missing tkinter set `PyKeePass` to None as well, and the vault stayed unopenable even with path and password configured. tkinter serves only the file picker, when no path is configured; two separate blocks give it back that single role. Assisted-by: Claude Opus 5
144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
import getpass
|
|
import logging
|
|
|
|
from script.todo.todo_i18n import t
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
# DEUX blocs, et c'est le point : tkinter ne sert QU'au sélecteur de fichier
|
|
# quand aucun chemin n'est configuré. Réunis dans un seul `try`, l'absence de
|
|
# tkinter mettait aussi `PyKeePass` à None — et le coffre devenait impossible
|
|
# à ouvrir sur toute machine sans interface graphique, chemin et mot de passe
|
|
# configurés ou non. C'est-à-dire sur tous les serveurs.
|
|
try:
|
|
from pykeepass import PyKeePass
|
|
from pykeepass.exceptions import CredentialsError
|
|
except ModuleNotFoundError:
|
|
PyKeePass = None
|
|
|
|
class CredentialsError(Exception):
|
|
"""Jamais levée ici : sans pykeepass, `get_kdbx` sort avant d'ouvrir
|
|
quoi que ce soit. Définie pour que le `except` reste écrivable."""
|
|
|
|
|
|
try:
|
|
import tkinter as tk
|
|
from tkinter import filedialog
|
|
except ModuleNotFoundError:
|
|
tk = None
|
|
filedialog = None
|
|
|
|
|
|
class KdbxManager:
|
|
def __init__(self, config_file) -> None:
|
|
self._config_file = config_file
|
|
self._kdbx = None
|
|
|
|
def get_kdbx(self, attempts: int = 3):
|
|
if self._kdbx:
|
|
return self._kdbx
|
|
|
|
kdbx_file_path = self._config_file.get_config_value(["kdbx", "path"])
|
|
if not kdbx_file_path:
|
|
if tk is None:
|
|
_logger.error("tkinter is not available")
|
|
return None
|
|
root = tk.Tk()
|
|
root.withdraw()
|
|
kdbx_file_path = filedialog.askopenfilename(
|
|
title="Select a File",
|
|
filetypes=(("KeepassX files", "*.kdbx"),),
|
|
)
|
|
if not kdbx_file_path:
|
|
_logger.error(
|
|
"KDBX is not configured, please fill"
|
|
f" {self._config_file.CONFIG_FILE}"
|
|
)
|
|
return None
|
|
|
|
if PyKeePass is None:
|
|
_logger.error("pykeepass is not installed")
|
|
return None
|
|
|
|
kdbx_password = self._config_file.get_config_value(
|
|
["kdbx", "password"]
|
|
)
|
|
if kdbx_password:
|
|
# Mot de passe pris dans la configuration : personne à qui
|
|
# redemander, mais il peut être faux — le dire au lieu de
|
|
# laisser remonter une trace de la bibliothèque.
|
|
try:
|
|
self._kdbx = PyKeePass(kdbx_file_path, password=kdbx_password)
|
|
except CredentialsError:
|
|
print(t("kdbx_wrong_password"))
|
|
return None
|
|
return self._kdbx
|
|
|
|
# Saisie interactive. Un mot de passe refusé est le cas NORMAL ici,
|
|
# pas une panne : la bibliothèque lève `CredentialsError` et, sans
|
|
# ce rattrapage, la trace remontait jusqu'à tuer le CLI. On nomme
|
|
# aussi le coffre — l'invite ne disait pas DE QUOI elle parlait.
|
|
print(f"{t('kdbx_vault_is')} {kdbx_file_path}")
|
|
for _ in range(attempts):
|
|
password = getpass.getpass(prompt=t("kdbx_ask_password"))
|
|
if not password:
|
|
print(t("kdbx_give_up"))
|
|
return None
|
|
try:
|
|
self._kdbx = PyKeePass(kdbx_file_path, password=password)
|
|
except CredentialsError:
|
|
print(t("kdbx_wrong_password"))
|
|
continue
|
|
return self._kdbx
|
|
print(t("kdbx_give_up"))
|
|
return None
|
|
|
|
def get_extra_command_user(
|
|
self, kdbx_key: str | list | None
|
|
) -> tuple[str | list, dict]:
|
|
"""(fragments de commande, variables d'environnement à poser).
|
|
|
|
Le mot de passe ne rejoint PAS la ligne de commande : seul le NOM
|
|
d'une variable y figure. /proc/<pid>/cmdline est lisible par tout
|
|
utilisateur de la machine, /proc/<pid>/environ par son seul
|
|
propriétaire — et un mot de passe KeePass n'a rien à faire dans la
|
|
liste des processus.
|
|
|
|
Un nom par entrée : plusieurs identifiants partent dans UNE seule
|
|
commande « parallel », donc une variable unique ne suffirait pas.
|
|
"""
|
|
values = []
|
|
env = {}
|
|
if kdbx_key:
|
|
kp = self.get_kdbx()
|
|
if not kp:
|
|
return "", {}
|
|
if type(kdbx_key) is not list:
|
|
kdbx_keys = [kdbx_key]
|
|
else:
|
|
kdbx_keys = kdbx_key
|
|
for key in kdbx_keys:
|
|
entry = kp.find_entries_by_title(key, first=True)
|
|
try:
|
|
odoo_user = entry.username
|
|
except AttributeError:
|
|
_logger.error(f"Cannot find username from keys {key}")
|
|
try:
|
|
odoo_password = entry.password
|
|
except AttributeError:
|
|
_logger.error(f"Cannot find password from keys {key}")
|
|
var = f"EL_WEB_LOGIN_PWD_{len(values)}"
|
|
env[var] = odoo_password
|
|
values.append(
|
|
" --default_email_auth"
|
|
f" {odoo_user} --default_password_auth_env {var}"
|
|
)
|
|
if len(values) == 0:
|
|
return "", {}
|
|
elif len(values) == 1:
|
|
return values[0], env
|
|
return values, env
|