Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
Comment on lines +12 to +13
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the pyproject file using Poetry to include these requirements.


(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.
25 changes: 25 additions & 0 deletions grimoirelab_toolkit/credential_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
#
#
#
# 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 ([email protected])
#

from .credential_manager import get_secret
from .secrets_manager_factory import SecretsManagerFactory

__all__ = ['get_secret', 'SecretsManagerFactory']
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()
110 changes: 110 additions & 0 deletions grimoirelab_toolkit/credential_manager/aws_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
#
#
#
# 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 ([email protected])
#

import logging
import json
import boto3
from botocore.exceptions import EndpointConnectionError, SSLError, ClientError

_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:
Exception: If there's a connection error.
"""

# Creates a client using the credentials found in the .aws folder
try:
_logger.info("Initializing client and login in")
self.client = boto3.client("secretsmanager")

except (EndpointConnectionError, SSLError, ClientError, Exception) as e:
_logger.error("Problem starting the client: %s", e)
raise e

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

Args:
service_name (str): Name of the service to retrieve credentials for.(or name of the secret)

Returns:
formatted_credentials (dict): Dictionary containing the credentials retrieved and formatted as a dict

Raises:
Exception: If there's a connection error.
"""
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, json.JSONDecodeError) as e:
_logger.error("Error retrieving the secret: %s", str(e))
raise e

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

Args:
service_name (str): Name of the service to retrieve credentials for
credential_name (str): Name of the credential

Returns:
str: The credential value if found, empty string if not found

Raises:
Exception: If there's a connection error.
"""
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 secret %s:%s, was not found.", service_name, credential_name)
_logger.error(
"Please check the secret name and the credential name. For now here you have an empty string.")
return ""
except ClientError as e:
# This handles AWS-specific errors like ResourceNotFoundException
if e.response['Error']['Code'] == 'ResourceNotFoundException':
_logger.error("The secret %s:%s, was not found.", service_name, credential_name)
_logger.error(e)
_logger.error(
"Please check the secret name and the credential name. For now here you have an empty string.")
return ""
_logger.error("There was a problem getting the secret")
raise e
except Exception as e:
_logger.error("There was a problem getting the secret")
raise e
Loading