#29 — fair-use-enforcer
PLANIFIÉ
Priorité: 🟠 HAUTE · Type: TYPE B · Conteneur: rgz-api · Code: app/services/fair_use.py
Dépendances: #1 rgz-api · #26 dba-daemon
Description
Le Fair Use Enforcer applique les politiques de consommation data attachées à chaque forfait WiFi ACCESS. Lorsqu'un abonné dépasse le volume de données inclus dans son forfait, le système réagit selon la politique configurée : throttling (réduction de débit à 256 Kbps) ou surfacturation (facturation du volume supplémentaire consommé, exprimé en entiers FCFA — SEC-04).
La surveillance de la consommation s'appuie sur les données déjà collectées par le DBA (#26) : les sessions actives dans radius_sessions contiennent les compteurs bytes_in et bytes_out mis à jour par FreeRADIUS via les paquets Accounting-Request. Le Fair Use Enforcer lit ces compteurs toutes les 5 minutes (en synchronisation avec le cycle DBA) et les compare au seuil volume_mb du forfait associé.
L'action de throttling est appliquée via un CoA RFC 5176 envoyé au CPE, qui abaisse le MIR à 256 Kbps. Cette valeur est suffisante pour maintenir les fonctions basiques (consultation email légère, navigation de base) tout en décourageant l'abus. Le throttling est automatiquement levé à l'expiration du forfait ou si l'abonné achète un volume supplémentaire.
La surfacturation est une alternative au throttling disponible pour certains forfaits. Le montant supplémentaire est calculé en entiers FCFA (jamais en float — SEC-04) et rattaché à la facture mensuelle du revendeur. L'abonné reçoit une notification SMS (#61) en cas de dépassement dans les deux cas.
Architecture Interne
rgz-beat / DBA cycle (every 5min)
│
▼
app/services/fair_use.py :: FairUseEnforcer.check_all_active_sessions()
│
├── SELECT rs.*, f.volume_mb, f.fair_use_policy, f.overage_rate_fcfa
│ FROM radius_sessions rs
│ JOIN forfaits f ON rs.forfait_id = f.id
│ WHERE rs.is_active = True
│
├── Pour chaque session active :
│ consumed_mb = (rs.bytes_in + rs.bytes_out) / 1_048_576
│ percent = consumed_mb / f.volume_mb × 100
│
├── Si percent < 80% → RAS (log debug uniquement)
│
├── Si percent >= 80% et < 100% → SMS WARNING (#61)
│ "Vous avez consommé {percent:.0f}% de votre forfait ACCESS."
│
├── Si percent >= 100% et policy = "throttle" :
│ ├── LL#26 : UPDATE radius_sessions SET throttled=True, throttled_at=NOW()
│ ├── HSET rgz:fairuse:{subscriber_id} throttled true consumed_mb X
│ └── CoA → MIR = 256 Kbps (Bandwidth-Max-Down=256000, Up=256000)
│
└── Si percent >= 100% et policy = "overage" :
├── overage_mb = consumed_mb - f.volume_mb
├── overage_blocks = ceil(overage_mb / 100)
├── amount_fcfa = overage_blocks × f.overage_rate_fcfa (entier FCFA)
├── LL#26 : INSERT INTO billing_overages (subscriber_id, amount_fcfa, ...)
├── UPDATE radius_sessions SET overage_mb = overage_mb
└── SMS NOTIFICATION : "Dépassement forfait : +{overage_mb}MB facturés"Configuration
Variables d'environnement
| Variable | Valeur exemple | Description |
|---|---|---|
FAIR_USE_THROTTLE_KBPS | 256 | Débit appliqué en cas de throttling (Kbps) |
FAIR_USE_WARNING_PERCENT | 80 | Seuil % de déclenchement SMS warning |
RADIUS_SECRET | secret_radius_rgz | Secret CoA pour throttling (SEC-05) |
RADIUS_HOST | rgz-radius | Hostname FreeRADIUS |
RADIUS_COA_PORT | 3799 | Port CoA RFC 5176 |
Colonnes DB concernées
-- Table forfaits (à créer dans Phase 2)
ALTER TABLE forfaits ADD COLUMN volume_mb INTEGER NOT NULL DEFAULT 500;
ALTER TABLE forfaits ADD COLUMN fair_use_policy VARCHAR(20) NOT NULL DEFAULT 'throttle'
CHECK (fair_use_policy IN ('throttle', 'overage', 'none'));
ALTER TABLE forfaits ADD COLUMN overage_rate_fcfa INTEGER DEFAULT 100;
-- 100 FCFA per 100MB supplémentaires (entier FCFA — SEC-04)
-- Table radius_sessions (extension)
ALTER TABLE radius_sessions ADD COLUMN throttled BOOLEAN DEFAULT FALSE;
ALTER TABLE radius_sessions ADD COLUMN throttled_at TIMESTAMPTZ;
ALTER TABLE radius_sessions ADD COLUMN overage_mb INTEGER DEFAULT 0;
-- Table billing_overages (nouvelle)
CREATE TABLE billing_overages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
subscriber_id UUID NOT NULL REFERENCES subscribers(id),
session_id UUID NOT NULL REFERENCES radius_sessions(id),
reseller_id UUID NOT NULL REFERENCES resellers(id),
overage_mb INTEGER NOT NULL,
amount_fcfa INTEGER NOT NULL CHECK (amount_fcfa >= 0), -- SEC-04: entier
billed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
invoice_id UUID REFERENCES invoices(id)
);Endpoints API
| Méthode | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v1/fair-use/{subscriber_id} | Bearer JWT | Consommation actuelle vs seuil |
| POST | /api/v1/fair-use/{subscriber_id}/throttle | Bearer JWT (admin) | Throttle manuel |
| DELETE | /api/v1/fair-use/{subscriber_id}/throttle | Bearer JWT (admin) | Lever throttle manuellement |
| GET | /api/v1/fair-use/{subscriber_id}/history | Bearer JWT | Historique dépassements |
Exemple — GET /api/v1/fair-use/
// Requête (abonné consulte sa propre consommation)
GET /api/v1/fair-use/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJ...
// Réponse 200
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"subscriber_ref": "RGZ-0197979964",
"forfait_name": "ACCESS 24h 500MB",
"volume_mb": 500,
"consumed_mb": 423,
"remaining_mb": 77,
"percent": 84.6,
"throttled": false,
"throttled_at": null,
"overage_mb": 0,
"overage_amount_fcfa": 0,
"warning_sent": true,
"session_id": "abc123-session-uuid",
"checked_at": "2026-02-21T14:30:00Z"
}Exemple — POST /api/v1/fair-use/{subscriber_id}/throttle
// Requête (admin uniquement)
POST /api/v1/fair-use/550e8400-e29b-41d4-a716-446655440000/throttle
Authorization: Bearer eyJ... (role=admin)
Content-Type: application/json
{
"reason": "Fair use enforcement — 100% volume consumed",
"throttle_kbps": 256
}
// Réponse 200
{
"subscriber_id": "550e8400-e29b-41d4-a716-446655440000",
"throttled": true,
"throttle_kbps": 256,
"coa_sent": true,
"throttled_at": "2026-02-21T14:35:00Z"
}Redis Keys
| Clé Redis | Type | TTL | Contenu |
|---|---|---|---|
rgz:fairuse:{subscriber_id} | Hash | 600s | throttled, consumed_mb, volume_mb, percent, checked_at |
rgz:fairuse:warning:{subscriber_id} | String | 3600s | Flag anti-doublon SMS warning (1h) |
# Structure Hash Redis fair-use
{
"throttled": "false",
"consumed_mb": "423",
"volume_mb": "500",
"percent": "84.6",
"checked_at": "2026-02-21T14:30:00Z"
}Commandes Utiles
# Vérifier l'état fair-use d'un abonné dans Redis
docker exec rgz-redis redis-cli HGETALL "rgz:fairuse:550e8400-e29b-41d4-a716-446655440000"
# Lister tous les abonnés actuellement throttlés (DB)
docker exec rgz-db psql -U rgz -d rgzdb -c \
"SELECT rs.subscriber_id, s.subscriber_ref, rs.bytes_in+rs.bytes_out as total_bytes
FROM radius_sessions rs JOIN subscribers s ON rs.subscriber_id=s.id
WHERE rs.is_active=True AND rs.throttled=True;"
# Lever manuellement le throttle via API
curl -X DELETE https://api-rgz.duckdns.org/api/v1/fair-use/{subscriber_id}/throttle \
-H "Authorization: Bearer $ADMIN_TOKEN"
# Voir les dépassements facturés ce mois
docker exec rgz-db psql -U rgz -d rgzdb -c \
"SELECT subscriber_id, SUM(overage_mb) as total_mb, SUM(amount_fcfa) as total_fcfa
FROM billing_overages
WHERE billed_at >= date_trunc('month', NOW())
GROUP BY subscriber_id ORDER BY total_fcfa DESC LIMIT 10;"
# Test manuel envoi CoA throttle (256Kbps)
docker exec rgz-radius radclient -x rgz-radius:3799 coa secret \
"NAS-Identifier=access_kossou,Bandwidth-Max-Down=256000,Bandwidth-Max-Up=256000,\
User-Name=RGZ-0197979964"
# Logs fair-use dans le service API
docker logs rgz-api --tail=50 | grep "fair.use\|throttle\|overage"Sécurité
| Règle | Application |
|---|---|
| SEC-01 | IDOR : GET /fair-use/{subscriber_id} vérifie current_user.subscriber_id == subscriber_id OU role == admin. Jamais d'accès croisé entre abonnés |
| SEC-04 | Montants overage_amount_fcfa et amount_fcfa en entiers FCFA uniquement — contrainte CHECK (amount_fcfa >= 0) en DB, calcul // (division entière) en Python |
| SEC-05 | RADIUS_SECRET uniquement en variable d'environnement pour l'envoi CoA de throttling |
| LL#26 | Ordre : UPDATE DB (throttled=True) → HSET Redis → CoA envoi. Si CoA échoue : log warning, état DB conservé, retry au prochain cycle |
| LL#8 | subscriber_id, session_id, reseller_id en UUID v4 dans billing_overages |
| LL#16 | CHECK constraint fair_use_policy IN ('throttle', 'overage', 'none') obligatoire en DB |
Implémentation TODO
- [ ] Créer
app/services/fair_use.pyavec classeFairUseEnforceret méthodescheck_all_active_sessions(),throttle_subscriber(),unlift_throttle() - [ ] Ajouter colonnes
volume_mb,fair_use_policy,overage_rate_fcfaà tableforfaitsavec CHECK constraints (LL#16) - [ ] Ajouter colonnes
throttled,throttled_at,overage_mbà tableradius_sessions - [ ] Créer table
billing_overagesavec contrainteCHECK (amount_fcfa >= 0)(SEC-04) - [ ] Implémenter endpoint
GET /api/v1/fair-use/{subscriber_id}avec vérification IDOR (SEC-01) - [ ] Implémenter endpoint
POST /api/v1/fair-use/{subscriber_id}/throttle(admin only) - [ ] Implémenter endpoint
DELETE /api/v1/fair-use/{subscriber_id}/throttle(admin only) - [ ] Intégrer appel
FairUseEnforcer.check_all_active_sessions()dans le cycle DBA (#26) ou dans une tâche Celery Beat dédiée (every 5min) - [ ] Intégrer envoi SMS warning (#61) à 80% de consommation — avec flag anti-doublon Redis (TTL 1h)
- [ ] Valider calcul surfacturation entiers FCFA :
overage_blocks = ceil(overage_mb / 100),amount_fcfa = overage_blocks * rate(division entière Python//) - [ ] Tests unitaires
check_all_active_sessions()avec sessions mockées (throttle + overage) - [ ] Tests SEC-01 : vérifier 403 si subscriber_id ne correspond pas à l'abonné connecté
Dernière mise à jour: 2026-02-21