Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d9308a3
Add credential manager for external secret providers
alberefe Jul 17, 2025
c1e088c
Updated logging bw_manager.py
alberefe Sep 15, 2025
4c5ba5d
Updated logging hc_manager.py
alberefe Sep 15, 2025
df4bc5e
Bitwarden now has to be installed and in path for the program to use it.
alberefe Sep 15, 2025
4652020
hc_manager.py raises exception if authentication fails
alberefe Sep 15, 2025
ec22cf6
It does not return an empty string anymore.
alberefe Sep 15, 2025
3f13072
commented line that prints credentials retrieved
alberefe Sep 15, 2025
36fcbdd
added return to main()
alberefe Sep 15, 2025
19d5525
Unified logger.
alberefe Sep 15, 2025
6e130ce
fixed bw snap path
alberefe Sep 15, 2025
936817a
fixed bw snap path
alberefe Sep 15, 2025
94e59d3
fixed debug lvl argument
alberefe Sep 15, 2025
165ebf0
Renamed datetime to datetime_toolkit so it does not cause import prob…
alberefe Sep 15, 2025
db69efc
Added dependencies
alberefe Sep 16, 2025
754627d
Fixed logger naming
alberefe Sep 25, 2025
4e6b1b5
Fixed exception complexity.
alberefe Sep 25, 2025
557cb89
Updated the docstring format to follow reStructuredText like the rest…
alberefe Sep 28, 2025
98c6eb7
Added W504 to ignored like in Perceval.
alberefe Sep 28, 2025
71e5d1f
Fixed flake8 errors.
alberefe Sep 28, 2025
855200f
Now raises error instead of returning an empty string.
alberefe Sep 28, 2025
b20d422
Created new exceptions.py file to hold the custom exceptions of the p…
alberefe Oct 3, 2025
410376c
New exceptions in aws_manager
alberefe Oct 3, 2025
864c51a
New exceptions in bw_manager
alberefe Oct 3, 2025
14894d7
new exceptions in hc_manager
alberefe Oct 3, 2025
88c48b9
Added imports for new exceptions
alberefe Oct 3, 2025
80982ac
Fixed docstring
alberefe Oct 3, 2025
55df93a
Fixed prompting andnow it's done in credential_manager.py
alberefe Oct 3, 2025
648720b
Fixed imports.
alberefe Oct 3, 2025
632121a
Fixed typo
alberefe Oct 3, 2025
d8cb01f
Changed pytest to unittest to match the rest of the project.
alberefe Oct 5, 2025
78b0a9e
Merge branch 'main' into feature/credential_manager
alberefe Oct 7, 2025
d642003
Merge branch 'main' into feature/credential_manager
alberefe Oct 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[flake8]
exclude = .git, __pycache__, build, dist, docs, docker
ignore = E129, E402, F841, C901
ignore = E129, E402, F841, C901, W504
max-line-length = 130
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ URIs/URLs, among other topics.
## Requirements

* Python >= 3.8
* hvac >= 2.3.0
* boto3 >= 1.35.63

(Hvac and boto3 are used for credential_manager)

You will also need some other libraries for running the tool, you can find the
whole list of dependencies in [pyproject.toml](pyproject.toml) file.
Expand Down Expand Up @@ -59,6 +63,119 @@ To spaw a new shell within the virtual environment use:
$ poetry shell
```

## Credential Manager

This is a module made to retrieve credentials from different secrets management systems like Bitwarden.
It accesses the secrets management service, looks for the desired credential and returns it in String form.


There are two ways of using this module.


### Terminal

To use this, any of these two is valid:

Command-Line Interface:

```
$ python -m credential_manager <manager> <service> <credential>
```

Where:

- manager → credential manager used to store the credentials (Bitwarden, aws, Hashicorp Vault)
- service → the platform to which you want to connect (github, gitlab, bugzilla). It is the name of the secret in the credential storage, it does not have to be the same as the service.
- credential → the field inside the secret that you want to retrieve (username, password, api-token)

Examples:

```
$ python -m credential_manager bitwarden gmail password
$ python -m credential_manager hashicorp github api_token
$ python -m credential_manager aws production db_password
```

In each case, the script will log / access into the corresponding vault, search for the secret with the name of the service that wants to be accessed and then retrieve, from that secret, the value with the name inserted as credential.

That is, in the first case, it will log into Bitwarden, access the secret called "bugzilla", and from it retrieve the value of the field "username".

Each of the secrets management services are accessed in different forms and need different configurations to work, as specified in the [[#Managers]] section.


### Python API

To use the module in your python code


```
# Retrieve a secret from Bitwarden
username = get_secret("bitwarden", "bugzilla", "username")

# Retrieve a secret from AWS Secrets Manager
api_token = get_secret("aws", "github", "api-token")

# Retrieve a secret from HashiCorp Vault
password = get_secret("hashicorp", "gitlab", "password")
```

For more advaced usage, you can directly use the factory to get a specific manager:

```
from credential_manager.secrets_manager_factory import SecretsManagerFactory

# Get a Bitwarden manager instance
bw_manager = SecretsManagerFactory.get_bitwarden_manager()
username = bw_manager.get_secret("bugzilla", "username")

# Get an AWS Secrets Manager instance
aws_manager = SecretsManagerFactory.get_aws_manager()
api_token = aws_manager.get_secret("github", "api-token")

