Skip to content

Commit 447d0c2

Browse files
committed
Integration Secrets Manager in Perceval
Signed-off-by: Alberto Ferrer Sánchez <alberefe@gmail.com>
1 parent 506f754 commit 447d0c2

20 files changed

Lines changed: 911 additions & 327 deletions

README.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,132 @@ A Perceval Docker image is available at
143143
Detailed information on how to run and/or build this image can be found
144144
[here](https://github.com/chaoss/grimoirelab-perceval/tree/main/docker/images/).
145145

146+
## Secrets Manager
147+
148+
Perceval supports retrieving credentials from a secrets manager instead of
149+
passing them directly on the command line. This is useful for automated
150+
pipelines and environments where storing credentials in plain text is not
151+
acceptable.
152+
153+
The following backends support secrets manager authentication: `bugzilla`,
154+
`bugzillarest`, `confluence`, `discourse`, `gerrit`, `git`, `github`,
155+
`gitlab`, `gitter`, `googlehits`, `groupsio`, `jenkins`, `rocketchat`,
156+
`stackexchange`.
157+
158+
### Installation
159+
160+
To use **HashiCorp Vault**, install Perceval with the `hashicorp-manager` group:
161+
162+
```
163+
$ poetry install --with hashicorp-manager
164+
```
165+
166+
**Bitwarden** does not require any extra Python package, but the
167+
[Bitwarden CLI](https://bitwarden.com/help/cli/) (`bw`) must be installed
168+
and available on your `PATH`.
169+
170+
### Common arguments
171+
172+
All secrets manager providers share the following arguments:
173+
174+
- `--secrets-manager` — Provider to use: `bitwarden` or `hashicorp`
175+
- `--item-name` — Name of the secret item in the secrets manager
176+
177+
Credentials must be stored in the vault using the same field names that
178+
Perceval expects: `user`, `password`, `api_token`, `email`, `access_token`,
179+
`user_id`. Perceval automatically looks up all of these fields and uses
180+
whichever ones are present.
181+
182+
### Expected field names
183+
184+
Store your credentials in the vault using these exact names:
185+
186+
- `api_token` — used by `github`, `gitlab`, `bugzillarest`, `stackexchange`, `gitter`
187+
- `user` — used by `bugzilla`, `confluence`, `discourse`, `gerrit`, `jenkins`
188+
- `password` — used by `bugzilla`, `confluence`, `discourse`, `gerrit`, `jenkins`
189+
- `email` — used by `groupsio`
190+
- `access_token` — used by `groupsio`
191+
- `user_id` — used by `rocketchat`
192+
193+
Only the fields present in the vault are used; the rest are silently ignored.
194+
195+
### Bitwarden
196+
197+
Store your credentials in a Bitwarden item using Perceval's expected field
198+
names. Fields can be stored as login fields or as custom fields.
199+
200+
For example, to store a GitHub API token, create a Bitwarden item named
201+
`GitHub` with a custom field named `api_token` containing the token value.
202+
203+
Bitwarden-specific arguments:
204+
205+
- `--bw-client-id` — Bitwarden API client ID
206+
- `--bw-client-secret` — Bitwarden API client secret
207+
- `--bw-master-password` — Bitwarden master password
208+
209+
#### Example
210+
```
211+
$ perceval github \
212+
--secrets-manager bitwarden \
213+
--item-name 'GitHub' \
214+
--bw-client-id $BW_CLIENT_ID \
215+
--bw-client-secret $BW_CLIENT_SECRET \
216+
--bw-master-password $BW_MASTER_PASSWORD \
217+
--from-date '2020-01-01' --no-archive \
218+
chaoss grimoirelab-perceval
219+
```
220+
221+
### HashiCorp Vault
222+
223+
Store your credentials as key-value pairs in a HashiCorp Vault KV secret
224+
using Perceval's expected field names.
225+
226+
For example, to store a GitHub API token:
227+
```
228+
$ vault kv put secret/GitHub api_token=ghp_xxxxxxxxxxxx
229+
```
230+
231+
HashiCorp-specific arguments:
232+
233+
- `--vault-url` — HashiCorp Vault server URL
234+
- `--vault-token` — Vault authentication token
235+
- `--vault-certificate` — Path to CA certificate for TLS verification (optional)
236+
237+
#### Example
238+
```
239+
$ perceval github \
240+
--secrets-manager hashicorp \
241+
--item-name 'GitHub' \
242+
--vault-url $VAULT_URL \
243+
--vault-token $VAULT_TOKEN \
244+
--from-date '2020-01-01' --no-archive \
245+
chaoss grimoirelab-perceval
246+
```
247+
248+
### Programmatic usage
249+
250+
The credential resolution logic is also available in `grimoirelab-toolkit`
251+
via the `CredentialManager` base class, so it can be used from any Python
252+
code without going through the CLI:
253+
254+
```python
255+
from grimoirelab_toolkit.credential_manager import BitwardenManager
256+
from grimoirelab_toolkit.credential_manager.hc_manager import HashicorpManager
257+
258+
# Bitwarden example
259+
bw_manager = BitwardenManager("your-client-id", "your-client-secret", "your-master-password")
260+
credentials = bw_manager.resolve_credentials("GitHub", ["api_token"])
261+
print(credentials) # {'api_token': 'ghp_...'}
262+
263+
# HashiCorp Vault example
264+
hc_manager = HashicorpManager("https://vault.example.com", "hvs.your-token")
265+
credentials = hc_manager.resolve_credentials("secret/GitHub", ["api_token"])
266+
print(credentials) # {'api_token': 'ghp_...'}
267+
```
268+
269+
This is useful for consumers like KingArthur or custom scripts that use
270+
Perceval's `Backend` class directly without the CLI.
271+
146272
## Documentation
147273

148274
Documentation is generated automatically in the [ReadTheDocs Perceval

perceval/backend.py

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -617,12 +617,14 @@ class BackendCommandArgumentParser:
617617

618618
def __init__(self, backend, from_date=False, to_date=False, offset=False,
619619
basic_auth=False, token_auth=False, archive=False,
620-
aliases=None, blacklist=False, ssl_verify=False):
620+
aliases=None, blacklist=False, ssl_verify=False,
621+
secrets_manager=False):
621622
self._from_date = from_date
622623
self._to_date = to_date
623624
self._archive = archive
624625
self._backend = backend
625626
self._ssl_verify = ssl_verify
627+
self._secrets_manager = secrets_manager
626628

627629
self.aliases = aliases or {}
628630
self.parser = argparse.ArgumentParser()
@@ -673,6 +675,9 @@ def __init__(self, backend, from_date=False, to_date=False, offset=False,
673675
group.add_argument('--no-ssl-verify', dest='ssl_verify', action='store_false',
674676
help="disable SSL verification")
675677

678+
if secrets_manager:
679+
self._set_secrets_manager_arguments()
680+
676681
self._set_output_arguments()
677682

678683
def parse(self, *args):
@@ -749,6 +754,38 @@ def _set_output_arguments(self):
749754
group.add_argument('--json-line', dest='json_line', action='store_true',
750755
help="produce a JSON line for each output item")
751756

757+
def _set_secrets_manager_arguments(self):
758+
"""Activate secret manager arguments parsing"""
759+
760+
group = self.parser.add_argument_group('secrets manager arguments')
761+
group.add_argument('--secrets-manager', dest='secrets_manager',
762+
choices=['bitwarden', 'hashicorp'],
763+
help="Secrets manager service to use for credential retrieval")
764+
group.add_argument('--item-name', dest='item_name',
765+
help="Name of the item in the secrets manager")
766+
767+
bw_group = self.parser.add_argument_group('bitwarden arguments')
768+
bw_group.add_argument('--bw-client-id', dest='bw_client_id',
769+
default=os.environ.get('PERCEVAL_BW_CLIENT_IDt '),
770+
help="Bitwarden API client ID (env: PERCEVAL_BW_CLIENT_ID)")
771+
bw_group.add_argument('--bw-client-secret', dest='bw_client_secret',
772+
default=os.environ.get('PERCEVAL_BW_CLIENT_SECRET'),
773+
help="Bitwarden API client secret (env: PERCEVAL_BW_CLIENT_SECRET)")
774+
bw_group.add_argument('--bw-master-password', dest='bw_master_password',
775+
default=os.environ.get('PERCEVAL_BW_MASTER_PASSWORD'),
776+
help="Bitwarden master password (env: PERCEVAL_BW_MASTER_PASSWORD)")
777+
778+
hc_group = self.parser.add_argument_group('hashicorp vault arguments')
779+
hc_group.add_argument('--vault-url', dest='vault_url',
780+
default=os.environ.get('PERCEVAL_VAULT_URL'),
781+
help="HashiCorp Vault URL (env: PERCEVAL_VAULT_URL)")
782+
hc_group.add_argument('--vault-token', dest='vault_token',
783+
default=os.environ.get('PERCEVAL_VAULT_TOKEN'),
784+
help="HashiCorp Vault authentication token (env: PERCEVAL_VAULT_TOKEN)")
785+
hc_group.add_argument('--vault-certificate', dest='vault_certificate',
786+
default=os.environ.get('PERCEVAL_VAULT_CERTIFICATE'),
787+
help="Path to CA certificate for HashiCorp Vault TLS verification (env: PERCEVAL_VAULT_CERTIFICATE)")
788+
752789

753790
class BackendCommand:
754791
"""Abstract class to run backends from the command line.
@@ -822,8 +859,76 @@ def run(self):
822859
logger.exception(f"Error!: {e}", exc_info=self.debug)
823860

824861
def _pre_init(self):
825-
"""Override to execute before backend is initialized."""
826-
pass
862+
"""Override to execute before backend is initialized.
863+
864+
This method handles fetching credentials from a secrets manager
865+
and injecting them into backend arguments.
866+
"""
867+
if not (hasattr(self.parsed_args, 'secrets_manager') and
868+
self.parsed_args.secrets_manager):
869+
return
870+
871+
if not getattr(self.parsed_args, 'item_name', None):
872+
raise ValueError("--item-name is required when --secrets-manager is specified.")
873+
874+
logging.debug("Processing credentials with %s", self.parsed_args.secrets_manager)
875+
876+
try:
877+
manager = self._build_manager()
878+
field_names = ['user', 'password', 'api_token', 'email', 'access_token', 'user_id']
879+
880+
credentials = manager.resolve_credentials(
881+
secret_name=self.parsed_args.item_name,
882+
field_names=field_names,
883+
)
884+
885+
# Post-process: GitHub backend expects api_token as a list
886+
if 'api_token' in credentials:
887+
credentials['api_token'] = [credentials['api_token']]
888+
889+
# Inject resolved credentials into parsed_args
890+
for param_name, value in credentials.items():
891+
setattr(self.parsed_args, param_name, value)
892+
logger.info('Using %s from secrets manager', param_name)
893+
894+
except ImportError:
895+
logging.warning('Credential management module not found. Using command line credentials.')
896+
except Exception as e:
897+
raise RuntimeError('Error retrieving credentials from secret manager: %s' % str(e)) from e
898+
899+
def _build_manager(self):
900+
"""Build and return a credential manager instance from parsed CLI args.
901+
902+
:returns: A credential manager instance
903+
:rtype: CredentialManager
904+
"""
905+
manager_type = self.parsed_args.secrets_manager
906+
907+
if manager_type == 'bitwarden':
908+
bw_client_id = getattr(self.parsed_args, 'bw_client_id', None)
909+
bw_client_secret = getattr(self.parsed_args, 'bw_client_secret', None)
910+
bw_master_password = getattr(self.parsed_args, 'bw_master_password', None)
911+
912+
if not all([bw_client_id, bw_client_secret, bw_master_password]):
913+
raise ValueError(
914+
'Bitwarden requires --bw-client-id, --bw-client-secret, and --bw-master-password'
915+
)
916+
917+
from grimoirelab_toolkit.credential_manager.bw_manager import BitwardenManager
918+
return BitwardenManager(bw_client_id, bw_client_secret, bw_master_password)
919+
920+
elif manager_type == 'hashicorp':
921+
vault_url = getattr(self.parsed_args, 'vault_url', None)
922+
vault_token = getattr(self.parsed_args, 'vault_token', None)
923+
vault_certificate = getattr(self.parsed_args, 'vault_certificate', None)
924+
925+
if not all([vault_url, vault_token]):
926+
raise ValueError('HashiCorp Vault requires --vault-url and --vault-token')
927+
928+
from grimoirelab_toolkit.credential_manager.hc_manager import HashicorpManager
929+
return HashicorpManager(vault_url, vault_token, vault_certificate)
930+
931+
raise ValueError(f"Unsupported secrets manager: '{manager_type}'")
827932

828933
def _post_init(self):
829934
"""Override to execute after backend is initialized."""

perceval/backends/core/bugzilla.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,8 @@ def setup_cmd_parser(cls):
363363
from_date=True,
364364
basic_auth=True,
365365
archive=True,
366-
ssl_verify=True)
366+
ssl_verify=True,
367+
secrets_manager=True)
367368

368369
# Bugzilla options
369370
group = parser.parser.add_argument_group('Bugzilla arguments')

perceval/backends/core/bugzillarest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,8 @@ def setup_cmd_parser(cls):
507507
basic_auth=True,
508508
token_auth=True,
509509
archive=True,
510-
ssl_verify=True)
510+
ssl_verify=True,
511+
secrets_manager=True)
511512

512513
# BugzillaREST options
513514
group = parser.parser.add_argument_group('Bugzilla REST arguments')

perceval/backends/core/confluence.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,8 @@ def setup_cmd_parser(cls):
351351
basic_auth=True,
352352
token_auth=True,
353353
archive=True,
354-
ssl_verify=True)
354+
ssl_verify=True,
355+
secrets_manager=True)
355356

356357
# Required arguments
357358
parser.parser.add_argument('url',

perceval/backends/core/discourse.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,8 @@ def setup_cmd_parser(cls):
436436
from_date=True,
437437
token_auth=True,
438438
archive=True,
439-
ssl_verify=True)
439+
ssl_verify=True,
440+
secrets_manager=True)
440441

441442
# Required arguments
442443
parser.parser.add_argument('url',

perceval/backends/core/gerrit.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,8 @@ def setup_cmd_parser(cls):
514514
parser = BackendCommandArgumentParser(cls.BACKEND,
515515
from_date=True,
516516
archive=True,
517-
blacklist=True)
517+
blacklist=True,
518+
secrets_manager=True)
518519

519520
# Gerrit options
520521
group = parser.parser.add_argument_group('Gerrit arguments')

perceval/backends/core/git.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,9 @@ class GitCommand(BackendCommand):
405405
def _pre_init(self):
406406
"""Initialize repositories directory path"""
407407

408+
# Fetch credentials from secrets manager if configured
409+
super()._pre_init()
410+
408411
if self.parsed_args.git_log:
409412
git_path = self.parsed_args.git_log
410413
elif self.parsed_args.git_path:
@@ -427,7 +430,8 @@ def setup_cmd_parser(cls):
427430
parser = BackendCommandArgumentParser(cls.BACKEND,
428431
from_date=True,
429432
to_date=True,
430-
ssl_verify=True)
433+
ssl_verify=True,
434+
secrets_manager=True)
431435

432436
# Optional arguments
433437
group = parser.parser.add_argument_group('Git arguments')

perceval/backends/core/github.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1176,7 +1176,8 @@ def setup_cmd_parser(cls):
11761176
to_date=True,
11771177
token_auth=False,
11781178
archive=True,
1179-
ssl_verify=True)
1179+
ssl_verify=True,
1180+
secrets_manager=True)
11801181
# GitHub options
11811182
group = parser.parser.add_argument_group('GitHub arguments')
11821183
group.add_argument('--enterprise-url', dest='base_url',

perceval/backends/core/gitlab.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,8 @@ def setup_cmd_parser(cls):
756756
token_auth=True,
757757
archive=True,
758758
blacklist=True,
759-
ssl_verify=True)
759+
ssl_verify=True,
760+
secrets_manager=True)
760761

761762
# GitLab options
762763
group = parser.parser.add_argument_group('gitlab arguments')

0 commit comments

Comments
 (0)