feat: doc

This commit is contained in:
egarette@silique.fr 2025-12-20 20:36:35 +01:00
parent 88e7548fa4
commit a86a8bdc28
10 changed files with 391 additions and 289 deletions

21
README.fr.md Normal file
View file

@ -0,0 +1,21 @@
---
gitea: none
include_toc: true
---
[🇬🇧 (EN)](README.md) - [🇫🇷 (FR)](README.fr.md)
## Les secrets sont dans Bitwarden
> [!NOTE]
>
> Charge les secrets depuis Bitwarden. Les données sont récupérées à l'aide de la commande officielle « bw » ou de la commande alternative « rbw » (plus rapide). Avant d'utiliser Rougail, la commande doit être connectée et déverrouillée.\
> **Chemin** : bitwarden\
> *`désactivé`*\
> **Désactivé** : si bitwarden n'est pas dans "[Sélection pour données utilisateur](#step.user_data)"
| Variable | Description | Valeur par défaut | Type | Validateur |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------------------------|-------------------------------------------------------------------------------------------------------------|--------------------------------------------------------|
| **<a id="bitwarden.command" name="bitwarden.command">bitwarden.command</a>**<br/>**Ligne de commande** : --bitwarden.command<br/>**Variable d'environnement** : BITWARDEN.COMMAND | Ligne de commande utilisée pour récupérer les secrets.<br/>Ligne de commande doit être dans le PATH. | •&nbsp;rbw<br/>&nbsp;bw | [`choice`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `multiple` `obligatoire` | `unique`<br/>**Choix** : <br/>&nbsp;rbw<br/>&nbsp;bw |
| **<a id="bitwarden.mock_enable" name="bitwarden.mock_enable">bitwarden.mock_enable</a>**<br/>**Ligne de commande** : <br/>&nbsp;--bitwarden.mock_enable<br/>&nbsp;--bitwarden.no-mock_enable<br/>**Variable d'environnement** : BITWARDEN.MOCK_ENABLE | Simuler la génération de mots de passe au lieu de les récupérer. | false | [`boolean`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `obligatoire` | |

View file

