Skip to content

#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

VariableValeur exempleDescription
FAIR_USE_THROTTLE_KBPS256Débit appliqué en cas de throttling (Kbps)
FAIR_USE_WARNING_PERCENT80Seuil % de déclenchement SMS warning
RADIUS_SECRETsecret_radius_rgzSecret CoA pour throttling (SEC-05)
RADIUS_HOSTrgz-radiusHostname FreeRADIUS
RADIUS_COA_PORT3799Port CoA RFC 5176

Colonnes DB concernées

sql
-- 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éthodeEndpointAuthDescription
GET/api/v1/fair-use/{subscriber_id}Bearer JWTConsommation actuelle vs seuil
POST/api/v1/fair-use/{subscriber_id}/throttleBearer JWT (admin)Throttle manuel
DELETE/api/v1/fair-use/{subscriber_id}/throttleBearer JWT (admin)Lever throttle manuellement
GET/api/v1/fair-use/{subscriber_id}/historyBearer JWTHistorique dépassements

Exemple — GET /api/v1/fair-use/

json
// 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

json
// 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é RedisTypeTTLContenu
rgz:fairuse:{subscriber_id}Hash600sthrottled, consumed_mb, volume_mb, percent, checked_at
rgz:fairuse:warning:{subscriber_id}String3600sFlag anti-doublon SMS warning (1h)
python
# 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

bash
# 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ègleApplication
SEC-01IDOR : 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-04Montants overage_amount_fcfa et amount_fcfa en entiers FCFA uniquement — contrainte CHECK (amount_fcfa >= 0) en DB, calcul // (division entière) en Python
SEC-05RADIUS_SECRET uniquement en variable d'environnement pour l'envoi CoA de throttling
LL#26Ordre : UPDATE DB (throttled=True) → HSET Redis → CoA envoi. Si CoA échoue : log warning, état DB conservé, retry au prochain cycle
LL#8subscriber_id, session_id, reseller_id en UUID v4 dans billing_overages
LL#16CHECK constraint fair_use_policy IN ('throttle', 'overage', 'none') obligatoire en DB

Implémentation TODO

  • [ ] Créer app/services/fair_use.py avec classe FairUseEnforcer et méthodes check_all_active_sessions(), throttle_subscriber(), unlift_throttle()
  • [ ] Ajouter colonnes volume_mb, fair_use_policy, overage_rate_fcfa à table forfaits avec CHECK constraints (LL#16)
  • [ ] Ajouter colonnes throttled, throttled_at, overage_mb à table radius_sessions
  • [ ] Créer table billing_overages avec contrainte CHECK (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

PROJET MOSAÏQUE — 81 outils, 22 conteneurs, 500+ revendeurs WiFi Zone