From 5682ac96b6e85a9d58b8ba2170a4adc1e7da5765 Mon Sep 17 00:00:00 2001 From: Emmanuel Garette Date: Wed, 5 Feb 2025 11:30:51 +0100 Subject: [PATCH] feat: first version --- locale/fr/LC_MESSAGES/user_data_yaml.po | 49 +++++++++ locale/rougail_user_data_yaml.pot | 49 +++++++++ pyproject.toml | 40 +++++++ src/rougail/user_data_bitwarden/__init__.py | 3 + src/rougail/user_data_bitwarden/config.py | 32 ++++++ src/rougail/user_data_bitwarden/data.py | 95 ++++++++++++++++ src/rougail/user_data_bitwarden/i18n.py | 26 +++++ .../locale/fr/LC_MESSAGES/user_data_yaml.mo | Bin 0 -> 1502 bytes .../test/00_6secret/errors/bitwarden.json | 7 ++ .../test/00_6secret/makedict/bitwarden.json | 4 + .../00_6secret/errors/bitwarden.json | 7 ++ .../00_6secret/makedict/bitwarden.json | 4 + tests/test_load.py | 103 ++++++++++++++++++ 13 files changed, 419 insertions(+) create mode 100644 locale/fr/LC_MESSAGES/user_data_yaml.po create mode 100644 locale/rougail_user_data_yaml.pot create mode 100644 pyproject.toml create mode 100644 src/rougail/user_data_bitwarden/__init__.py create mode 100644 src/rougail/user_data_bitwarden/config.py create mode 100644 src/rougail/user_data_bitwarden/data.py create mode 100644 src/rougail/user_data_bitwarden/i18n.py create mode 100644 src/rougail/user_data_yaml/locale/fr/LC_MESSAGES/user_data_yaml.mo create mode 100644 tests/results/test/00_6secret/errors/bitwarden.json create mode 100644 tests/results/test/00_6secret/makedict/bitwarden.json create mode 100644 tests/results/test_namespace/00_6secret/errors/bitwarden.json create mode 100644 tests/results/test_namespace/00_6secret/makedict/bitwarden.json create mode 100644 tests/test_load.py diff --git a/locale/fr/LC_MESSAGES/user_data_yaml.po b/locale/fr/LC_MESSAGES/user_data_yaml.po new file mode 100644 index 0000000..bea5f40 --- /dev/null +++ b/locale/fr/LC_MESSAGES/user_data_yaml.po @@ -0,0 +1,49 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2025-02-05 11:18+0100\n" +"PO-Revision-Date: 2025-02-05 11:21+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" +"X-Generator: Poedit 3.5\n" + +#: src/rougail/user_data_bitwarden/data.py:50 +msgid "\"bitwarden\" is not set in step.user_data" +msgstr "\"bitwarden\" n'est pas dans step.user_data" + +#: src/rougail/user_data_bitwarden/data.py:67 +msgid "the default value for \"{0}\" must be the Bitwarden password name" +msgstr "la valeur par défaut pour \"{0}\" doit être un nom de mot de passe de Bitwarden" + +#: src/rougail/user_data_bitwarden/data.py:72 +msgid "cannot execute the \"bw\" commandline from Bitwarden for \"{0}\": {1}" +msgstr "ne peut exécuter la ligne de commande \"bw\" pour Bitwarden pour \"{0}\": {1}" + +#: src/rougail/user_data_bitwarden/data.py:77 +msgid "cannot get password \"{0}\" from Bitwarden for \"{1}\": {2} ({3})" +msgstr "ne peut récupérer le mot de passe \"{0}\" pour Bitwarden pour \"{1}\": {2} ({3})" + +#: src/rougail/user_data_bitwarden/data.py:82 +msgid "cannot load password \"{0}\" from Bitwarden for \"{1}\": {2}" +msgstr "ne peut charger le mot de passe \"{0}\" pour Bitwarden pour \"{1}\": {2}" + +#: src/rougail/user_data_bitwarden/data.py:85 +msgid "cannot find password \"{0}\" from Bitwarden for \"{1}\"" +msgstr "ne peut trouver le mot de passe \"{0}\" pour Bitwarden pour \"{1}\"" + +#: src/rougail/user_data_bitwarden/data.py:89 +msgid "several passwords found with name \"{0}\" from Bitwarden for \"{1}\": \"{2}\"" +msgstr "plusieurs mots de passe trouvés avec le nom \"{0}\" pour Bitwarden pour \"{1}\": \"{2}\"" + +#: src/rougail/user_data_bitwarden/data.py:94 +msgid "unexpected password \"{0}\" from Bitwarden for \"{1}\": {2}" +msgstr "mot de passe inattendu \"{0}\" pour Bitwarden pour \"{1}\": {2}" diff --git a/locale/rougail_user_data_yaml.pot b/locale/rougail_user_data_yaml.pot new file mode 100644 index 0000000..9f55612 --- /dev/null +++ b/locale/rougail_user_data_yaml.pot @@ -0,0 +1,49 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2025-02-05 11:21+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + + +#: src/rougail/user_data_bitwarden/data.py:50 +msgid "\"bitwarden\" is not set in step.user_data" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:67 +msgid "the default value for \"{0}\" must be the Bitwarden password name" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:72 +msgid "cannot execute the \"bw\" commandline from Bitwarden for \"{0}\": {1}" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:77 +msgid "cannot get password \"{0}\" from Bitwarden for \"{1}\": {2} ({3})" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:82 +msgid "cannot load password \"{0}\" from Bitwarden for \"{1}\": {2}" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:85 +msgid "cannot find password \"{0}\" from Bitwarden for \"{1}\"" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:89 +msgid "several passwords found with name \"{0}\" from Bitwarden for \"{1}\": \"{2}\"" +msgstr "" + +#: src/rougail/user_data_bitwarden/data.py:94 +msgid "unexpected password \"{0}\" from Bitwarden for \"{1}\": {2}" +msgstr "" + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1dc0781 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +build-backend = "flit_core.buildapi" +requires = ["flit_core >=3.8.0,<4"] + +[project] +name = "rougail.user_data_bitwarden" +version = "0.0.0" +authors = [{name = "Emmanuel Garette", email = "gnunux@gnunux.info"}] +readme = "README.md" +description = "Rougail user_data Bitwarden" +requires-python = ">=3.8" +license = {file = "LICENSE"} +classifiers = [ + "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Natural Language :: English", + "Natural Language :: French", + +] +dependencies = [ + "rougail >= 1.1,<2", +] + +[project.urls] +Home = "https://forge.cloud.silique.fr/stove/rougail-user-data-bitwarden" + +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "pep621" +update_changelog_on_bump = true +changelog_merge_prerelease = true diff --git a/src/rougail/user_data_bitwarden/__init__.py b/src/rougail/user_data_bitwarden/__init__.py new file mode 100644 index 0000000..237b084 --- /dev/null +++ b/src/rougail/user_data_bitwarden/__init__.py @@ -0,0 +1,3 @@ +from .data import RougailUserDataBitwarden +RougailUserData = RougailUserDataBitwarden +__all__ = ('RougailUserDataBitwarden',) diff --git a/src/rougail/user_data_bitwarden/config.py b/src/rougail/user_data_bitwarden/config.py new file mode 100644 index 0000000..08c1b7c --- /dev/null +++ b/src/rougail/user_data_bitwarden/config.py @@ -0,0 +1,32 @@ +""" +Silique (https://www.silique.fr) +Copyright (C) 2025 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + + +def get_rougail_config(*, + backward_compatibility=True, + ) -> dict: + return {'name': 'bitwarden', + 'process': 'user data', + 'level': 90, + } + + +__all__ = ('get_rougail_config',) diff --git a/src/rougail/user_data_bitwarden/data.py b/src/rougail/user_data_bitwarden/data.py new file mode 100644 index 0000000..90a9d1b --- /dev/null +++ b/src/rougail/user_data_bitwarden/data.py @@ -0,0 +1,95 @@ +""" +Silique (https://www.silique.fr) +Copyright (C) 2025 + +distribued with GPL-2 or later license + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + +from subprocess import run +from json import loads + + +from rougail.error import ExtentionError +from .i18n import _ + + +class RougailUserDataBitwarden: + force_apply_user_data = True + + def __init__(self, + config: 'Config', + *, + rougailconfig: "RougailConfig"=None, + ): + # this is the tiramisu config object + self.config = config + if rougailconfig is None: + from rougail.config import RougailConfig + rougailconfig = RougailConfig + user_data = rougailconfig['step.user_data'] + if 'bitwarden' not in user_data: + user_data.append('bitwarden') + rougailconfig['step.user_data'] = user_data + user_data = rougailconfig['step.user_data'] + self.rougailconfig = rougailconfig + if 'bitwarden' not in user_data: + raise ExtentionError(_('"bitwarden" is not set in step.user_data')) + self.errors = [] + self.warnings = [] + + def run(self): + self.set_passwords(self.config) + return {'errors': self.errors, + 'warnings': self.warnings, + } + + def set_passwords(self, optiondescription): + for option in optiondescription: + if option.isoptiondescription(): + self.set_passwords(option) + elif option.owner.isdefault() and option.type() == 'password': + key_bitwarden = option.value.get() + if not isinstance(key_bitwarden, str): + self.errors.append(_('the default value for "{0}" must be the Bitwarden password name').format(option.path())) + continue + try: + cpe = run(["bw", "list", "items", "--search", key_bitwarden, '--nointeraction'], capture_output=True) + except Exception as exc: + self.errors.append(_('cannot execute the "bw" commandline from Bitwarden for "{0}": {1}').format(option.path(), exc)) + continue + out = cpe.stdout.decode('utf8') + err = cpe.stderr.decode('utf8') + if cpe.returncode != 0 or err: + self.errors.append(_('cannot get password "{0}" from Bitwarden for "{1}": {2} ({3})').format(key_bitwarden, option.path(), err, cpe.returncode)) + continue + try: + data = loads(out) + except Exception as exc: + self.errors.append(_('cannot load password "{0}" from Bitwarden for "{1}": {2}').format(key_bitwarden, option.path(), exc)) + continue + if not data: + self.errors.append(_('cannot find password "{0}" from Bitwarden for "{1}"').format(key_bitwarden, option.path())) + continue + if len(data) != 1: + names = [d["name"] for d in data] + self.errors.append(_('several passwords found with name "{0}" from Bitwarden for "{1}": "{2}"').format(key_bitwarden, option.path(), "\", \"".join(names))) + continue + try: + option.value.set(data[0]['login']['password']) + except Exception as exc: + self.errors.append(_('unexpected password "{0}" from Bitwarden for "{1}": {2}').format(key_bitwarden, option.path(), exc)) + continue diff --git a/src/rougail/user_data_bitwarden/i18n.py b/src/rougail/user_data_bitwarden/i18n.py new file mode 100644 index 0000000..fb05a74 --- /dev/null +++ b/src/rougail/user_data_bitwarden/i18n.py @@ -0,0 +1,26 @@ +""" +Silique (https://www.silique.fr) +Copyright (C) 2025 + +This program is free software: you can redistribute it and/or modify it +under the terms of the GNU Lesser General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . +""" + +from gettext import translation +from pathlib import Path + +t = translation( + "rougail_user_data_bitwarden", str(Path(__file__).parent / "locale"), fallback=True +) + +_ = t.gettext diff --git a/src/rougail/user_data_yaml/locale/fr/LC_MESSAGES/user_data_yaml.mo b/src/rougail/user_data_yaml/locale/fr/LC_MESSAGES/user_data_yaml.mo new file mode 100644 index 0000000000000000000000000000000000000000..55f1aabdb1288f8809f30a66eeb6e63a0e6f2403 GIT binary patch literal 1502 zcmbVLT~8B16de`6n)u?g4>x@QG+TE|h-3*NB1kYG1kw0rhVE@yvYpw?&bBn9(LdoI z@U6yIV|?+Y|HF@8;giNQEnA9#2u^Zyc6R2T`!VN!y*=?(VBLh>fkm)eun#b|zQd+q zKVT1GrAtE00M~&Fz&F4-;1}R3@F#E)xOiEJ2f)|BU%*emY2e2zLOcO}1G;;6Cxmzk z+y^cLKLeiue*-sx+gF8{2fhP>THJtvO5k{qGk%L}Ft1I=^D2WwQ-RLGQ6@*mRhRTP2 zw;a-8&@CA1v5F{_S=QDj8lIo-7<9{pPXj5P(*?1rU79+a=}s5kiI(S%IB*vkx7f&J z6f;9DInr*&c8`>7qIOr;nR$meuOfZdrHJb?Pb{@$lA}dMPpFw^mUf4oJnq@3N*6`s ziVsq*S-xQYTSo80&0CAS7i_XvtB^`to8C6JVz={_rR9*Ses$LKtDZkgK@e7hdw$^i zrFEHEZ^uZLCDLjG&vmI9xoohv!?HQH57B_q#@fcpag?CqmmXY8>ytmW|DeXW7j;xY)D5%Vq zUV29tT#j3squA06{)=%73?j;Xt#vPS0+4<$@Z%nNATlNN$Gox~}<@HZ}#wBIuq=NOlMLFYQ&_)jX4CV3Wf zZZcO2I#|>)sIlM6NVd4K;Y6HSU@;PCFeU+{=!>DK-@|m^fn0Nk43Wk=_TO8> el&b+V;+|+F_M?FjCr)A|ArTej|IP;UDgFS{M#)A1 literal 0 HcmV?d00001 diff --git a/tests/results/test/00_6secret/errors/bitwarden.json b/tests/results/test/00_6secret/errors/bitwarden.json new file mode 100644 index 0000000..1563bb3 --- /dev/null +++ b/tests/results/test/00_6secret/errors/bitwarden.json @@ -0,0 +1,7 @@ +{ + "errors": [ + "the default value for \"secret1\" must be the Bitwarden password name", + "cannot get password \"value\" from Bitwarden for \"secret2\": Vault is locked. (1)" + ], + "warnings": [] +} \ No newline at end of file diff --git a/tests/results/test/00_6secret/makedict/bitwarden.json b/tests/results/test/00_6secret/makedict/bitwarden.json new file mode 100644 index 0000000..98ad1d3 --- /dev/null +++ b/tests/results/test/00_6secret/makedict/bitwarden.json @@ -0,0 +1,4 @@ +{ + "secret1": null, + "secret2": "value" +} \ No newline at end of file diff --git a/tests/results/test_namespace/00_6secret/errors/bitwarden.json b/tests/results/test_namespace/00_6secret/errors/bitwarden.json new file mode 100644 index 0000000..5759673 --- /dev/null +++ b/tests/results/test_namespace/00_6secret/errors/bitwarden.json @@ -0,0 +1,7 @@ +{ + "errors": [ + "the default value for \"rougail.secret1\" must be the Bitwarden password name", + "cannot get password \"value\" from Bitwarden for \"rougail.secret2\": Vault is locked. (1)" + ], + "warnings": [] +} \ No newline at end of file diff --git a/tests/results/test_namespace/00_6secret/makedict/bitwarden.json b/tests/results/test_namespace/00_6secret/makedict/bitwarden.json new file mode 100644 index 0000000..d21a23e --- /dev/null +++ b/tests/results/test_namespace/00_6secret/makedict/bitwarden.json @@ -0,0 +1,4 @@ +{ + "rougail.secret1": null, + "rougail.secret2": "value" +} \ No newline at end of file diff --git a/tests/test_load.py b/tests/test_load.py new file mode 100644 index 0000000..4df4829 --- /dev/null +++ b/tests/test_load.py @@ -0,0 +1,103 @@ +import os +from pytest import fixture # , raises +from pathlib import Path +from rougail import Rougail +######################### +from dotenv import load_dotenv +from rougail.user_data_bitwarden import RougailUserDataBitwarden as RougailUserData +from json import load, dump +######################### + +from rougail_tests.utils import get_structures_list, get_rougail_config, get_values_for_config, config_to_dict + +EXT = "env" + +######################### +#let's save the original environment +save = os.environ.copy() +######################### + + +excludes = [] + +test_ok = get_structures_list(excludes) +test_ok = [Path('../rougail-tests/structures/00_6secret')] + + +def idfn(fixture_value): + return fixture_value.name + + +@fixture(scope="module", params=test_ok, ids=idfn) +def test_dir(request): + return request.param + + +def _test_dictionaries(test_dir, namespace, ext): + rougailconfig = get_rougail_config(test_dir, namespace) + if not rougailconfig: + return + ################################## + rougailconfig['step.user_data'] = ['bitwarden'] + ################################## + dir_name = 'test' + if namespace: + dir_name += '_namespace' + elif (test_dir / 'force_namespace').is_file(): + return + rougail = Rougail(rougailconfig) + config = rougail.run() + ################################## + if not has_secrets(config): + return + ################################## + # loads variables in the tiramisu config + errors = RougailUserData(config, rougailconfig=rougailconfig).run() + #expected output + config_dict = dict(config_to_dict(config.value.get())) + ok_file = Path('tests') / 'results' / dir_name / test_dir.name / 'makedict' / 'bitwarden.json' + if not ok_file.is_file(): + ok_file.parent.mkdir(parents=True, exist_ok=True) + with open(ok_file, 'a') as json_file: + dump(config_dict, json_file, indent=4) + with open(ok_file) as json_file: + expected = load(json_file) + errors_file = Path('tests') / 'results' / dir_name / test_dir.name / 'errors' / 'bitwarden.json' + if not errors_file.is_file(): + errors_file.parent.mkdir(parents=True, exist_ok=True) + with open(errors_file, 'a') as json_file: + dump(errors, json_file, indent=4) + with open(errors_file) as json_file: + expected_errors = load(json_file) + # expected_errors = { + # 'errors': [], + # 'warnings': [], + # } + assert expected_errors == errors + # + config.property.read_only() + assert expected == config_dict + ###################################### + #teardown: set the original environement again + os.environ = save.copy() + ###################################### + + +def has_secrets(optiondescription): + for option in optiondescription: + if option.isoptiondescription(): + if has_secrets(option): + return True + elif option.information.get('type', None) == 'secret': + return True + return False + + +def test_dictionaries_all(test_dir): + "tests the output" + _test_dictionaries(test_dir, False, EXT) + + +def test_dictionaries_namespace_all(test_dir): + "tests the output" + _test_dictionaries(test_dir, True, EXT)