""" 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 os import environ from shutil import which 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 = [] self.leader_informations = {} self.bitwarden_command_line = self.get_command(rougailconfig) 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 ExtentionError( _("please unlock Bitwarden password database with {0}").format( command_string ) ) raise ExtentionError( _("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): self.set_passwords(self.config.forcepermissive) return { "errors": self.errors, "warnings": self.warnings, } def run_commandline(self, cmd) -> str: cpe = run(cmd, capture_output=True) err = cpe.stderr.decode("utf8") if cpe.returncode != 0 or err: raise Exception("{0} ({1})".format(err, cpe.returncode)) return cpe.stdout.decode("utf8") def get_key_from_commandline( self, key_bitwarden: str, allow_multiple: bool ) -> list[str]: if self.bitwarden_command_line == "rbw": keys = [] items = self.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 allow_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( self.run_commandline( ["rbw", "get", key, "--raw", "--ignorecase"] ).strip() ) datas.append({"name": key, "login": data["data"]}) return datas return loads( self.run_commandline( ["bw", "list", "items", "--search", key_bitwarden, "--nointeraction"] ) ) def set_passwords(self, optiondescription): for option in optiondescription: if option.isoptiondescription(): self.set_passwords(option) elif option.information.get("bitwarden", False): path = option.path() if not option.owner.isdefault(): if option.isfollower(): self.errors.append( _( 'the value for "{0}" at index {1} is already set while it should be filled in by Bitwarden' ).format(path, option.index()) ) else: self.errors.append( _( 'the value for "{0}" is already set while it should be filled in by Bitwarden' ).format(path) ) continue type_ = option.information.get("type") if option.isleader(): leader_values = [] self.leader_informations[path] = [] values = option.value.get() for val in values: names, values = self.get_values( path, type_, val, allow_multiple=True ) if isinstance(values, list): leader_values.extend(values) self.leader_informations[path].extend(names) else: leader_values.append(values) self.leader_informations[path].append(names) option.value.set(leader_values) else: if option.isfollower(): leader_path = optiondescription.leader().path() if leader_path in self.leader_informations: key_bitwarden = self.leader_informations[leader_path][ option.index() ] else: key_bitwarden = option.value.get() else: key_bitwarden = option.value.get() option.value.set(self.get_values(path, type_, key_bitwarden)[1]) option.permissive.add("novalidator") def get_values(self, path, type_, key_bitwarden, *, allow_multiple=False): if not isinstance(key_bitwarden, str): self.errors.append( _('the default value for "{0}" must be the Bitwarden item name').format( path ) ) return None, None if "ROUGAIL_BITWARDEN_MOCK_ENABLE" in environ: if type_ == "secret": value = "Ex4mpL3_P4ssw0rD" else: value = "example_login" if allow_multiple: return [key_bitwarden], [value] return key_bitwarden, value try: data = self.get_key_from_commandline(key_bitwarden, allow_multiple) except Exception as exc: self.errors.append( _( 'cannot execute the "{0}" commandline from Bitwarden for "{1}": {2}' ).format(self.bitwarden_command_line, path, exc) ) return None, None if not data: self.errors.append( _('item "{0}" in Bitwarden is not found for "{1}"').format( key_bitwarden, path ) ) return None, None if len(data) != 1: names = [d["name"] for d in data] if allow_multiple: ret = [] return names, [ self.get_value(key_bitwarden, path, type_, d) for d in data ] self.errors.append( _( 'several items found with name "{0}" in Bitwarden for "{1}": "{2}"' ).format(key_bitwarden, path, '", "'.join(names)) ) return None, None return data[0]["name"], self.get_value(key_bitwarden, path, type_, data[0]) def get_value(self, key_bitwarden: str, path: str, type_: str, data: dict) -> str: try: if type_ == "secret": value = data["login"]["password"] else: value = data["login"]["username"] except Exception as exc: self.errors.append( _('unexpected datas "{0}" from Bitwarden for "{1}": {2}').format( key_bitwarden, path, exc ) ) value = None else: if value is None: if type_ == "secret": bw_type = _("password") else: bw_type = _("username") self.errors.append( _('item "{0}" in Bitwarden has no {1} for "{2}"').format( key_bitwarden, bw_type, path ) ) return value