fujin-secrets-bitwarden 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.3
2
+ Name: fujin-secrets-bitwarden
3
+ Version: 0.16.0
4
+ Summary: Bitwarden secret adapter for Fujin
5
+ Author: Tobi
6
+ Author-email: Tobi <tobidegnon@proton.me>
7
+ Classifier: Programming Language :: Python :: 3 :: Only
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: fujin-cli>=0.16
14
+ Requires-Dist: python-dotenv>=1.0.1
15
+ Requires-Python: >=3.10
16
+ Project-URL: Homepage, https://github.com/Tobi-De/fujin
17
+ Project-URL: Issues, https://github.com/Tobi-De/fujin/issues
18
+ Project-URL: Source, https://github.com/Tobi-De/fujin
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Fujin Secrets - Bitwarden
22
+
23
+ Bitwarden secret adapter for Fujin deployment tool.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install fujin-secrets-bitwarden
29
+ ```
30
+
31
+ Or with uv:
32
+
33
+ ```bash
34
+ uv pip install fujin-secrets-bitwarden
35
+ ```
36
+
37
+ ## Prerequisites
38
+
39
+ Download and install the [Bitwarden CLI](https://bitwarden.com/help/cli/#download-and-install) and log in to your account.
40
+
41
+ You should be able to run `bw get password <name_of_secret>` and get the value for the secret. This is the command that will be executed when pulling your secrets.
42
+
43
+ ## Configuration
44
+
45
+ Add the following to your `fujin.toml` file:
46
+
47
+ ```toml
48
+ [secrets]
49
+ adapter = "bitwarden"
50
+ password_env = "BW_PASSWORD"
51
+ ```
52
+
53
+ To unlock the Bitwarden vault, the password is required. Set the `BW_PASSWORD` environment variable in your shell. When Fujin signs in, it will always sync the vault first.
54
+
55
+ Alternatively, you can set the `BW_SESSION` environment variable. If `BW_SESSION` is present, Fujin will use it directly without signing in or syncing the vault. In this case, the `password_env` configuration is not required.
56
+
57
+ ## Usage
58
+
59
+ In your environment file (`.env` or configured via `envfile` in `fujin.toml`), prefix secret values with `$`:
60
+
61
+ ```env
62
+ DEBUG=False
63
+ AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
64
+ AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
65
+ ```
66
+
67
+ The `$` sign indicates to Fujin that it is a secret that should be resolved using the configured adapter.
68
+
69
+ ## How it Works
70
+
71
+ The adapter:
72
+ 1. Authenticates with Bitwarden using `BW_SESSION` or unlocks the vault with the configured password
73
+ 2. Syncs the vault (when unlocking)
74
+ 3. Resolves all secrets concurrently using `bw get password <name> --raw`
75
+ 4. Returns the resolved environment variables
76
+
77
+ ## Related
78
+
79
+ - [Fujin Documentation](https://github.com/Tobi-De/fujin)
80
+ - [Bitwarden CLI Documentation](https://bitwarden.com/help/cli/)
@@ -0,0 +1,60 @@
1
+ # Fujin Secrets - Bitwarden
2
+
3
+ Bitwarden secret adapter for Fujin deployment tool.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install fujin-secrets-bitwarden
9
+ ```
10
+
11
+ Or with uv:
12
+
13
+ ```bash
14
+ uv pip install fujin-secrets-bitwarden
15
+ ```
16
+
17
+ ## Prerequisites
18
+
19
+ Download and install the [Bitwarden CLI](https://bitwarden.com/help/cli/#download-and-install) and log in to your account.
20
+
21
+ You should be able to run `bw get password <name_of_secret>` and get the value for the secret. This is the command that will be executed when pulling your secrets.
22
+
23
+ ## Configuration
24
+
25
+ Add the following to your `fujin.toml` file:
26
+
27
+ ```toml
28
+ [secrets]
29
+ adapter = "bitwarden"
30
+ password_env = "BW_PASSWORD"
31
+ ```
32
+
33
+ To unlock the Bitwarden vault, the password is required. Set the `BW_PASSWORD` environment variable in your shell. When Fujin signs in, it will always sync the vault first.
34
+
35
+ Alternatively, you can set the `BW_SESSION` environment variable. If `BW_SESSION` is present, Fujin will use it directly without signing in or syncing the vault. In this case, the `password_env` configuration is not required.
36
+
37
+ ## Usage
38
+
39
+ In your environment file (`.env` or configured via `envfile` in `fujin.toml`), prefix secret values with `$`:
40
+
41
+ ```env
42
+ DEBUG=False
43
+ AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
44
+ AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
45
+ ```
46
+
47
+ The `$` sign indicates to Fujin that it is a secret that should be resolved using the configured adapter.
48
+
49
+ ## How it Works
50
+
51
+ The adapter:
52
+ 1. Authenticates with Bitwarden using `BW_SESSION` or unlocks the vault with the configured password
53
+ 2. Syncs the vault (when unlocking)
54
+ 3. Resolves all secrets concurrently using `bw get password <name> --raw`
55
+ 4. Returns the resolved environment variables
56
+
57
+ ## Related
58
+
59
+ - [Fujin Documentation](https://github.com/Tobi-De/fujin)
60
+ - [Bitwarden CLI Documentation](https://bitwarden.com/help/cli/)
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = [ "uv-build>=0.9.18,<0.10" ]
4
+
5
+ [project]
6
+ name = "fujin-secrets-bitwarden"
7
+ version = "0.16.0"
8
+ description = "Bitwarden secret adapter for Fujin"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Tobi", email = "tobidegnon@proton.me" },
12
+ ]
13
+ requires-python = ">=3.10"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3 :: Only",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "fujin-cli>=0.16",
24
+ "python-dotenv>=1.0.1",
25
+ ]
26
+
27
+ urls.Homepage = "https://github.com/Tobi-De/fujin"
28
+ urls.Issues = "https://github.com/Tobi-De/fujin/issues"
29
+ urls.Source = "https://github.com/Tobi-De/fujin"
30
+ entry-points."fujin.secrets".bitwarden = "fujin_secrets_bitwarden:bitwarden"
31
+
32
+ [tool.uv.sources]
33
+ fujin-cli = { workspace = true }
@@ -0,0 +1,165 @@
1
+ """Bitwarden secret adapter for Fujin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from contextlib import closing
9
+ from io import StringIO
10
+
11
+ from dotenv import dotenv_values
12
+ from fujin.config import SecretConfig
13
+ from fujin.errors import SecretResolutionError
14
+
15
+ __version__ = "0.1.0"
16
+
17
+
18
+ def bitwarden(env_content: str, secret_config: SecretConfig) -> str:
19
+ """Bitwarden secret adapter.
20
+
21
+ Retrieves secrets from Bitwarden vault using the `bw` CLI.
22
+ Handles concurrent resolution of multiple secrets for performance.
23
+
24
+ Configuration:
25
+ - Requires BW_SESSION environment variable, OR
26
+ - Set password_env to unlock vault using environment variable
27
+
28
+ Example fujin.toml:
29
+ [secrets]
30
+ adapter = "bitwarden"
31
+ password_env = "BW_PASSWORD" # Optional
32
+
33
+ Args:
34
+ env_content: Raw environment file content
35
+ secret_config: Secret configuration with adapter settings
36
+
37
+ Returns:
38
+ Resolved environment content with secrets replaced
39
+
40
+ Raises:
41
+ SecretResolutionError: If authentication fails or secret not found
42
+ """
43
+ # Parse env file
44
+ with closing(StringIO(env_content)) as buffer:
45
+ env_dict = dotenv_values(stream=buffer)
46
+
47
+ # Identify secrets (values starting with $)
48
+ secrets = {
49
+ key: value for key, value in env_dict.items() if value and value.startswith("$")
50
+ }
51
+
52
+ if not secrets:
53
+ return env_content
54
+
55
+ # Authenticate with Bitwarden
56
+ session = os.getenv("BW_SESSION")
57
+ if not session:
58
+ if not secret_config.password_env:
59
+ raise SecretResolutionError(
60
+ "You need to set the password_env to use the bitwarden adapter "
61
+ "or set the BW_SESSION environment variable",
62
+ adapter="bitwarden",
63
+ )
64
+ session = _signin(secret_config.password_env)
65
+
66
+ # Resolve secrets concurrently
67
+ resolved_secrets = {}
68
+ with ThreadPoolExecutor() as executor:
69
+ future_to_key = {
70
+ executor.submit(_read_secret, secret[1:], session): key
71
+ for key, secret in secrets.items()
72
+ }
73
+
74
+ for future in as_completed(future_to_key):
75
+ key = future_to_key[future]
76
+ try:
77
+ resolved_secrets[key] = future.result()
78
+ except Exception as e:
79
+ raise SecretResolutionError(
80
+ f"Failed to retrieve secret for {key}: {e}",
81
+ adapter="bitwarden",
82
+ key=key,
83
+ ) from e
84
+
85
+ # Merge resolved secrets back into env dict
86
+ env_dict.update(resolved_secrets)
87
+
88
+ return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
89
+
90
+
91
+ def _read_secret(name: str, session: str) -> str:
92
+ """Read a single secret from Bitwarden.
93
+
94
+ Args:
95
+ name: Secret name/identifier
96
+ session: Bitwarden session token
97
+
98
+ Returns:
99
+ Secret value
100
+
101
+ Raises:
102
+ SecretResolutionError: If secret not found
103
+ """
104
+ result = subprocess.run(
105
+ [
106
+ "bw",
107
+ "get",
108
+ "password",
109
+ name,
110
+ "--raw",
111
+ "--session",
112
+ session,
113
+ "--nointeraction",
114
+ ],
115
+ capture_output=True,
116
+ text=True,
117
+ )
118
+
119
+ if result.returncode != 0:
120
+ raise SecretResolutionError(
121
+ f"Password not found for {name}", adapter="bitwarden", key=name
122
+ )
123
+
124
+ return result.stdout.strip()
125
+
126
+
127
+ def _signin(password_env: str) -> str:
128
+ """Unlock Bitwarden vault and return session token.
129
+
130
+ Args:
131
+ password_env: Name of environment variable containing vault password
132
+
133
+ Returns:
134
+ Bitwarden session token
135
+
136
+ Raises:
137
+ SecretResolutionError: If authentication fails
138
+ """
139
+ # Sync vault first
140
+ sync_result = subprocess.run(["bw", "sync"], capture_output=True, text=True)
141
+ if sync_result.returncode != 0:
142
+ raise SecretResolutionError(
143
+ f"Bitwarden sync failed: {sync_result.stdout}", adapter="bitwarden"
144
+ )
145
+
146
+ # Unlock vault
147
+ unlock_result = subprocess.run(
148
+ [
149
+ "bw",
150
+ "unlock",
151
+ "--nointeraction",
152
+ "--passwordenv",
153
+ password_env,
154
+ "--raw",
155
+ ],
156
+ capture_output=True,
157
+ text=True,
158
+ )
159
+
160
+ if unlock_result.returncode != 0:
161
+ raise SecretResolutionError(
162
+ f"Bitwarden unlock failed: {unlock_result.stderr}", adapter="bitwarden"
163
+ )
164
+
165
+ return unlock_result.stdout.strip()