Skip to content

Commit 4f49daa

Browse files
committed
Add HashiCorp Vault Manager
Signed-off-by: Alberto Ferrer Sánchez <alberefe@gmail.com>
1 parent f455a55 commit 4f49daa

File tree

7 files changed

+692
-136
lines changed

7 files changed

+692
-136
lines changed

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,91 @@ _NOTE: the parameter "item_name" corresponds with the field "name" of the json.
174174

175175
The module uses the [Bitwarden CLI](https://bitwarden.com/help/cli/) to interact with Bitwarden.
176176

177+
### HashiCorp Vault
178+
179+
180+
#### Installing the dependency
181+
182+
This module uses hvac library, which is set as optional module in pyproject.toml.
183+
184+
1. Normal install: poetry install --with hashicorp-manager
185+
2. For development: poetry install --with hashicorp-manager --with dev
186+
187+
188+
#### Example use
189+
190+
```python
191+
from grimoirelab_toolkit.credential_manager.hc_manager import HashicorpManager
192+
193+
194+
# Instantiate the HashiCorp Vault manager using the vault URL and token
195+
# The certificate can be a boolean (True/False) or a path to a CA bundle file
196+
hc_manager = HashicorpManager("https://vault.example.com", "your_token", certificate=True)
197+
198+
# Retrieve a secret from HashiCorp Vault
199+
github_secret = hc_manager.get_secret("github")
200+
elasticsearch_secret = hc_manager.get_secret("elasticsearch")
201+
```
202+
203+
#### Response format
204+
205+
When calling `get_secret(item_name)`, the method returns a JSON object with the following structure:
206+
207+
_NOTE: the parameter "item_name" corresponds to the secret path in HashiCorp Vault._
208+
209+
##### Example Response
210+
211+
```json
212+
{
213+
"request_id": "d09e2bb5-00ee-576b-6078-5d291d35ccc3",
214+
"lease_id": "",
215+
"renewable": false,
216+
"lease_duration": 0,
217+
"data": {
218+
"data": {
219+
"username": "test_user",
220+
"password": "test_pass",
221+
"api_key": "test_key"
222+
},
223+
"metadata": {
224+
"created_time": "2024-11-23T12:20:59.985132927Z",
225+
"custom_metadata": null,
226+
"deletion_time": "",
227+
"destroyed": false,
228+
"version": 1
229+
}
230+
},
231+
"wrap_info": null,
232+
"warnings": null,
233+
"auth": null,
234+
"mount_type": "kv"
235+
}
236+
```
237+
238+
Field Descriptions
239+
240+
- request_id: Unique identifier for this Vault request
241+
- lease_id: Lease identifier for renewable secrets (empty for KV secrets)
242+
- renewable: Boolean indicating if the secret is renewable
243+
- lease_duration: Lease duration in seconds (0 for KV secrets)
244+
- data: Main data object containing the secret
245+
- data: The actual secret key-value pairs
246+
- username: Username credential
247+
- password: Password credential
248+
- api_key: API key or other custom fields
249+
- metadata: Vault metadata for this secret
250+
- created_time: Secret creation timestamp (ISO 8601 format)
251+
- custom_metadata: Custom metadata if configured
252+
- deletion_time: Soft deletion timestamp (empty if not deleted)
253+
- destroyed: Boolean indicating if secret version is destroyed
254+
- version: Secret version number
255+
- wrap_info: Response wrapping information (null if not wrapped)
256+
- warnings: Array of warning messages (null if none)
257+
- auth: Authentication information (null for read operations)
258+
- mount_type: Type of secrets engine (typically "kv" for key-value)
259+
260+
The module uses the [hvac](https://hvac.readthedocs.io/) Python library to interact with HashiCorp Vault.
261+
177262
## License
178263

179264
Licensed under GNU General Public License (GPL), version 3 or later.

grimoirelab_toolkit/credential_manager/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"InvalidCredentialsError",
2727
"CredentialNotFoundError",
2828
"BitwardenCLIError",
29+
"HashicorpVaultError",
2930
]
3031

3132

@@ -51,3 +52,9 @@ class BitwardenCLIError(CredentialManagerError):
5152
"""Raised for Bitwarden CLI specific errors."""
5253

5354
pass
55+
56+
57+
class HashicorpVaultError(CredentialManagerError):
58+
"""Raised for HashiCorp Vault-specific operation errors."""
59+
60+
pass
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Copyright (C) Grimoirelab Contributors
4+
#
5+
# This program is free software; you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation; either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
#
18+
19+
import logging
20+
21+
import hvac
22+
import hvac.exceptions
23+
24+
from .exceptions import HashicorpVaultError, CredentialNotFoundError
25+
26+
logger = logging.getLogger(__name__)
27+
28+
29+
class HashicorpManager:
30+
"""Retrieve credentials from HashiCorp Vault.
31+
32+
This class defines functions to initialize a client and retrieve
33+
secrets from HashiCorp Vault. The workflow is:
34+
35+
manager = HashicorpManager(vault_url, token, certificate)
36+
manager.get_secret("github")
37+
manager.get_secret("elasticsearch")
38+
39+
The manager initializes the client using the vault_url, token,
40+
and certificate given as arguments when creating the instance,
41+
so the object is reusable along the program.
42+
43+
The get_secret function returns the whole item object, with metadata
44+
included, so the user can choose to store it and retrieve desired data.
45+
"""
46+
47+
def __init__(self, vault_url: str, token: str, certificate: str | bool = None):
48+
"""
49+
Creates HashicorpManager object using token authentication
50+
51+
:param str vault_url: The URL of the vault
52+
:param str token: The access token for authentication
53+
:param Union[str, bool, None] certificate: TLS verification setting. Either a boolean to indicate whether TLS
54+
verification should be performed, a string pointing at the CA bundle to use for
55+
verification
56+
57+
:raises ConnectionError: If connection issues occur
58+
"""
59+
try:
60+
logger.debug("Creating Vault client")
61+
# Initialize client with URL, token, and certificate verification setting
62+
self.client = hvac.Client(url=vault_url, token=token, verify=certificate)
63+
logger.debug("Vault client initialized successfully")
64+
except Exception as e:
65+
logger.error("An error occurred initializing the client: %s", str(e))
66+
raise e
67+
68+
def get_secret(self, item_name: str) -> dict:
69+
"""Retrieve an item from the HashiCorp Vault.
70+
71+
Retrieves all the fields stored for an item with the name
72+
provided as an argument and returns them as a dictionary.
73+
74+
The returned dictionary includes fields such as:
75+
- data: The actual secret data and metadata
76+
- request_id, lease_id, renewable, lease_duration
77+
- Other vault metadata
78+
79+
:param str item_name: The name of the item to retrieve
80+
81+
:returns: Dictionary containing the secret data and metadata
82+
:rtype: dict
83+
84+
:raises CredentialNotFoundError: If the secret path is not found
85+
:raises HashicorpVaultError: If Vault operations fail
86+
"""
87+
try:
88+
logger.info("Retrieving credentials from vault: %s", item_name)
89+
# Read secret from KV secrets engine
90+
secret = self.client.secrets.kv.read_secret(path=item_name)
91+
return secret
92+
except hvac.exceptions.InvalidPath:
93+
logger.error("The path %s does not exist in the vault", item_name)
94+
raise CredentialNotFoundError(
95+
f"Secret path '{item_name}' not found in Vault"
96+
)
97+
except Exception as e:
98+
logger.error("Error retrieving the secret: %s", str(e))
99+
raise HashicorpVaultError(f"Vault operation failed: {e}")

0 commit comments

Comments
 (0)