#!/usr/bin/env python3 import argparse import enum import getpass import shutil import subprocess as sp from pathlib import Path import jinja2 PROJECT_ROOT_DIRECTORY = Path(__file__).parent.parent.resolve() bitwarden_session = None class TemplateEnvType(enum.StrEnum): USERNAME = "username" PASSWORD = "password" class TemplateEnv: bw_path: Path env_type: TemplateEnvType cached_items: dict[str, str] def _fetch_secret(self, secret_id: str) -> str: global bitwarden_session if not bitwarden_session: raise NotImplementedError("Failed to get bitwarden session") res = sp.run( [ self.bw_path, "get", self.env_type.value, secret_id, "--session", bitwarden_session, ], capture_output=True, text=True, ) res.check_returncode() return res.stdout def __init__(self, _type: TemplateEnvType, bw_path: Path): self.env_type = _type self.bw_path = bw_path self.cached_items = dict() def __getitem__(self, item): if cached_item := self.cached_items.get(item): return cached_item self.cached_items[item] = self._fetch_secret( item, ) return self.cached_items[item] def _add_args(parser: argparse.ArgumentParser): parser.add_argument( "search_paths", help="start directory to walk files to find secret references", default=[PROJECT_ROOT_DIRECTORY], nargs="*", ) parser.add_argument( "--bw-path", "-b", help="custom path for bitwarden cli executable", default=shutil.which("bw"), ) def init_bw_session(bw_path: Path): global bitwarden_session if (pw_file := (PROJECT_ROOT_DIRECTORY / ".bw2secrets")).exists(): bitwarden_password = pw_file.read_text().strip() else: print("Please, provide your bitwarden master password") bitwarden_password = getpass.getpass("Master password: ") res = sp.run( [bw_path, "unlock", bitwarden_password, "--raw"], capture_output=True, text=True, ) res.check_returncode() bitwarden_session = res.stdout def sync_bw_session(bw_path: Path): global bitwarden_session if not bitwarden_session: raise NotImplementedError("Failed to get bitwarden session") res = sp.run( [bw_path, "sync", "--session", bitwarden_session], capture_output=True, text=True, ) res.check_returncode() def find_templates(base_dirs: set[Path]) -> set[Path]: env_templates: set[Path] = set() for path in base_dirs: for env_template in path.glob("**/*.template"): print(f"INFO: Found template at {env_template}") env_templates.add(env_template) return env_templates def compile_file(file_path: Path, bw_path: Path): jinja_env = jinja2.Environment( loader=jinja2.FileSystemLoader(file_path.parent), ) username = TemplateEnv(TemplateEnvType.USERNAME, bw_path) password = TemplateEnv(TemplateEnvType.PASSWORD, bw_path) template = jinja_env.get_template(file_path.name) rendered_template = template.render( dict( username=username, password=password, ), ) file_path.with_name( file_path.name.replace( ".template", "", ), ).write_text(rendered_template) def main() -> int: parser = argparse.ArgumentParser("bw2secrets") _add_args(parser) args = parser.parse_args() if not (bw_path := args.bw_path): print("Bitwarden CLI `bw` executable not found in PATH") return 1 search_paths: set[Path] = set() for path in args.search_paths: search_path = Path(path) search_paths.add(search_path) print(f"INFO: Will be searching {path}") print("INFO: Searching templates") template_files = find_templates(search_paths) init_bw_session(bw_path) sync_bw_session(bw_path) for file in template_files: print(f"INFO: Compiling {file}") compile_file(file, bw_path) return 0 if __name__ == "__main__": raise SystemExit(main())