```

### Supported Managers


This section explains the different things to consider when using each of the supported secrets management services, like where to store the credentials to access the secrets manager.

#### AWS

The module uses [boto3](https://boto3.amazonaws.com/v1/documentation/api/latest/index.html), and this looks for your credentials in the .aws folder, in the files "credentials" and "config".

Configuration:

- Credentials are read from the standard AWS credentials file (~/.aws/credentials)
- Region configuration is read from ~/.aws/config
- Ensure your IAM user/role has appropriate permissions to access Secrets Manager

More about this [here](https://docs.aws.amazon.com/sdkref/latest/guide/file-location.html).

#### Hashicorp Vault

The module uses [hvac](https://hvac.readthedocs.io/en/stable/overview.html) to interact with Hashicorp Vault.

The function will look for the following environment variables to get into the vault, and prompt the user for them if not found:

- VAULT_ADDR → Address of the Vault server.
- VAULT_TOKEN → A Vault-issued service token that authenticates the CLI user to Vault.
- VAULT_CACERT → Path to a PEM-encoded CA certificate file on the local disk. Used to verify SSL certificates for the server

If environment variables are not found, the user will be prompted to introduce the data manually.

More info on this can be found [here](https://developer.hashicorp.com/vault/docs/commands).

#### Bitwarden

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

Required environment variables:

- BW_EMAIL → the email used to log into the bitwarden account
- BW_PASSWORD

If environment variables are not found, the user will be prompted to introduce the data manually.

## License

Licensed under GNU General Public License (GPL), version 3 or later.
63 changes: 63 additions & 0 deletions grimoirelab_toolkit/credential_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
#
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Author:
# Alberto Ferrer Sánchez (alberefe@gmail.com)
#

from .credential_manager import get_secret
from .secrets_manager_factory import SecretsManagerFactory
from .exceptions import (
CredentialManagerError,
AuthenticationError,
InvalidCredentialsError,
SessionExpiredError,
SessionNotFoundError,
ConnectionError,
ServiceUnavailableError,
NetworkTimeoutError,
ConfigurationError,
SecretRetrievalError,
SecretNotFoundError,
CredentialNotFoundError,
InvalidSecretFormatError,
BitwardenCLIError,
AWSSecretsManagerError,
HashicorpVaultError,
UnsupportedSecretsManagerError,
)

__all__ = [
"get_secret",
"SecretsManagerFactory",
"CredentialManagerError",
"AuthenticationError",
"InvalidCredentialsError",
"SessionExpiredError",
"SessionNotFoundError",
"ConnectionError",
"ServiceUnavailableError",
"NetworkTimeoutError",
"ConfigurationError",
"SecretRetrievalError",
"SecretNotFoundError",
"CredentialNotFoundError",
"InvalidSecretFormatError",
"BitwardenCLIError",
"AWSSecretsManagerError",
"HashicorpVaultError",
"UnsupportedSecretsManagerError",
]
4 changes: 4 additions & 0 deletions grimoirelab_toolkit/credential_manager/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .credential_manager import main

if __name__ == "__main__":
main()
88 changes: 88 additions & 0 deletions grimoirelab_toolkit/credential_manager/aws_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Author:
# Alberto Ferrer Sánchez (alberefe@gmail.com)
#

import logging
import json
import boto3
from botocore.exceptions import ClientError

from .exceptions import AWSSecretsManagerError, SecretNotFoundError, CredentialNotFoundError, InvalidSecretFormatError

logger = logging.getLogger(__name__)


class AwsManager:
def __init__(self):
"""
Initializes the client that will access to the credentials management service.

This takes the credentials to log into aws from the .aws folder.
This constructor also takes other relevant information from that folder if it exists.

:raises AWSSecretsManagerError: If AWS Secrets Manager operations fail
:raises CredentialConnectionError: If connection issues occur
"""

# Creates a client using the credentials found in the .aws folder (the possible exceptions are propagated)
logger.info("Initializing client and login in")
self.client = boto3.client("secretsmanager")

def _retrieve_and_format_credentials(self, service_name: str) -> dict:
"""
Retrieves credentials using the class client.

:param str service_name: Name of the service to retrieve credentials for (or name of the secret)
:returns: Dictionary containing the credentials retrieved and formatted as a dict
:rtype: dict
:raises AWSSecretsManagerError: If AWS Secrets Manager operations fail
:raises CredentialConnectionError: If connection issues occur
"""
try:
logger.info("Retrieving credentials: %s", service_name)
secret_value_response = self.client.get_secret_value(SecretId=service_name)
formatted_credentials = json.loads(secret_value_response["SecretString"])
return formatted_credentials
except ClientError as e:
logger.error("Error retrieving the secret: %s", str(e))
if e.response["Error"]["Code"] == "ResourceNotFoundException":
raise SecretNotFoundError(f"Secret '{service_name}' not found in AWS Secrets Manager")
raise AWSSecretsManagerError(f"AWS Secrets Manager error: {e}")
except json.JSONDecodeError as e:
logger.error("Error parsing secret JSON: %s", str(e))
raise InvalidSecretFormatError(f"Invalid secret format: {e}")

def get_secret(self, service_name: str, credential_name: str) -> str:
"""
Gets a secret based on the service name and the desired credential.

:param str service_name: Name of the service to retrieve credentials for
:param str credential_name: Name of the credential
:returns: The credential value if found
:rtype: str
:raises CredentialNotFoundError: If the credential name is not found in the secret
:raises SecretNotFoundError: If the secret is not found
:raises AWSSecretsManagerError: If AWS Secrets Manager operations fail
:raises InvalidSecretFormatError: If secret data is malformed
"""
try:
formatted_credentials = self._retrieve_and_format_credentials(service_name)
credential = formatted_credentials[credential_name]
return credential
except KeyError:
# This handles when the credential doesn't exist in the secret
logger.error("The credential '%s' was not found in secret '%s'", credential_name, service_name)
raise CredentialNotFoundError(f"Credential '{credential_name}' not found in secret '{service_name}'")
Loading
Loading