secrets-config 1.0.0
Python module for managing secrets from Vaultwarden and environment variables with multi-environment support
Requires Python: >=3.8
Keywords: bitwarden, config, environment, secrets, vaultwarden

# secrets_config

**Module Python de Gestion Centralisée des Secrets et Configuration**

Version 1.0.0 | Python 3.8+

---

## Table des matières

- [Vue d'ensemble](#vue-densemble)
- [Architecture du projet](#architecture-du-projet)
- [Installation](#installation)
- [Les Modèles de Données](#les-modèles-de-données)
  - [Classe Secret](#classe-secret)
  - [Classe Config](#classe-config)
- [SecretManager - Gestion des Secrets](#secretmanager---gestion-des-secrets)
  - [Authentification Vaultwarden](#authentification-vaultwarden)
  - [Bitwarden CLI](#bitwarden-cli)
  - [Modes de Chargement](#modes-de-chargement)
- [ConfigManager - Gestion de la Configuration](#configmanager---gestion-de-la-configuration)
- [Exemples d'utilisation](#exemples-dutilisation)
- [Variables d'environnement](#variables-denvironnement)

---

## Vue d'ensemble

`secrets_config` est un module Python qui permet de **séparer clairement** :

1. **Les secrets sensibles** (mots de passe, tokens, clés API) → stockés dans **Vaultwarden/Bitwarden**
2. **La configuration non-sensible** (ports, URLs, paramètres) → stockés dans **fichiers .env**

### Caractéristiques

✅ **Zéro dépendance externe** - Utilise uniquement la bibliothèque standard Python
✅ **Support multi-environnement** - dev, qa, prod avec auto-détection
✅ **4 modes de chargement de secrets** - all, list, collection, organization
✅ **Distinction Organisation vs Personnel** - Gestion des secrets partagés et personnels
✅ **Conversion de types automatique** - int, float, bool pour la configuration
✅ **Champs personnalisés** - Support complet des custom fields de Vaultwarden
✅ **Gestion automatique des sessions** - Login/Logout automatique avec Vaultwarden

---

## Architecture du projet

```
secrets_config/
├── src/
│   └── secrets_config/
│       ├── __init__.py          # Point d'entrée du module
│       ├── models.py            # Modèles de données (Secret, Config)
│       ├── secret_manager.py    # Gestion des secrets Vaultwarden
│       └── config_manager.py    # Gestion des variables d'environnement
├── .env.example                 # Template de configuration
├── .gitignore                   # Fichiers à ignorer
├── pyproject.toml               # Configuration du package
├── uv.lock                      # Lock file pour uv
└── README.md                    # Cette documentation
```

### Description des fichiers

#### `__init__.py`
Point d'entrée du module qui expose les classes principales :
```python
from .models import Secret, Config
from .secret_manager import SecretManager
from .config_manager import ConfigManager

__version__ = "1.0.0"
__all__ = ["Secret", "Config", "SecretManager", "ConfigManager"]
```

#### `models.py`
Définit les deux classes de données principales :
- **Secret** : Représente un secret Vaultwarden
- **Config** : Représente la configuration d'environnement

#### `secret_manager.py`
Gère l'extraction des secrets depuis Vaultwarden via Bitwarden CLI :
- Authentification automatique
- Support de 4 modes de chargement
- Gestion des organisations et collections
- Extraction des champs personnalisés

#### `config_manager.py`
Gère le chargement des variables d'environnement :
- Auto-détection des fichiers .env selon l'environnement
- Parsing des fichiers .env
- Validation des variables requises

---

## Installation

### Via uv (recommandé)
```bash
uv pip install /path/to/secrets_config
```

### Via pip
```bash
pip install /path/to/secrets_config
```

### Prérequis
- **Python 3.8+**
- **Bitwarden CLI** installé et configuré :
  ```bash
  # Installation
  npm install -g @bitwarden/cli

  # Configuration du serveur Vaultwarden
  bw config server https://your-vaultwarden-server.com
  ```

---

## Les Modèles de Données

### Classe Secret

Représente un secret stocké dans Vaultwarden avec tous ses attributs.

#### Attributs

| Attribut | Type | Description |
|----------|------|-------------|
| `id` | `str` | Identifiant unique du secret |
| `name` | `str` | Nom du secret |
| `type` | `int` | Type d'élément Bitwarden (1=login, 2=note, 3=card, 4=identity) |
| `username` | `Optional[str]` | Nom d'utilisateur |
| `password` | `Optional[str]` | Mot de passe |
| `notes` | `Optional[str]` | Notes associées |
| `fields` | `Dict[str, str]` | Champs personnalisés (custom fields) |
| `uris` | `List[str]` | Liste des URLs associées |
| `organization` | `Optional[str]` | Nom de l'organisation (None si personnel) |
| `collections` | `List[str]` | Liste des collections auxquelles appartient le secret |
| `creation_date` | `Optional[str]` | Date de création (ISO 8601) |
| `revision_date` | `Optional[str]` | Date de dernière modification (ISO 8601) |

#### Méthodes

**`get_field(field_name: str, default=None)`**
Récupère la valeur d'un champ personnalisé avec une valeur par défaut optionnelle.

```python
secret = secrets["database"]
db_host = secret.get_field("host", "localhost")
db_port = secret.get_field("port", "5432")
```

#### Exemple d'utilisation

```python
from secrets_config import SecretManager

# Charger les secrets
secrets = SecretManager.load_from_env()

# Accéder à un secret
db_secret = secrets["database"]

print(f"ID: {db_secret.id}")
print(f"Name: {db_secret.name}")
print(f"Type: {db_secret.type}")
print(f"Username: {db_secret.username}")
print(f"Password: {db_secret.password}")
print(f"Organization: {db_secret.organization}")
print(f"Collections: {db_secret.collections}")

# Accéder aux champs personnalisés
host = db_secret.get_field("host")
port = db_secret.get_field("port")
database = db_secret.get_field("database")

# Accéder aux URIs
if db_secret.uris:
    print(f"URL: {db_secret.uris[0]}")

# Vérifier les dates
print(f"Créé le: {db_secret.creation_date}")
print(f"Modifié le: {db_secret.revision_date}")
```

---

### Classe Config

Représente la configuration d'environnement avec conversion de types automatique.

#### Attributs

| Attribut | Type | Description |
|----------|------|-------------|
| `env_vars` | `Dict[str, str]` | Dictionnaire des variables d'environnement |

#### Méthodes

**`get(key: str, default: Optional[str] = None) -> Optional[str]`**
Récupère une valeur string.

```python
config = ConfigManager.load()
api_url = config.get("API_URL", "http://localhost:8000")
```

**`get_int(key: str, default: Optional[int] = None) -> Optional[int]`**
Récupère une valeur entière avec conversion automatique.

```python
port = config.get_int("PORT", 8000)
max_connections = config.get_int("MAX_CONNECTIONS", 100)
```

**`get_float(key: str, default: Optional[float] = None) -> Optional[float]`**
Récupère une valeur décimale avec conversion automatique.

```python
timeout = config.get_float("TIMEOUT", 30.5)
rate_limit = config.get_float("RATE_LIMIT", 1.5)
```

**`get_bool(key: str, default: bool = False) -> bool`**
Récupère une valeur booléenne. Considère comme `True` : `true`, `1`, `yes`, `on` (insensible à la casse).

```python
debug = config.get_bool("DEBUG", False)
use_ssl = config.get_bool("USE_SSL", True)
```

**`__getitem__(key: str) -> str`**
Accès style dictionnaire (lève KeyError si la clé n'existe pas).

```python
api_key = config["API_KEY"]
```

**`__contains__(key: str) -> bool`**
Vérifie si une clé existe.

```python
if "DATABASE_URL" in config:
    db_url = config["DATABASE_URL"]
```

#### Exemple d'utilisation

```python
from secrets_config import ConfigManager

# Charger la configuration
config = ConfigManager.load()

# Récupérer différents types
api_url = config.get("API_URL", "http://localhost:8000")
port = config.get_int("PORT", 8000)
timeout = config.get_float("TIMEOUT", 30.0)
debug = config.get_bool("DEBUG", False)

# Vérifier l'existence d'une clé
if "REDIS_URL" in config:
    redis_url = config["REDIS_URL"]
```

---

## SecretManager - Gestion des Secrets

La classe `SecretManager` gère l'extraction des secrets depuis Vaultwarden via Bitwarden CLI.

### Authentification Vaultwarden

#### Configuration requise

Le `SecretManager` nécessite deux variables d'environnement :
- `VAULTWARDEN_EMAIL` : Email du compte Vaultwarden
- `VAULTWARDEN_PASSWORD` : Mot de passe du compte

Ces variables peuvent être définies dans le fichier `.env` de votre microservice.

#### Processus d'authentification

```python
def _login(self) -> str:
    # 1. Déconnexion préalable pour garantir un état propre
    subprocess.run([self.bw_cli, "logout"], capture_output=True)

    # 2. Connexion et récupération de la clé de session
    result = subprocess.run(
        [self.bw_cli, "login", self.email, self.password, "--raw"],
        capture_output=True,
        text=True
    )

    # 3. Stockage de la clé de session pour les requêtes suivantes
    self.session_key = result.stdout.strip()
    return self.session_key
```

**Important** :
- La 2FA (authentification à deux facteurs) doit être **désactivée** pour les comptes utilisés par les scripts automatisés
- La session est automatiquement fermée après le chargement des secrets
- Les identifiants ne sont jamais stockés en dur dans le code

---

### Bitwarden CLI

Le module utilise **Bitwarden CLI** pour communiquer avec Vaultwarden.

#### Installation de Bitwarden CLI

```bash
# Via npm
npm install -g @bitwarden/cli

# Vérifier l'installation
bw --version
```

#### Configuration du serveur

```bash
# Pointer vers votre instance Vaultwarden
bw config server https://vaultwarden.dats.ddnsfree.com

# Vérifier la configuration
bw config server
```

#### Commandes utilisées par le module

| Commande | Usage |
|----------|-------|
| `bw login <email> <password> --raw` | Authentification et récupération de la session |
| `bw logout` | Déconnexion et fermeture de session |
| `bw list organizations --session <key>` | Liste des organisations |
| `bw list org-collections --organizationid <id> --session <key>` | Liste des collections d'une organisation |
| `bw list items --session <key>` | Liste de tous les secrets |

#### Chemin du CLI

Par défaut, le module cherche `bw` dans `~/.local/bin/bw`. Vous pouvez spécifier un chemin personnalisé :

```python
manager = SecretManager(
    email="user@example.com",
    password="password",
    bw_cli_path="/usr/local/bin/bw"
)
```

---

### Modes de Chargement

Le `SecretManager` propose **4 modes de chargement** des secrets, contrôlés par la variable d'environnement `SECRETS_MODE`.

#### Mode 1 : `all` (par défaut)

Charge **tous les secrets** du compte Vaultwarden (personnels + organisations).

**Configuration** :
```bash
SECRETS_MODE=all
```

**Utilisation** :
```python
from secrets_config import SecretManager

secrets = SecretManager.load_from_env()
# secrets = {"secret1": Secret(...), "secret2": Secret(...), ...}
```

**Cas d'usage** :
- Environnement de développement local
- Petit nombre de secrets
- Besoin d'accéder à tous les secrets

---

#### Mode 2 : `list`

Charge **une liste spécifique** de secrets par leurs noms.

**Configuration** :
```bash
SECRETS_MODE=list
SECRETS_LIST=database,redis,smtp_server
```

**Utilisation** :
```python
secrets = SecretManager.load_from_env()
# secrets = {"database": Secret(...), "redis": Secret(...), "smtp_server": Secret(...)}

db = secrets["database"]
redis = secrets["redis"]
smtp = secrets["smtp_server"]
```

**Cas d'usage** :
- Microservice qui n'utilise que quelques secrets spécifiques
- Optimisation du temps de chargement
- Contrôle précis des secrets chargés

---

#### Mode 3 : `collection`

Charge **tous les secrets d'une collection** spécifique.

**Configuration** :
```bash
SECRETS_MODE=collection
SECRETS_COLLECTION=Production
```

**Utilisation** :
```python
secrets = SecretManager.load_from_env()
# Tous les secrets de la collection "Production"
```

**Cas d'usage** :
- Organisation par environnement (dev/qa/prod dans différentes collections)
- Organisation par projet
- Partage de secrets entre microservices d'un même projet

**Exemple d'organisation** :
```
Vaultwarden Collections:
├── UserAPI-Dev
│   ├── db_users_dev
│   ├── redis_dev
│   └── api_key_dev
├── UserAPI-Prod
│   ├── db_users_prod
│   ├── redis_prod
│   └── api_key_prod
└── PaymentAPI-Prod
    ├── db_payments_prod
    ├── stripe_key
    └── paypal_key
```

---

#### Mode 4 : `organization`

Charge **tous les secrets d'une organisation** spécifique.

**Configuration** :
```bash
SECRETS_MODE=organization
SECRETS_ORGANIZATION=DATS
```

**Utilisation** :
```python
secrets = SecretManager.load_from_env()
# Tous les secrets de l'organisation "DATS"
```

**Cas d'usage** :
- Environnement multi-tenant
- Secrets partagés au niveau de l'organisation
- Séparation entre secrets personnels et d'entreprise

**Différence Organisation vs Personnel** :
- **Organisation** : `secret.organization = "DATS"` → Secrets partagés avec l'équipe
- **Personnel** : `secret.organization = None` → Secrets privés à l'utilisateur

---

### Méthode `load_from_env()`

**Signature** :
```python
@classmethod
def load_from_env(cls) -> Dict[str, Secret]
```

Méthode principale qui charge les secrets selon la configuration d'environnement.

**Fonctionnement** :
1. Lit la variable `SECRETS_MODE` (défaut: `all`)
2. Lit les variables complémentaires selon le mode (`SECRETS_LIST`, `SECRETS_COLLECTION`, `SECRETS_ORGANIZATION`)
3. Crée une instance de `SecretManager`
4. Appelle la méthode appropriée (`load_all()`, `load()`, `load_from_collection()`, `load_from_organization()`)
5. Retourne un dictionnaire `{nom_secret: Secret}`

**Exemple** :
```python
from secrets_config import SecretManager

# Configuration dans .env :
# SECRETS_MODE=collection
# SECRETS_COLLECTION=Production

secrets = SecretManager.load_from_env()

for name, secret in secrets.items():
    print(f"{name}: {secret.username}@{secret.get_field('host')}")
```

---

### Autres Méthodes

**`load_all() -> Dict[str, Secret]`**
Charge tous les secrets.

**`load(secret_names: List[str]) -> Dict[str, Secret]`**
Charge une liste spécifique de secrets.

**`load_from_collection(collection_name: str) -> Dict[str, Secret]`**
Charge tous les secrets d'une collection.

**`load_from_organization(org_name: str) -> Dict[str, Secret]`**
Charge tous les secrets d'une organisation.

**`_logout()`**
Déconnexion automatique après chargement (appelée automatiquement).

---

## ConfigManager - Gestion de la Configuration

La classe `ConfigManager` gère le chargement des variables d'environnement avec support multi-environnement.

### Auto-détection des fichiers .env

Le `ConfigManager` détecte **automatiquement** quel fichier `.env` charger selon la variable d'environnement `ENV`.

#### Ordre de priorité

```python
def _detect_env_file() -> Optional[str]:
    env = os.getenv("ENV", "").lower()

    # 1. Fichier spécifique à l'environnement
    if env:
        env_file = f".env.{env}"
        if os.path.exists(env_file):
            return env_file

    # 2. Fichier .env par défaut
    if os.path.exists(".env"):
        return ".env"

    # 3. Aucun fichier
    return None
```

#### Exemples

| Variable ENV | Fichier chargé |
|--------------|----------------|
| `ENV=dev` | `.env.dev` |
| `ENV=qa` | `.env.qa` |
| `ENV=prod` | `.env.prod` |
| Non défini | `.env` |

**Cas particulier** : Si `ENV=prod` mais `.env.prod` n'existe pas, le système cherchera `.env` en fallback.

---

### Méthodes

**`load(prefix: Optional[str] = None, env_file: Optional[str] = None) -> Config`**

Charge les variables d'environnement.

**Paramètres** :
- `prefix` : Filtre les variables par préfixe (ex: `APP_`)
- `env_file` : Force le chargement d'un fichier spécifique (ignore l'auto-détection)

**Exemples** :
```python
# Charger toutes les variables
config = ConfigManager.load()

# Charger uniquement les variables APP_*
config = ConfigManager.load(prefix="APP_")
# Si APP_PORT=8000 → config.get("PORT") == "8000"

# Forcer un fichier spécifique
config = ConfigManager.load(env_file=".env.test")
```

---

**`get_required(key: str, error_msg: Optional[str] = None) -> str`**

Récupère une variable **obligatoire** (lève une exception si absente).

```python
# Avec message d'erreur par défaut
api_key = ConfigManager.get_required("API_KEY")
# ValueError: Required environment variable 'API_KEY' is not set

# Avec message personnalisé
db_url = ConfigManager.get_required(
    "DATABASE_URL",
    error_msg="La base de données doit être configurée !"
)
```

---

**`validate_required(keys: list) -> dict`**

Valide plusieurs variables obligatoires en une seule fois.

```python
# Vérifier plusieurs variables
required_vars = ConfigManager.validate_required([
    "DATABASE_URL",
    "REDIS_URL",
    "SECRET_KEY"
])

# Si toutes présentes :
# required_vars = {
#     "DATABASE_URL": "postgresql://...",
#     "REDIS_URL": "redis://...",
#     "SECRET_KEY": "xyz123"
# }

# Si une manque :
# ValueError: Missing required environment variables: SECRET_KEY
```

---

### Parsing des fichiers .env

Le `ConfigManager` parse les fichiers `.env` avec les règles suivantes :

**Format supporté** :
```bash
# Commentaires (ignorés)
KEY=value
KEY="value with spaces"
KEY='single quotes'
EMPTY_VALUE=
```

**Traitement** :
- Lignes vides → ignorées
- Lignes commençant par `#` → ignorées
- Guillemets doubles/simples → supprimés automatiquement
- Espaces autour de `=` → supprimés

**Exemple** :
```bash
# .env.dev
DB_HOST=localhost
DB_PORT=5432
DB_NAME="my_database"
DB_USER='admin'
# Commentaire
API_URL=http://localhost:8000
```

```python
config = ConfigManager.load(env_file=".env.dev")
print(config.get("DB_HOST"))   # "localhost"
print(config.get("DB_NAME"))   # "my_database" (sans guillemets)
```

---

## Exemples d'utilisation

### Exemple 1 : Chargement simple

```python
from secrets_config import SecretManager, ConfigManager

# Charger la configuration
config = ConfigManager.load()
port = config.get_int("PORT", 8000)
debug = config.get_bool("DEBUG", False)

# Charger tous les secrets
secrets = SecretManager.load_from_env()

# Utiliser un secret
db_secret = secrets["database"]
connection_string = f"postgresql://{db_secret.username}:{db_secret.password}@{db_secret.get_field('host')}/{db_secret.get_field('database')}"
```

---

### Exemple 2 : Mode collection

**Fichier `.env.prod`** :
```bash
ENV=prod
VAULTWARDEN_EMAIL=admin@company.com
VAULTWARDEN_PASSWORD=SecurePassword123
SECRETS_MODE=collection
SECRETS_COLLECTION=UserAPI-Prod
```

**Code** :
```python
from secrets_config import SecretManager

# Charge uniquement les secrets de la collection "UserAPI-Prod"
secrets = SecretManager.load_from_env()

db = secrets["database"]
redis = secrets["redis_cache"]
smtp = secrets["email_server"]

print(f"DB: {db.username}@{db.get_field('host')}")
print(f"Redis: {redis.get_field('url')}")
print(f"SMTP: {smtp.get_field('server')}:{smtp.get_field('port')}")
```

---

### Exemple 3 : Mode list avec préfixe de config

**Fichier `.env.dev`** :
```bash
ENV=dev
APP_PORT=8000
APP_DEBUG=true
APP_LOG_LEVEL=DEBUG

VAULTWARDEN_EMAIL=dev@company.com
VAULTWARDEN_PASSWORD=DevPassword
SECRETS_MODE=list
SECRETS_LIST=db_dev,redis_dev
```

**Code** :
```python
from secrets_config import SecretManager, ConfigManager

# Charger config avec préfixe APP_
config = ConfigManager.load(prefix="APP_")
port = config.get_int("PORT")        # APP_PORT → PORT
debug = config.get_bool("DEBUG")     # APP_DEBUG → DEBUG
log_level = config.get("LOG_LEVEL")  # APP_LOG_LEVEL → LOG_LEVEL

# Charger uniquement db_dev et redis_dev
secrets = SecretManager.load_from_env()
db = secrets["db_dev"]
redis = secrets["redis_dev"]
```

---

### Exemple 4 : Validation des variables requises

```python
from secrets_config import ConfigManager

# Valider les variables obligatoires au démarrage
required = ConfigManager.validate_required([
    "DATABASE_URL",
    "REDIS_URL",
    "SECRET_KEY",
    "API_KEY"
])

# Si toutes présentes, continuer
config = ConfigManager.load()
```

---

## Variables d'environnement

### Configuration Vaultwarden (Obligatoires)

| Variable | Description | Exemple |
|----------|-------------|---------|
| `VAULTWARDEN_EMAIL` | Email du compte Vaultwarden | `admin@company.com` |
| `VAULTWARDEN_PASSWORD` | Mot de passe du compte | `SecurePassword123` |

### Configuration des Secrets (Optionnelles)

| Variable | Description | Valeurs possibles | Défaut |
|----------|-------------|-------------------|--------|
| `SECRETS_MODE` | Mode de chargement | `all`, `list`, `collection`, `organization` | `all` |
| `SECRETS_LIST` | Liste de secrets (mode `list`) | Noms séparés par virgules | - |
| `SECRETS_COLLECTION` | Nom de collection (mode `collection`) | Nom de la collection | - |
| `SECRETS_ORGANIZATION` | Nom d'organisation (mode `organization`) | Nom de l'organisation | - |

### Configuration Environnement (Optionnelle)

| Variable | Description | Valeurs possibles | Défaut |
|----------|-------------|-------------------|--------|
| `ENV` | Environnement d'exécution | `dev`, `qa`, `prod` | - |

---

## Bonnes Pratiques

### Sécurité

1. **Ne jamais commiter les fichiers `.env`** contenant des identifiants réels
2. **Utiliser `.env.example`** comme template pour les autres développeurs
3. **Désactiver la 2FA** pour les comptes de service automatisés
4. **Utiliser des comptes de service dédiés** pour les microservices
5. **Limiter les permissions** aux collections nécessaires uniquement

### Organisation

1. **Une collection par environnement** : `MyAPI-Dev`, `MyAPI-QA`, `MyAPI-Prod`
2. **Préfixer les secrets** par environnement : `db_prod`, `db_dev`
3. **Documenter les custom fields** dans les notes du secret
4. **Grouper par microservice** dans différentes collections

### Performance

1. **Utiliser le mode `list`** ou `collection` plutôt que `all` en production
2. **Charger les secrets au démarrage** de l'application, pas à chaque requête
3. **Mettre en cache** les secrets chargés dans votre application

---

## Licence

Propriétaire - DATS Team

---

## Support

Pour toute question ou problème, contactez l'équipe DATS.