aleph-secrets-manager 0.1.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,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ **/.python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ **.env*
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, AlephTechAi
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: aleph-secrets-manager
3
+ Version: 0.1.0
4
+ Summary: Tool for managing secrets in Azure Key Vault
5
+ Author-email: Zachary Lau <zachary.lau@alephtech.ai>
6
+ License: BSD-3-Clause
7
+ License-File: LICENSE
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Requires-Python: >=3.8
10
+ Requires-Dist: azure-identity>=1.21.0
11
+ Requires-Dist: azure-keyvault-secrets>=4.9.0
12
+ Requires-Dist: pydantic>=2.10.6
13
+ Requires-Dist: pynacl>=1.6.0
14
+ Requires-Dist: python-dotenv>=1.0.1
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Aleph Secrets Manager
18
+
19
+ A simple Python tool for managing secrets in Azure Key Vault using a command-line interface (CLI).
20
+
21
+ You may also use the underlying Azure Key Vault adaptor directly if for instance you want to load secrets into an application programmatically.
22
+
23
+ ## Purpose
24
+ - **Read, write, and delete secrets** in Azure Key Vault from the command line.
25
+ - Supports bulk upload/download from `.env` files.
26
+
27
+ ## Dependencies
28
+ Install the following Python packages (see [`aleph_secrets_manager/requirements.txt`](./aleph_secrets_manager/requirements.txt)):
29
+
30
+ - `azure-identity`
31
+ - `azure-keyvault-secrets`
32
+ - `pydantic`
33
+ - `python-dotenv`
34
+
35
+ Install dependencies:
36
+ ```sh
37
+ pip install -r aleph_secrets_manager/requirements.txt
38
+ ```
39
+
40
+ ## CLI Usage
41
+
42
+ Run the CLI from the project root:
43
+
44
+ ### Read all secrets from Key Vault and save to a .env file
45
+ ```sh
46
+ python cli.py -v <vault-name> read -f .env
47
+ ```
48
+
49
+ ### Write all secrets from a .env file to Key Vault
50
+ ```sh
51
+ python cli.py -v <vault-name> write -f .env
52
+ ```
53
+
54
+ ### Delete secrets from Key Vault
55
+ ```sh
56
+ python cli.py -v <vault-name> delete -k SECRET_1 -k SECRET_2
57
+ ```
58
+
59
+ - Replace `<vault-name>` with your Azure Key Vault name.
60
+ - The `-f` flag specifies the path to your `.env` file.
61
+ - The `-k` flag can be repeated for each secret key to delete.
62
+
63
+ ## Adaptor Usage
64
+ ```python
65
+ # Initialize the secrets manager
66
+ vault_name = "test-kv"
67
+ secrets_manager = AzureSecretsManager(AzureClientFactory.create(vault_name))
68
+
69
+ # Read a secret
70
+ secrets_manager.read_secret("SECRET-1")
71
+
72
+ # Write secret
73
+ new_secret = Secret(key="NULL_SECRET", value="test123")
74
+ secrets_manager.write_secret(new_secret)
75
+
76
+ # List all secrets
77
+ secrets_manager.list_all_secrets()
78
+
79
+ # Read all secrets
80
+ secrets_manager.read_all_secrets()
81
+
82
+ # Download all
83
+ env_path = Path().parent.parent / ".env"
84
+ secrets_manager.download_all_to_env_file(env_path)
85
+
86
+ # Upload from .env
87
+ secrets_manager.upload_from_env_file(env_path)
88
+
89
+ # Delete a secret
90
+ secrets_manager.delete_secret('SECRET_2')
91
+ secrets_manager.delete_secret('SECRET_1')
92
+
93
+ ```
94
+
95
+ ## Authentication
96
+ This tool uses Azure's `DefaultAzureCredential`. Make sure you are authenticated (e.g., via `az login`) and have access to the Key Vault (e.g. Key Vault Secrets User).
97
+
98
+ ---
@@ -0,0 +1,82 @@
1
+ # Aleph Secrets Manager
2
+
3
+ A simple Python tool for managing secrets in Azure Key Vault using a command-line interface (CLI).
4
+
5
+ You may also use the underlying Azure Key Vault adaptor directly if for instance you want to load secrets into an application programmatically.
6
+
7
+ ## Purpose
8
+ - **Read, write, and delete secrets** in Azure Key Vault from the command line.
9
+ - Supports bulk upload/download from `.env` files.
10
+
11
+ ## Dependencies
12
+ Install the following Python packages (see [`aleph_secrets_manager/requirements.txt`](./aleph_secrets_manager/requirements.txt)):
13
+
14
+ - `azure-identity`
15
+ - `azure-keyvault-secrets`
16
+ - `pydantic`
17
+ - `python-dotenv`
18
+
19
+ Install dependencies:
20
+ ```sh
21
+ pip install -r aleph_secrets_manager/requirements.txt
22
+ ```
23
+
24
+ ## CLI Usage
25
+
26
+ Run the CLI from the project root:
27
+
28
+ ### Read all secrets from Key Vault and save to a .env file
29
+ ```sh
30
+ python cli.py -v <vault-name> read -f .env
31
+ ```
32
+
33
+ ### Write all secrets from a .env file to Key Vault
34
+ ```sh
35
+ python cli.py -v <vault-name> write -f .env
36
+ ```
37
+
38
+ ### Delete secrets from Key Vault
39
+ ```sh
40
+ python cli.py -v <vault-name> delete -k SECRET_1 -k SECRET_2
41
+ ```
42
+
43
+ - Replace `<vault-name>` with your Azure Key Vault name.
44
+ - The `-f` flag specifies the path to your `.env` file.
45
+ - The `-k` flag can be repeated for each secret key to delete.
46
+
47
+ ## Adaptor Usage
48
+ ```python
49
+ # Initialize the secrets manager
50
+ vault_name = "test-kv"
51
+ secrets_manager = AzureSecretsManager(AzureClientFactory.create(vault_name))
52
+
53
+ # Read a secret
54
+ secrets_manager.read_secret("SECRET-1")
55
+
56
+ # Write secret
57
+ new_secret = Secret(key="NULL_SECRET", value="test123")
58
+ secrets_manager.write_secret(new_secret)
59
+
60
+ # List all secrets
61
+ secrets_manager.list_all_secrets()
62
+
63
+ # Read all secrets
64
+ secrets_manager.read_all_secrets()
65
+
66
+ # Download all
67
+ env_path = Path().parent.parent / ".env"
68
+ secrets_manager.download_all_to_env_file(env_path)
69
+
70
+ # Upload from .env
71
+ secrets_manager.upload_from_env_file(env_path)
72
+
73
+ # Delete a secret
74
+ secrets_manager.delete_secret('SECRET_2')
75
+ secrets_manager.delete_secret('SECRET_1')
76
+
77
+ ```
78
+
79
+ ## Authentication
80
+ This tool uses Azure's `DefaultAzureCredential`. Make sure you are authenticated (e.g., via `az login`) and have access to the Key Vault (e.g. Key Vault Secrets User).
81
+
82
+ ---
@@ -0,0 +1,5 @@
1
+ azure-identity
2
+ azure-keyvault-secrets
3
+ pydantic
4
+ python-dotenv
5
+ pynacl
@@ -0,0 +1,89 @@
1
+ from time import sleep
2
+ from azure.keyvault.secrets import SecretClient
3
+ from azure.identity import DefaultAzureCredential
4
+ from azure.core.exceptions import ResourceExistsError, ClientAuthenticationError, ResourceNotFoundError
5
+ import re
6
+ import logging
7
+
8
+ from aleph_secrets_manager.src.port import Secret, SecretsManagerPort
9
+
10
+
11
+ class AzureClientFactory:
12
+ @staticmethod
13
+ def create(vault_name: str) -> SecretClient:
14
+ credential = DefaultAzureCredential()
15
+ vault_url = f"https://{vault_name}.vault.azure.net"
16
+ client = SecretClient(vault_url=vault_url, credential=credential)
17
+ return client
18
+
19
+
20
+ class AzureSecretsManager(SecretsManagerPort):
21
+ def __init__(self, client: SecretClient):
22
+ self.client = client
23
+
24
+ def write_secret(self, secret):
25
+ converted_key = self._convert_write(secret.key)
26
+ successfully_deleted = False
27
+
28
+ while not successfully_deleted:
29
+ try:
30
+ self.client.set_secret(converted_key, secret.value)
31
+ successfully_deleted = True
32
+ except ResourceExistsError as e:
33
+ code = self._extract_error_code(e.message)
34
+
35
+ if self._is_secret_recoverable(code):
36
+ self.client.begin_recover_deleted_secret(converted_key)
37
+
38
+ logging.warning(f"🚨 Encountered error writing {secret.key}: \n{e}")
39
+ logging.warning(f"Retrying writing of {secret.key}...")
40
+ sleep(1)
41
+ except Exception as e:
42
+ raise
43
+
44
+ def _extract_error_code(self, error_msg: str) -> str:
45
+ """Get the internal error code for an Azure Resource error"""
46
+ match = re.search(r'"code":\s*"([^"]+)"', error_msg)
47
+ code = match.group(1)
48
+ return code
49
+
50
+ def _is_secret_recoverable(self, code: str) -> bool:
51
+ """Checks if a secret key can be recovered from an error code"""
52
+ return code == "ObjectIsDeletedButRecoverable"
53
+
54
+ def read_secret(self, key):
55
+ converted_key = self._convert_write(key)
56
+
57
+ try:
58
+ az_secret = self.client.get_secret(converted_key)
59
+ return Secret(key=self._convert_read(az_secret.name), value=az_secret.value)
60
+ except ResourceNotFoundError:
61
+ raise ValueError(f"❌ Failed to find secret '{key}' in {self.client.vault_url}")
62
+ except Exception as e:
63
+ raise
64
+
65
+ def delete_secret(self, key):
66
+ converted_key = self._convert_write(key)
67
+
68
+ try:
69
+ deleted_secret = self.client.begin_delete_secret(converted_key).result()
70
+ except ResourceNotFoundError:
71
+ raise ValueError(f"❌ Failed to delete secret '{key}' from {self.client.vault_url} as it does not exist.")
72
+ except Exception as e:
73
+ raise
74
+
75
+ def list_all_secrets(self):
76
+ return [
77
+ self._convert_read(secret_properties.name)
78
+ for secret_properties in self.client.list_properties_of_secrets()
79
+ ]
80
+
81
+ def read_all_secrets(self):
82
+ keys = self.list_all_secrets()
83
+ return [self.read_secret(key) for key in keys]
84
+
85
+ def _convert_read(self, key: str):
86
+ return key.replace('-', '_')
87
+
88
+ def _convert_write(self, key: str):
89
+ return key.replace('_', '-')
@@ -0,0 +1,108 @@
1
+ from aleph_secrets_manager.src.port import Secret, SecretsManagerPort
2
+
3
+ import requests
4
+ import logging
5
+ from nacl import public, encoding
6
+ from base64 import b64encode
7
+ from pydantic import BaseModel
8
+
9
+
10
+ class GitHubPublicKey(BaseModel):
11
+ id: str
12
+ value: str
13
+
14
+
15
+ # Guide: https://gist.github.com/comdotlinux/9a53bb00767a16d6646464c4b8249094
16
+ # REST API Reference: https://docs.github.com/en/rest/actions?apiVersion=2022-11-28#secrets
17
+ class GitHubSecretsManager(SecretsManagerPort):
18
+ def __init__(self, org: str, repo: str, auth_token: str):
19
+ self.base_url = "https://api.github.com"
20
+ self.org = org
21
+ self.repo = repo
22
+ self.token = auth_token
23
+ self.headers = {
24
+ "Authorization": f"Bearer {self.token}",
25
+ "X-GitHub-Api-Version": "2022-11-28"
26
+ }
27
+ raw_public_key = self._get_public_key(
28
+ base_url=self.base_url,
29
+ org=self.org,
30
+ repo=self.repo,
31
+ auth_token=self.token
32
+ )
33
+ self.public_key = GitHubPublicKey(id=raw_public_key['key_id'], value=raw_public_key['key'])
34
+
35
+
36
+ def _encrypt(self, public_key: str, unecrypted_value: str) -> str:
37
+ """Encrypt a secret value using the repo public key"""
38
+
39
+ encoding_system = "utf-8"
40
+ public_key_obj = public.PublicKey(public_key.encode(encoding_system), encoder=encoding.Base64Encoder())
41
+ sealed_box = public.SealedBox(public_key_obj)
42
+ encrypted_value = sealed_box.encrypt(unecrypted_value.encode(encoding_system))
43
+
44
+ return b64encode(encrypted_value).decode(encoding_system)
45
+
46
+ def _get_public_key(self, base_url: str, org: str, repo: str, auth_token: str):
47
+ url = f"{base_url}/repos/{org}/{repo}/actions/secrets/public-key"
48
+ headers = {"Authorization": f"Bearer {auth_token}"}
49
+
50
+ response = requests.get(url=url, headers=headers, timeout=60)
51
+ if response.status_code != 200:
52
+ logging.error(f"Response: {response.json()}")
53
+ raise IOError(f"❌ Failed to get public key for {org} ({response.status_code}): \n{response.json()}")
54
+
55
+ public_key_json = response.json()
56
+ return public_key_json
57
+
58
+ def list_all_secrets(self):
59
+ """Retrieve all environment and repo secrets"""
60
+ secrets = []
61
+ url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets"
62
+
63
+ response = requests.get(url=url, headers=self.headers, timeout=60)
64
+ if not response.ok:
65
+ logging.error(f"Response: {response.json()}")
66
+ raise IOError(f"❌ Failed to list secrets for {self.org} ({response.status_code}): \n{response.json()}")
67
+
68
+ secrets += response.json()["secrets"]
69
+ return [s['name'] for s in secrets]
70
+
71
+ def write_secret(self, secret: Secret):
72
+ """Only repository secrets should be written"""
73
+
74
+ encrypted_value: str = self._encrypt(self.public_key.value, secret.value)
75
+ url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{secret.key}"
76
+ data = {
77
+ "encrypted_value": encrypted_value,
78
+ "key_id": self.public_key.id
79
+ }
80
+
81
+ response = requests.put(url=url, headers=self.headers, timeout=60, json=data)
82
+ if not response.ok:
83
+ logging.error(f"Response: {response.json()}")
84
+ raise IOError(f"❌ Failed to write secret {secret.key} ({response.status_code}): \n{response.json()}")
85
+
86
+ def read_secret(self, key):
87
+ """GitHub does not allow reading of secrets"""
88
+
89
+ url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{key}"
90
+ response = requests.get(url=url, headers=self.headers, timeout=60)
91
+ if not response.ok:
92
+ logging.error(f"Response: {response.json()}")
93
+ raise IOError(f"❌ Failed to read secret {key} ({response.status_code}): \n{response.json()}")
94
+
95
+ return Secret(key=response.json()['name'], value="")
96
+
97
+ def delete_secret(self, key):
98
+ url = f"{self.base_url}/repos/{self.org}/{self.repo}/actions/secrets/{key}"
99
+ response = requests.delete(url=url, headers=self.headers, timeout=60)
100
+ if not response.ok:
101
+ logging.error(f"Response: {response.json()}")
102
+ if response.status_code == 404:
103
+ raise ValueError(f"❌ Failed to delete secret {key}: Does not exist")
104
+ raise IOError(f"❌ Failed to delete secret {key} ({response.status_code}): \n{response.json()}")
105
+
106
+ def read_all_secrets(self):
107
+ secret_strings = self.list_all_secrets()
108
+ return [self.read_secret(key) for key in secret_strings]
@@ -0,0 +1,55 @@
1
+ from abc import ABC
2
+ from pathlib import Path
3
+ from pydantic import BaseModel
4
+ from dotenv import dotenv_values
5
+
6
+ import typing as T
7
+ import logging
8
+
9
+
10
+ class Secret(BaseModel):
11
+ key: str
12
+ value: str
13
+
14
+
15
+ class SecretsManagerPort(ABC):
16
+ def write_secret(self, secret: Secret):
17
+ """Upload a secret to secret vault/manager"""
18
+ raise NotImplementedError
19
+
20
+ def read_secret(self, key: str) -> Secret:
21
+ """Download a secret from a secrets vault/manager"""
22
+ raise NotImplementedError
23
+
24
+ def delete_secret(self, key: str):
25
+ """Delete a secret from secret vault/manager"""
26
+ raise NotImplementedError
27
+
28
+ def list_all_secrets(self) -> T.List[str]:
29
+ """List all secret names/keys from a secrets vault/manager"""
30
+ raise NotImplementedError
31
+
32
+ def read_all_secrets(self) -> T.List[Secret]:
33
+ """Download all secrets"""
34
+ raise NotImplementedError
35
+
36
+ def download_all_to_env_file(self, env_path: Path):
37
+ """Download all secrets from a secrets vault/manager to a `.env` file"""
38
+
39
+ secrets = self.read_all_secrets()
40
+ with open(env_path, "w") as f:
41
+ for secret in secrets:
42
+ f.write(f"{secret.key}={secret.value}\n")
43
+
44
+ logging.info(f"✅ Downloaded {len(secrets)} secrets to {str(env_path)}!")
45
+
46
+
47
+ def upload_from_env_file(self, env_path: Path):
48
+ """Upload all secrets to secret vault/manager from env file"""
49
+
50
+ kv_pairs = dotenv_values(env_path)
51
+ for key, value in kv_pairs.items():
52
+ self.write_secret(Secret(key=key, value=value))
53
+
54
+ logging.info(f"✅ Uploaded secrets from {str(env_path)}!")
55
+