""" 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 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 = {} 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.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] = [] for val in option.value.get(): names, values = self.get_values(path, type_, val, allow_multiple=True) print(names, values) 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]) 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': return 'Ex4mpL3_P4ssw0rD' return 'example_login' 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(path, exc)) return None, None out = cpe.stdout.decode('utf8') err = cpe.stderr.decode('utf8') if cpe.returncode != 0 or err: self.errors.append(_('cannot get {0} "{1}" from Bitwarden for "{2}": {3} ({4})').format(type_, key_bitwarden, path, err, cpe.returncode)) return None, None try: data = loads(out) except Exception as exc: self.errors.append(_('cannot load {0} "{1}" from Bitwarden for "{2}": {3}').format(type_, key_bitwarden, path, exc)) return None, None if not data: self.errors.append(_('cannot find {0} "{1}" from Bitwarden for "{2}"').format(type_, 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}" from 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': return data['login']['password'] return data['login']['username'] except Exception as exc: self.errors.append(_('unexpected datas "{0}" from Bitwarden for "{1}": {2}').format(key_bitwarden, path, exc)) return None