@ -1,2 +1,21 @@
# rougail-user-data-bitwarden ---
gitea: none
include_toc: true
---
[🇬🇧 (EN)](README.md) - [🇫🇷 (FR)](README.fr.md)
## Secrets are in Bitwarden
> [!NOTE]
>
> Load the secrets from Bitwarden. The data is retrieved using the official command line "bw" or the alternative command "rbw" (faster). Before using Rougail, the command must be logged and unlocked.\
> **Path**: bitwarden\
> *`disabled`*\
> **Disabled**: if bitwarden is not set in "[Select for user datas](#step.user_data)"
| Variable | Description | Default value | Type | Validator |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|--------------------------|-----------------------------------------------------------------------------------------------------------|---------------------------------------------------------|
| **<a id="bitwarden.command" name="bitwarden.command">bitwarden.command</a>**<br/>**Command line**: --bitwarden.command<br/>**Environment variable**: BITWARDEN.COMMAND | Command line used to retrieve secrets.<br/>Command line must be in PATH. | •&nbsp;rbw<br/>&nbsp;bw | [`choice`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `multiple` `mandatory` | `unique`<br/>**Choices**: <br/>&nbsp;rbw<br/>&nbsp;bw |
| **<a id="bitwarden.mock_enable" name="bitwarden.mock_enable">bitwarden.mock_enable</a>**<br/>**Command line**: <br/>&nbsp;--bitwarden.mock_enable<br/>&nbsp;--bitwarden.no-mock_enable<br/>**Environment variable**: BITWARDEN.MOCK_ENABLE | Simulate password generation instead of retrieve them. | false | [`boolean`](https://rougail.readthedocs.io/en/latest/variable.html#variables-types) `mandatory` | |

44
doc.py Executable file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
from rougail import Rougail
from rougail.config import get_common_rougail_config, get_rougail_config
from rougail.output_doc import RougailOutputDoc
from rougail.user_data_bitwarden.config import get_rougail_config as local_get_rougail_config
def to_yaml(options):
return """%YAML 1.2
---
version: 1.1
""" + options + "..."
backward_compatibility = False
rougailconfig = get_rougail_config(backward_compatibility=backward_compatibility)
rougailconfig['force_optional'] = True
rougailconfig['step.output'] = "doc"
# rougailconfig['tiramisu_cache'] = "a.py"
rougailconfig['step.structural'] = ["commandline", "string"]
rougailconfig['main_namespace'] = None
rougailconfig["doc.root"] = "bitwarden"
rougailconfig["doc.title_level"] = 2
rougailconfig["doc.tabulars.with_commandline"] = True
rougailconfig["doc.tabulars.with_environment"] = True
rougailconfig["doc.tabulars.environment_prefix"] = "ROUGAILCLI"
rougailconfig["doc.output_format"] = "github"
rougailconfig['doc.tabular_template'] = 'six_columns'
rougail_config = to_yaml(get_common_rougail_config(backward_compatibility=backward_compatibility)[2])
module_config = to_yaml(local_get_rougail_config(backward_compatibility=False)['options'])
rougailconfig['main_structural_strings'] = [rougail_config, module_config]
rougail = Rougail(rougailconfig)
config = rougail.run()
config.property.read_write()
output = RougailOutputDoc(
config=config,
rougailconfig=rougailconfig,
)
print("""---
gitea: none
include_toc: true
---
[🇬🇧 (EN)](README.md) - [🇫🇷 (FR)](README.fr.md)
""")
output.print()

View file

@ -5,8 +5,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"POT-Creation-Date: 2025-11-05 08:24+0100\n" "POT-Creation-Date: 2025-12-20 20:33+0100\n"
"PO-Revision-Date: 2025-11-05 08:24+0100\n" "PO-Revision-Date: 2025-12-20 20:33+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: fr\n" "Language: fr\n"
@ -14,74 +14,100 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
"X-Generator: Poedit 3.7\n" "X-Generator: Poedit 3.8\n"
#: src/rougail/user_data_bitwarden/config.py:46 #: src/rougail/user_data_bitwarden/config.py:30
msgid "Configuration rougail-user-data-bitwarden" msgid "Secrets are in Bitwarden"
msgstr "Configuration de rougail-user-data-bitwarden" msgstr "Les secrets sont dans Bitwarden"
#: src/rougail/user_data_bitwarden/config.py:54 #: src/rougail/user_data_bitwarden/config.py:32
msgid "Application used to retrieve secrets" msgid ""
msgstr "Application utilisé pour récupérer des secrets" "Load the secrets from Bitwarden. The data is retrieved using the official "
"command line \"bw\" or the alternative command \"rbw\" (faster). Before "
"using Rougail, the command must be logged and unlocked."
msgstr ""
"Charge les secrets depuis Bitwarden. Les données sont récupérées à l'aide de "
"la commande officielle « bw » ou de la commande alternative « rbw » (plus "
"rapide). Avant d'utiliser Rougail, la commande doit être connectée et "
"déverrouillée."
#: src/rougail/user_data_bitwarden/data.py:53 #: src/rougail/user_data_bitwarden/config.py:37
msgid "if bitwarden is not set in \"_.step.user_data\""
msgstr "si bitwarden n'est pas dans \"_.step.user_data\""
#: src/rougail/user_data_bitwarden/config.py:40
msgid "Command line used to retrieve secrets"
msgstr "Ligne de commande utilisée pour récupérer les secrets"
#: src/rougail/user_data_bitwarden/config.py:41
msgid "Command line must be in PATH"
msgstr "Ligne de commande doit être dans le PATH"
#: src/rougail/user_data_bitwarden/config.py:49
msgid "Simulate password generation instead of retrieve them"
msgstr "Simuler la génération de mots de passe au lieu de les récupérer"
#: src/rougail/user_data_bitwarden/data.py:132
msgid "\"bitwarden\" is not set in step.user_data" msgid "\"bitwarden\" is not set in step.user_data"
msgstr "\"bitwarden\" n'est pas dans step.user_data" msgstr "\"bitwarden\" n'est pas dans step.user_data"
#: src/rougail/user_data_bitwarden/data.py:75 #: src/rougail/user_data_bitwarden/data.py:169
msgid "\"rbw\" or \"bw\"" msgid "please unlock Bitwarden password database with {0} command line"
msgstr "\"rbw\" ou \"bw\""
#: src/rougail/user_data_bitwarden/data.py:77
msgid "\"{0}\""
msgstr "\"{0}\""
#: src/rougail/user_data_bitwarden/data.py:80
msgid "please unlock Bitwarden password database with {0}"
msgstr "" msgstr ""
"veuillez déverrouiller la base de donnée de mot de passe Bitwarden avec {0}" "veuillez déverrouiller la base de mot de passe Bitwarden avec la ligne de "
"commande {0}"
#: src/rougail/user_data_bitwarden/data.py:85 #: src/rougail/user_data_bitwarden/data.py:171
msgid "cannot find Bitwarden command {0} please install it" msgid "cannot find Bitwarden command line {0} please install it"
msgstr "ne peut trouver la commande Bitwarden {0} veuillez l'installer" msgstr ""
"ne peut trouver la ligne de commande Bitwarden {0} veuillez l'installer"
#: src/rougail/user_data_bitwarden/data.py:163 #: src/rougail/user_data_bitwarden/data.py:208
msgid "the default value for \"{0}\" must be the Bitwarden item name" msgid "the default value for \"{0}\" must be the Bitwarden item name"
msgstr "" msgstr ""
"la valeur par défaut pour \"{0}\" doit être un nom d'élément de Bitwarden" "la valeur par défaut pour \"{0}\" doit être un nom d'élément de Bitwarden"
#: src/rougail/user_data_bitwarden/data.py:186 #: src/rougail/user_data_bitwarden/data.py:219
msgid "cannot execute the \"{0}\" commandline from Bitwarden for \"{1}\": {2}" msgid "cannot execute the \"{0}\" commandline from Bitwarden for \"{1}\": {2}"
msgstr "" msgstr ""
"ne peut exécuter la ligne de commande \"{0}\" pour Bitwarden pour \"{1}\": " "ne peut exécuter la ligne de commande \"{0}\" pour Bitwarden pour \"{1}\": "
"{2}" "{2}"
#: src/rougail/user_data_bitwarden/data.py:195 #: src/rougail/user_data_bitwarden/data.py:228
msgid "item \"{0}\" in Bitwarden is not found for \"{1}\"" msgid "item \"{0}\" in Bitwarden is not found for \"{1}\""
msgstr "l'élément \"{0}\" dans Bitwarden n'est pas trouvé pour \"{1}\"" msgstr "l'élément \"{0}\" dans Bitwarden n'est pas trouvé pour \"{1}\""
#: src/rougail/user_data_bitwarden/data.py:210 #: src/rougail/user_data_bitwarden/data.py:243
msgid "several items found with name \"{0}\" in Bitwarden for \"{1}\": \"{2}\"" msgid "several items found with name \"{0}\" in Bitwarden for \"{1}\": \"{2}\""
msgstr "" msgstr ""
"plusieurs éléments trouvés avec le nom \"{0}\" dans Bitwarden pour \"{1}\": " "plusieurs éléments trouvés avec le nom \"{0}\" dans Bitwarden pour \"{1}\": "
"\"{2}\"" "\"{2}\""
#: src/rougail/user_data_bitwarden/data.py:229 #: src/rougail/user_data_bitwarden/data.py:262
msgid "unexpected datas \"{0}\" from Bitwarden for \"{1}\": {2}" msgid "unexpected datas \"{0}\" from Bitwarden for \"{1}\": {2}"
msgstr "données inattendues \"{0}\" pour Bitwarden pour \"{1}\": {2}" msgstr "données inattendues \"{0}\" pour Bitwarden pour \"{1}\": {2}"
#: src/rougail/user_data_bitwarden/data.py:237 #: src/rougail/user_data_bitwarden/data.py:270
msgid "password" msgid "password"
msgstr "de mot de passe" msgstr "de mot de passe"
#: src/rougail/user_data_bitwarden/data.py:239 #: src/rougail/user_data_bitwarden/data.py:272
msgid "username" msgid "username"
msgstr "de nom d'utilisateur" msgstr "de nom d'utilisateur"
#: src/rougail/user_data_bitwarden/data.py:241 #: src/rougail/user_data_bitwarden/data.py:274
msgid "item \"{0}\" in Bitwarden has no {1} for \"{2}\"" msgid "item \"{0}\" in Bitwarden has no {1} for \"{2}\""
msgstr "l'élément \"{0}\" dans Bitwarden n'a pas {1} for \"{2}\"" msgstr "l'élément \"{0}\" dans Bitwarden n'a pas {1} for \"{2}\""
#~ msgid "Configuration rougail-user-data-bitwarden"
#~ msgstr "Configuration de rougail-user-data-bitwarden"
#~ msgid "\"rbw\" or \"bw\""
#~ msgstr "\"rbw\" ou \"bw\""
#~ msgid "\"{0}\""
#~ msgstr "\"{0}\""
#~ msgid "" #~ msgid ""
#~ "the value for \"{0}\" at index {1} is already set while it should be " #~ "the value for \"{0}\" at index {1} is already set while it should be "
#~ "filled in by Bitwarden" #~ "filled in by Bitwarden"

View file

@ -5,7 +5,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"POT-Creation-Date: 2025-11-05 08:24+0100\n" "POT-Creation-Date: 2025-12-20 20:33+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -15,63 +15,71 @@ msgstr ""
"Generated-By: pygettext.py 1.5\n" "Generated-By: pygettext.py 1.5\n"
#: src/rougail/user_data_bitwarden/config.py:46 #: src/rougail/user_data_bitwarden/config.py:30
msgid "Configuration rougail-user-data-bitwarden" msgid "Secrets are in Bitwarden"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/config.py:54 #: src/rougail/user_data_bitwarden/config.py:32
msgid "Application used to retrieve secrets" msgid "Load the secrets from Bitwarden. The data is retrieved using the official command line \"bw\" or the alternative command \"rbw\" (faster). Before using Rougail, the command must be logged and unlocked."
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:53 #: src/rougail/user_data_bitwarden/config.py:37
msgid "if bitwarden is not set in \"_.step.user_data\""
msgstr ""
#: src/rougail/user_data_bitwarden/config.py:40
msgid "Command line used to retrieve secrets"
msgstr ""
#: src/rougail/user_data_bitwarden/config.py:41
msgid "Command line must be in PATH"
msgstr ""
#: src/rougail/user_data_bitwarden/config.py:49
msgid "Simulate password generation instead of retrieve them"
msgstr ""
#: src/rougail/user_data_bitwarden/data.py:132
msgid "\"bitwarden\" is not set in step.user_data" msgid "\"bitwarden\" is not set in step.user_data"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:75 #: src/rougail/user_data_bitwarden/data.py:169
msgid "\"rbw\" or \"bw\"" msgid "please unlock Bitwarden password database with {0} command line"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:77 #: src/rougail/user_data_bitwarden/data.py:171
msgid "\"{0}\"" msgid "cannot find Bitwarden command line {0} please install it"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:80 #: src/rougail/user_data_bitwarden/data.py:208
msgid "please unlock Bitwarden password database with {0}"
msgstr ""
#: src/rougail/user_data_bitwarden/data.py:85
msgid "cannot find Bitwarden command {0} please install it"
msgstr ""
#: src/rougail/user_data_bitwarden/data.py:163
msgid "the default value for \"{0}\" must be the Bitwarden item name" msgid "the default value for \"{0}\" must be the Bitwarden item name"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:186 #: src/rougail/user_data_bitwarden/data.py:219
msgid "cannot execute the \"{0}\" commandline from Bitwarden for \"{1}\": {2}" msgid "cannot execute the \"{0}\" commandline from Bitwarden for \"{1}\": {2}"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:195 #: src/rougail/user_data_bitwarden/data.py:228
msgid "item \"{0}\" in Bitwarden is not found for \"{1}\"" msgid "item \"{0}\" in Bitwarden is not found for \"{1}\""
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:210 #: src/rougail/user_data_bitwarden/data.py:243
msgid "several items found with name \"{0}\" in Bitwarden for \"{1}\": \"{2}\"" msgid "several items found with name \"{0}\" in Bitwarden for \"{1}\": \"{2}\""
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:229 #: src/rougail/user_data_bitwarden/data.py:262
msgid "unexpected datas \"{0}\" from Bitwarden for \"{1}\": {2}" msgid "unexpected datas \"{0}\" from Bitwarden for \"{1}\": {2}"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:237 #: src/rougail/user_data_bitwarden/data.py:270
msgid "password" msgid "password"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:239 #: src/rougail/user_data_bitwarden/data.py:272
msgid "username" msgid "username"
msgstr "" msgstr ""
#: src/rougail/user_data_bitwarden/data.py:241 #: src/rougail/user_data_bitwarden/data.py:274
msgid "item \"{0}\" in Bitwarden has no {1} for \"{2}\"" msgid "item \"{0}\" in Bitwarden has no {1} for \"{2}\""
msgstr "" msgstr ""

View file

@ -0,0 +1,48 @@
"""
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 rougail.error import DictConsistencyError
from rougail.annotator.variable import Walk
class Annotator(Walk):
"""Annotate for bitwarden"""
level = 90
def __init__(
self,
objectspace,
*args, # pylint: disable=unused-argument
) -> None:
if not objectspace.paths:
return
self.objectspace = objectspace
self.check_variable()
def check_variable(self):
for variable in self.get_variables():
if not variable.secret_manager:
continue
path = variable.path
self.objectspace.informations.add(path, "bitwarden", True)
self.objectspace.informations.add(path, "default_value_makes_sense", False)
self.objectspace.properties.add(path, "novalidator", True)
variable.default = variable.secret_manager

View file

@ -26,36 +26,27 @@ def get_rougail_config(
*, *,
backward_compatibility=True, backward_compatibility=True,
) -> dict: ) -> dict:
options = f""" options = f"""bitwarden:
step: description: {_("Secrets are in Bitwarden")}
help: |-
structural: {_("Load the secrets from Bitwarden. The data is retrieved using the official command line \"bw\" or the alternative command \"rbw\" (faster). Before using Rougail, the command must be logged and unlocked.")}
redefine: true
default:
jinja: |-
{{% if step.user_data is not propertyerror and 'bitwarden' in step.user_data %}}
bitwarden
{{% endif %}}
{{% if step.user_data is not propertyerror and 'risotto' in step.user_data %}}
risotto
{{% else %}}
directory
{{% endif %}}
bitwarden:
description: {_("Configuration rougail-user-data-bitwarden")}
disabled: disabled:
jinja: | jinja: |-
{{% if step.user_data is propertyerror or 'bitwarden' not in step.user_data %}} {{{{ _.step.user_data is propertyerror or 'bitwarden' not in _.step.user_data }}}}
disabled return_type: boolean
{{% endif %}} description: {_('if bitwarden is not set in "_.step.user_data"')}
command: command:
description: {_("Application used to retrieve secrets")} description: {_("Command line used to retrieve secrets")}
help: {_("Command line must be in PATH")}
choices: choices:
- rbw - rbw
- bw - bw
mandatory: false default:
- rbw
- bw
mock_enable: false # {_("Simulate password generation instead of retrieve them")}
""" """
return { return {
"name": "bitwarden", "name": "bitwarden",

View file

@ -25,12 +25,90 @@ from os import environ
from shutil import which from shutil import which
from tiramisu.error import display_list
from rougail.error import ExtensionError from rougail.error import ExtensionError
from .i18n import _ from .i18n import _
class RougailUserDataBitwarden: class FakeBW:
def get_key(
self,
key_bitwarden: str,
multiple: bool,
):
return [{'name': key_bitwarden, 'login': {'username': "example_login", 'password': "Ex4mpL3_P4ssw0rD"}}]
class RBW:
def unlocked(self):
try:
cpe = run(["rbw", "unlocked"], capture_output=True)
except Exception as exc:
return False
return cpe.returncode == 0
def get_key(
self,
key_bitwarden: str,
multiple: bool,
):
keys = []
items = run_commandline(["rbw", "search", key_bitwarden]).strip()
if items:
items = items.split("\n")
else:
items = []
for item in items:
if "@" in item:
keys.append(item.split("@", 1)[-1])
else:
keys.append(item.rsplit("/", 1)[-1])
if not multiple:
if not items:
return []
if len(items) > 1:
return [{"name": key} for key in keys]
keys = [key_bitwarden]
datas = []
for key in keys:
data = loads(
run_commandline(
["rbw", "get", key, "--raw", "--ignorecase"]
).strip()
)
datas.append({"name": key, "login": data["data"]})
return datas
class BW:
def unlocked(self):
try:
cpe = run(["bw", "status"], capture_output=True)
except Exception as exc:
return False
if cpe.returncode != 0:
return False
try:
data = loads(cpe.stdout.decode("utf8"))
if data["status"] == "unlocked":
return True
except:
pass
return False
def get_key(
self,
key_bitwarden: str,
*args,
):
return loads(
run_commandline(
["bw", "list", "items", "--search", key_bitwarden, "--nointeraction"]
)
)
class RougailUserDataBitwarden:
def __init__( def __init__(
self, self,
config: "Config", config: "Config",
@ -47,6 +125,7 @@ class RougailUserDataBitwarden:
if "bitwarden" not in user_data: if "bitwarden" not in user_data:
user_data.append("bitwarden") user_data.append("bitwarden")
rougailconfig["step.user_data"] = user_data rougailconfig["step.user_data"] = user_data
else:
user_data = rougailconfig["step.user_data"] user_data = rougailconfig["step.user_data"]
self.rougailconfig = rougailconfig self.rougailconfig = rougailconfig
if "bitwarden" not in user_data: if "bitwarden" not in user_data:
@ -54,65 +133,14 @@ class RougailUserDataBitwarden:
self.errors = [] self.errors = []
self.warnings = [] self.warnings = []
self.leader_informations = {} self.leader_informations = {}
self.bitwarden_command_line = self.get_command(rougailconfig) self.commands = {'rbw': RBW(),
'bw': BW(),
def get_command(self, rougailconfig): }
if "ROUGAIL_BITWARDEN_MOCK_ENABLE" in environ:
return None
one_is_find = False
force_command = rougailconfig["bitwarden.command"]
if force_command:
commands = [force_command]
else:
commands = ["rbw", "bw"]
for command in commands:
status = self.test_command(command)
if status is False:
one_is_find = True
elif status:
return command
if not force_command:
command_string = _('"rbw" or "bw"')
else:
command_string = _('"{0}"').format(force_command)
if one_is_find:
raise ExtensionError(
_("please unlock Bitwarden password database with {0}").format(
command_string
)
)
raise ExtensionError(
_("cannot find Bitwarden command {0} please install it").format(
command_string
)
)
def test_command(self, command):
if not which(command):
return None
if command == "rbw":
cmd = ["rbw", "unlocked"]
else:
cmd = ["bw", "status"]
try:
cpe = run(cmd, capture_output=True)
except Exception as exc:
return False
if cpe.returncode != 0:
return False
if command == "rbw":
return True
try:
data = loads(cpe.stdout.decode("utf8"))
if data["status"] == "unlocked":
return True
except:
pass
return False
def run(self): def run(self):
values = {} values = {}
self.set_passwords(self.config.forcepermissive, values) self.command = self.get_command()
self.set_passwords(self.config.unrestraint, values)
return [ return [
{ {
"source": 'Bitwarden', "source": 'Bitwarden',
@ -125,15 +153,33 @@ class RougailUserDataBitwarden:
} }
] ]
def get_command(self):
if self.rougailconfig["bitwarden.mock_enable"]:
return FakeBW()
one_is_find = False
for command_name in self.rougailconfig["bitwarden.command"]:
if not which(command_name):
continue
command = self.commands[command_name]
if not command.unlocked():
one_is_find = True
continue
return command
if one_is_find:
msg = _("please unlock Bitwarden password database with {0} command line")
else:
msg = _("cannot find Bitwarden command line {0} please install it")
raise ExtensionError(msg.format(display_list(list(self.commands))))
def set_passwords(self, optiondescription, values): def set_passwords(self, optiondescription, values):
for option in optiondescription.list(uncalculated=True): for option in optiondescription.list(uncalculated=True):
if option.isoptiondescription(): if option.isoptiondescription():
self.set_passwords(option, values) self.set_passwords(option, values)
elif option.information.get("bitwarden", False): elif option.information.get("bitwarden", False):
values[option.path(uncalculated=True)] = (set_password, self.leader_informations, self.bitwarden_command_line) values[option.path(uncalculated=True)] = (set_password, self.leader_informations, self.command)
def set_password(leader_informations, bitwarden_command_line, *, option, warnings, errors): def set_password(leader_informations, command, *, option, warnings, errors):
path = option.path() path = option.path()
leader_key = None leader_key = None
if option.isfollower(): if option.isfollower():
@ -141,21 +187,20 @@ def set_password(leader_informations, bitwarden_command_line, *, option, warning
if leader_path in leader_informations: if leader_path in leader_informations:
leader_key = leader_informations[leader_path][option.index()] leader_key = leader_informations[leader_path][option.index()]
if not option.isleader(): if not option.isleader():
return get_values(bitwarden_command_line, option, warnings, errors, leader_key=leader_key) return get_values(command, option, warnings, errors, leader_key=leader_key)
leader_informations[path], option_values = get_values( leader_informations[path], option_values = get_values(
bitwarden_command_line, option, warnings, errors, multiple=True, leader_key=option.value.default()[0] command, option, warnings, errors, multiple=True, leader_key=option.value.default()[0]
) )
return option_values return option_values
def get_values(bitwarden_command_line, option, warnings, errors, *, multiple=False, leader_key=None): def get_values(command, option, warnings, errors, *, multiple=False, leader_key=None):
if leader_key: if leader_key:
key = leader_key key = leader_key
else: else:
key = option.value.default() key = option.value.default()
path = option.path() path = option.path()
type_ = option.information.get("type") type_ = option.information.get("type")
# FIXME retrait du permissive suffit ?
if "validator" not in option.property.get(): if "validator" not in option.property.get():
option.property.add("validator") option.property.add("validator")
if not isinstance(key, str): if not isinstance(key, str):
@ -167,25 +212,13 @@ def get_values(bitwarden_command_line, option, warnings, errors, *, multiple=Fal
if multiple: if multiple:
return [], [] return [], []
return None return None
if "ROUGAIL_BITWARDEN_MOCK_ENABLE" in environ:
if type_ == "secret":
value = "Ex4mpL3_P4ssw0rD"
else:
value = "example_login"
if multiple:
return [key], [value]
return value
try: try:
if bitwarden_command_line == "rbw": data = command.get_key(key, multiple)
data = get_key_from_rbw(key, multiple)
else:
data = get_key_from_bw(key)
except Exception as exc: except Exception as exc:
errors.append( errors.append(
_( _(
'cannot execute the "{0}" commandline from Bitwarden for "{1}": {2}' 'cannot execute the "{0}" commandline from Bitwarden for "{1}": {2}'
).format(bitwarden_command_line, path, exc) ).format(command, path, exc)
) )
if multiple: if multiple:
return [], [] return [], []
@ -245,52 +278,10 @@ def get_value( key_bitwarden: str, path: str, type_: str, data: dict, errors: di
return value return value
def get_key_from_rbw(
key_bitwarden: str, multiple: bool
):
keys = []
items = run_commandline(["rbw", "search", key_bitwarden]).strip()
if items:
items = items.split("\n")
else:
items = []
for item in items:
# if item.count('@') != 1:
# continue
if "@" in item:
keys.append(item.split("@", 1)[-1])
else:
keys.append(item.rsplit("/", 1)[-1])
if not multiple:
if not items:
return []
if len(items) > 1:
return [{"name": key} for key in keys]
keys = [key_bitwarden]
datas = []
for key in keys:
data = loads(
run_commandline(
["rbw", "get", key, "--raw", "--ignorecase"]
).strip()
)
datas.append({"name": key, "login": data["data"]})
return datas
def get_key_from_bw(
key_bitwarden: str,
):
return loads(
run_commandline(
["bw", "list", "items", "--search", key_bitwarden, "--nointeraction"]
)
)
def run_commandline(cmd) -> str: def run_commandline(cmd) -> str:
cpe = run(cmd, capture_output=True) cpe = run(cmd, capture_output=True)
returncode = cpe.returncode
err = cpe.stderr.decode("utf8") err = cpe.stderr.decode("utf8")
if cpe.returncode != 0 or err: if returncode != 0 or err:
raise ExtensionError("{0} ({1})".format(err, cpe.returncode)) raise ExtensionError("{0} ({1})".format(err, returncode))
return cpe.stdout.decode("utf8") return cpe.stdout.decode("utf8")

View file

@ -16,12 +16,14 @@ from rougail_tests.utils import config_to_dict
test_dir = Path(__file__).parent / 'structures' test_dir = Path(__file__).parent / 'structures'
def _test_structural_files(test_dir, command, *, env=False, modified=False): def _test_structural_files(test_dir, command, *, env=False, modified=False, mock=False):
rougailconfig = RougailConfig.copy() rougailconfig = RougailConfig.copy()
rougailconfig['main_structural_directories'] = [str(test_dir)] rougailconfig['main_structural_directories'] = [str(test_dir)]
# rougailconfig['tiramisu_cache'] = "cache.py" # rougailconfig['tiramisu_cache'] = "cache.py"
rougailconfig['step.user_data'] = ['bitwarden'] rougailconfig['step.user_data'] = ['bitwarden']
rougailconfig['bitwarden.command'] = command rougailconfig['bitwarden.command'] = [command]
if mock:
rougailconfig["bitwarden.mock_enable"] = True
rougail = Rougail(rougailconfig) rougail = Rougail(rougailconfig)
config = rougail.run() config = rougail.run()
config.property.read_write() config.property.read_write()
@ -29,7 +31,7 @@ def _test_structural_files(test_dir, command, *, env=False, modified=False):
generated_user_data = RougailUserData(config, rougailconfig=rougailconfig).run() generated_user_data = RougailUserData(config, rougailconfig=rougailconfig).run()
if modified: if modified:
generated_user_data.insert(0, {'source': 'By Hand', 'errors': [], 'warnings': [], 'values': {'rougail.modified_variable': ['user_1', 'user_2'], 'rougail.modified_variable_single': 'user_2'}}) generated_user_data.insert(0, {'source': 'By Hand', 'errors': [], 'warnings': [], 'values': {'rougail.modified_variable': ['user_1', 'user_2'], 'rougail.modified_variable_single': 'user_2'}})
errors = rougail.user_datas(generated_user_data) errors = rougail.user_data(generated_user_data)
#expected output #expected output
config_dict = dict(config_to_dict(config.value.get())) config_dict = dict(config_to_dict(config.value.get()))
if not env: if not env:
@ -213,152 +215,104 @@ def test_structural_files_9_unknown_type_bw():
def test_structural_files_env_1_secret_rbw(): def test_structural_files_env_1_secret_rbw():
"tests the output" _test_structural_files(test_dir / '1_secret', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '1_secret', 'rbw', env=True)
def test_structural_files_env_1_secret_bw(): def test_structural_files_env_1_secret_bw():
"tests the output" _test_structural_files(test_dir / '1_secret', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '1_secret', 'bw', env=True)
def test_structural_files_env_1_secret_unknown_rbw(): def test_structural_files_env_1_secret_unknown_rbw():
"tests the output" _test_structural_files(test_dir / '1_secret_unknown', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '1_secret_unknown', 'rbw', env=True)
def test_structural_files_env_1_secret_unknown_bw(): def test_structural_files_env_1_secret_unknown_bw():
"tests the output" _test_structural_files(test_dir / '1_secret_unknown', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '1_secret_unknown', 'bw', env=True)
def test_structural_files_env_2_username_secret_rbw(): def test_structural_files_env_2_username_secret_rbw():
"tests the output" _test_structural_files(test_dir / '2_username_secret', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret', 'rbw', env=True)
def test_structural_files_env_2_username_secret_bw(): def test_structural_files_env_2_username_secret_bw():
"tests the output" _test_structural_files(test_dir / '2_username_secret', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret', 'bw', env=True)
def test_structural_files_env_2_username_secret_upper_rbw(): def test_structural_files_env_2_username_secret_upper_rbw():
"tests the output" _test_structural_files(test_dir / '2_username_secret_upper', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret_upper', 'rbw', env=True)
def test_structural_files_env_2_username_secret_upper_bw(): def test_structural_files_env_2_username_secret_upper_bw():
"tests the output" _test_structural_files(test_dir / '2_username_secret_upper', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret_upper', 'bw', env=True)
def test_structural_files_env_2_username_secret_invalid_rbw(): def test_structural_files_env_2_username_secret_invalid_rbw():
"tests the output" _test_structural_files(test_dir / '2_username_secret_invalid', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret_invalid', 'rbw', env=True)
def test_structural_files_env_2_username_secret_invalid_bw(): def test_structural_files_env_2_username_secret_invalid_bw():
"tests the output" _test_structural_files(test_dir / '2_username_secret_invalid', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '2_username_secret_invalid', 'bw', env=True)
def test_structural_files_env_3_leadership_secret_rbw(): def test_structural_files_env_3_leadership_secret_rbw():
"tests the output" _test_structural_files(test_dir / '3_leadership_secret', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '3_leadership_secret', 'rbw', env=True)
def test_structural_files_env_3_leadership_secret_bw(): def test_structural_files_env_3_leadership_secret_bw():
"tests the output" _test_structural_files(test_dir / '3_leadership_secret', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '3_leadership_secret', 'bw', env=True)
def test_structural_files_env_3_leadership_secret_several_rbw(): def test_structural_files_env_3_leadership_secret_several_rbw():
"tests the output" _test_structural_files(test_dir / '3_leadership_secret_several', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '3_leadership_secret_several', 'rbw', env=True)
def test_structural_files_env_3_leadership_secret_several_bw(): def test_structural_files_env_3_leadership_secret_several_bw():
"tests the output" _test_structural_files(test_dir / '3_leadership_secret_several', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '3_leadership_secret_several', 'bw', env=True)
def test_structural_files_env_4_several_secrets_rbw(): def test_structural_files_env_4_several_secrets_rbw():
"tests the output" _test_structural_files(test_dir / '4_several_secrets', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '4_several_secrets', 'rbw', env=True)
def test_structural_files_env_4_several_secrets_bw(): def test_structural_files_env_4_several_secrets_bw():
"tests the output" _test_structural_files(test_dir / '4_several_secrets', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '4_several_secrets', 'bw', env=True)
def test_structural_files_env_4_several_secrets_upper_rbw(): def test_structural_files_env_4_several_secrets_upper_rbw():
"tests the output" _test_structural_files(test_dir / '4_several_secrets_upper', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '4_several_secrets_upper', 'rbw', env=True)
def test_structural_files_env_4_several_secrets_upper_bw(): def test_structural_files_env_4_several_secrets_upper_bw():
"tests the output" _test_structural_files(test_dir / '4_several_secrets_upper', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '4_several_secrets_upper', 'bw', env=True)
def test_structural_files_env_5_secret_calc_rbw(): def test_structural_files_env_5_secret_calc_rbw():
"tests the output" _test_structural_files(test_dir / '5_secret_calc', 'rbw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '5_secret_calc', 'rbw', env=True)
def test_structural_files_env_5_secret_calc_bw(): def test_structural_files_env_5_secret_calc_bw():
"tests the output" _test_structural_files(test_dir / '5_secret_calc', 'bw', env=True, mock=True)
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
_test_structural_files(test_dir / '5_secret_calc', 'bw', env=True)
def test_structural_files_env_8_multi_variable_rbw(): def test_structural_files_env_8_multi_variable_rbw():
"tests the output"
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
with raises(DictConsistencyError) as err: with raises(DictConsistencyError) as err:
_test_structural_files(test_dir / '8_multi_variable', 'rbw', env=True) _test_structural_files(test_dir / '8_multi_variable', 'rbw', env=True, mock=True)
assert err.value.errno == 57 assert err.value.errno == 57
def test_structural_files_env_8_multi_variable_bw(): def test_structural_files_env_8_multi_variable_bw():
"tests the output"
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
with raises(DictConsistencyError) as err: with raises(DictConsistencyError) as err:
_test_structural_files(test_dir / '8_multi_variable', 'bw', env=True) _test_structural_files(test_dir / '8_multi_variable', 'bw', env=True, mock=True)
assert err.value.errno == 57 assert err.value.errno == 57
def test_structural_files_env_9_unknown_type_rbw(): def test_structural_files_env_9_unknown_type_rbw():
"tests the output"
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
with raises(DictConsistencyError) as err: with raises(DictConsistencyError) as err:
_test_structural_files(test_dir / '9_unknown_type', 'rbw', env=True) _test_structural_files(test_dir / '9_unknown_type', 'rbw', env=True, mock=True)
assert err.value.errno == 56 assert err.value.errno == 56
def test_structural_files_env_9_unknown_type_bw(): def test_structural_files_env_9_unknown_type_bw():
"tests the output"
with mock.patch.dict(os.environ, {'ROUGAIL_BITWARDEN_MOCK_ENABLE': '1'}):
with raises(DictConsistencyError) as err: with raises(DictConsistencyError) as err:
_test_structural_files(test_dir / '9_unknown_type', 'bw', env=True) _test_structural_files(test_dir / '9_unknown_type', 'bw', env=True, mock=True)
assert err.value.errno == 56 assert err.value.errno == 56