2026-03-10 03:59:35 -04:00
|
|
|
#!/usr/bin/env python3
|
2026-03-11 23:15:07 -04:00
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
2026-03-10 03:59:35 -04:00
|
|
|
# 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__)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
from tkinter import filedialog
|
|
|
|
|
|
|
|
|
|
from pykeepass import PyKeePass
|
2026-08-10 03:10:50 -04:00
|
|
|
from pykeepass.exceptions import CredentialsError
|
2026-03-10 03:59:35 -04:00
|
|
|
except ModuleNotFoundError:
|
|
|
|
|
PyKeePass = None
|
|
|
|
|
tk = None
|
|
|
|
|
filedialog = None
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
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."""
|
|
|
|
|
|
2026-03-10 03:59:35 -04:00
|
|
|
|
|
|
|
|
class KdbxManager:
|
2026-03-10 04:05:04 -04:00
|
|
|
def __init__(self, config_file) -> None:
|
2026-03-10 03:59:35 -04:00
|
|
|
self._config_file = config_file
|
|
|
|
|
self._kdbx = None
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
def get_kdbx(self, attempts: int = 3):
|
2026-03-10 03:59:35 -04:00
|
|
|
if self._kdbx:
|
|
|
|
|
return self._kdbx
|
|
|
|
|
|
2026-03-10 04:05:04 -04:00
|
|
|
kdbx_file_path = self._config_file.get_config_value(["kdbx", "path"])
|
2026-03-10 03:59:35 -04:00
|
|
|
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
|
|
|
|
|
|
2026-08-10 03:10:50 -04:00
|
|
|
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
|
2026-03-10 03:59:35 -04:00
|
|
|
|
2026-03-10 04:05:04 -04:00
|
|
|
def get_extra_command_user(
|
|
|
|
|
self, kdbx_key: str | list | None
|
|
|
|
|
) -> str | list:
|
2026-03-10 03:59:35 -04:00
|
|
|
values = []
|
|
|
|
|
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:
|
2026-03-10 04:05:04 -04:00
|
|
|
_logger.error(f"Cannot find username from keys {key}")
|
2026-03-10 03:59:35 -04:00
|
|
|
try:
|
|
|
|
|
odoo_password = entry.password
|
|
|
|
|
except AttributeError:
|
2026-03-10 04:05:04 -04:00
|
|
|
_logger.error(f"Cannot find password from keys {key}")
|
2026-03-10 03:59:35 -04:00
|
|
|
values.append(
|
|
|
|
|
" --default_email_auth"
|
|
|
|
|
f" {odoo_user} --default_password_auth"
|
|
|
|
|
f" '{odoo_password}'"
|
|
|
|
|
)
|
|
|
|
|
if len(values) == 0:
|
|
|
|
|
return ""
|
|
|
|
|
elif len(values) == 1:
|
|
|
|
|
return values[0]
|
|
|
|
|
return values
|