Skip to content
up2devPublic

About

Framework CRUD/RBAC par convention pour FastAPI : Router/Controller/Repository, RBAC deny-by-default, MFA (TOTP, email, WebAuthn), stockage de fichiers, planificateur et générateur de CRUD.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Forge

Framework CRUD/RBAC par convention pour FastAPI, inspiré de Rivet : Router / Controller / Repository plutôt que des endpoints libres.

Inclus : authentification, RBAC deny-by-default, MFA (TOTP, email, WebAuthn) avec codes de secours, reset de mot de passe, stockage de fichiers, planificateur de tâches, générateur de CRUD, audit, i18n et mailing. Testé sur SQLite et PostgreSQL.

Venant de Rivet : docs/RIVET.md liste les différences.

Sommaire

Démarrage

Avec Docker

make up
make seed   # migrations, permissions, utilisateur admin/changeme (rôle ADMIN)
Service URL
API http://localhost:8000
Swagger (bouton "Authorize" pour coller un token Bearer) http://localhost:8000/docs
Redoc http://localhost:8000/redoc
Schéma OpenAPI http://localhost:8000/openapi.json
Adminer (dev uniquement) : système PostgreSQL, serveur db, forge/forge/forge http://localhost:8081
Mailpit : capture tous les emails, rien ne part vers un vrai serveur http://localhost:8025

PostgreSQL est exposé sur localhost:5432 (forge/forge/forge).

Le service api monte le code en volume avec --reload : les modifications sont prises en compte sans rebuild.

Sans Docker

pip install -r requirements-dev.txt
uvicorn example_app.main:app --reload
python scripts/seed.py

SQLite (./forge.db) par défaut ; DATABASE_URL pour une autre base.

Bruno

bruno/ est une collection Bruno prête à l'emploi : ouvrir le dossier, choisir l'environnement local, lancer Auth/Login (le token est stocké et réutilisé par les autres requêtes). Les dossiers couvrent auth, MFA, CRUD, fichiers, ainsi que des cas d'erreur volontaires (filtre non autorisé, suppression en masse non confirmée, permission manquante).

Format des réponses

Toute réponse JSON est enveloppée :

{"data": {"id": 1, "title": "Dune"}, "meta": {"status": "success", "code": 200, "duration_ms": 3.1}}
{"errors": {"detail": "Not found"}, "meta": {"status": "error", "code": 404, "duration_ms": 0.4}}

Seul le contenu binaire des fichiers n'est pas enveloppé. Détail, pagination comprise : docs/RESPONSES.md.

Auth et RBAC

POST /auth/login {login, password} renvoie un token Bearer. GET /auth/me et POST /auth/logout l'utilisent. /auth/login est limité à 5 tentatives par minute et par IP (en mémoire : à remplacer par un backend partagé en multi-instance, voir forge/security/rate_limit.py).

Chaque route déclare sa politique d'accès : permission="XXX", public=True ou authenticated_only=True. Une route sans décision empêche l'application de démarrer (RouteAuthorizationError) : aucune route n'est ouverte par oubli.

Permissions, rôles, utilisateurs

Une permission déclarée dans le code doit exister en base pour être attribuée.

make permissions                                   # crée les permissions déclarées par les routes
make grant role=ADMIN permission=BOOKS_DELETE      # attribue à un rôle
make create-role uid=EDITOR name="Éditeur" permissions=BOOKS_DELETE
make create-user login=carol email=carol@exemple.fr role=EDITOR

Sans password=, un mot de passe est généré et affiché une seule fois. scripts/create_user.py --file users.csv crée plusieurs utilisateurs (colonnes login,email,password,role). L'attribution aux rôles reste toujours manuelle ; make seed donne au rôle ADMIN les permissions existantes à ce moment-là.

Identifiants de l'admin de seed : FORGE_ADMIN_LOGIN, FORGE_ADMIN_EMAIL, FORGE_ADMIN_PASSWORD (obligatoires en production).

MFA

Méthodes : totp, email, webauthn (FORGE_MFA_METHODS). Une méthode de chaque type par utilisateur.

Gestion (self-service)

Route Rôle
POST /auth/mfa/setup {method, secret?} Démarre l'inscription. totp : {secret, qr_uri} (secret optionnel pour importer un token matériel). email : envoie un code. webauthn : {webauthn_options} pour navigator.credentials.create().
POST /auth/mfa/confirm {method, code} ou {method: "webauthn", webauthn_response} Confirme la méthode. À la première méthode confirmée, renvoie aussi les codes de secours.
GET /auth/mfa/methods {available, enabled, recovery_codes_remaining}
POST /auth/mfa/request-code {pending_token?} Renvoie un code email (connecté, ou en cours de login).
DELETE /auth/mfa/{method} Désactive une méthode.

Connexion

Une fois une méthode confirmée, POST /auth/login renvoie {pending_token, intent: "verify", methods, recovery} au lieu d'un token (le code email part automatiquement). Puis :

  • TOTP / email : POST /auth/mfa/verify {pending_token, method, code}.
  • WebAuthn : POST /auth/mfa/webauthn/login-options {pending_token} → options pour navigator.credentials.get(), puis /verify avec webauthn_response.
  • Code de secours : /verify avec method: "recovery".

/verify renvoie le token d'accès. Rate-limité comme /auth/login.

Inscription forcée

Avec FORGE_MFA_FORCE_ENROLLMENT=1, un compte sans méthode confirmée reçoit {pending_token, intent: "enroll", methods} au login. Le client appelle setup puis confirm avec ce pending_token dans le body (sans Bearer) ; la confirmation renvoie directement le token d'accès.

Codes de secours

Codes à usage unique pour se connecter sans la méthode habituelle.

  • Remis une seule fois, dans la réponse de confirm à la première méthode confirmée (recovery_codes). Seul leur hash est stocké.
  • Format XXXXX-XXXXX, sans caractères ambigus ; casse, espaces et tirets tolérés à la saisie.
  • Un code utilisé est marqué et ne fonctionne plus. Un code faux compte dans FORGE_MFA_MAX_ATTEMPTS.
  • Chaque utilisation envoie un email (codes restants, alerte sous FORGE_MFA_RECOVERY_LOW_THRESHOLD) et est tracée dans activity_log.
  • Régénération : POST /auth/mfa/recovery-codes {password}. Le lot précédent est invalidé.
  • Ce n'est pas une méthode : ils ne satisfont pas l'inscription forcée et sont supprimés quand plus aucune méthode n'est confirmée.

Réinitialisation par un administrateur

Pour un utilisateur ayant perdu son moyen d'authentification.

  • GET /auth/mfa/users/{user_id} → {user_id, enabled, pending}
  • DELETE /auth/mfa/users/{user_id}[?method=totp] → {user_id, removed}

Supprime les méthodes, codes email, pending tokens et codes de secours. Permission FORGE_MFA_RESET_PERMISSION (défaut MFA_RESET). Tracé dans activity_log (mfa_reset). Un administrateur ne peut pas cibler son propre compte.

Sécurité

  • Secret TOTP chiffré au repos avec FORGE_APP_KEY. Changer cette clé rend les secrets existants indéchiffrables.
  • Anti-rejeu TOTP : un code déjà accepté est refusé, même encore dans sa fenêtre de validité.
  • WebAuthn : le compteur de signatures doit progresser à chaque connexion (détection de clé clonée).
  • FORGE_MFA_MAX_ATTEMPTS (défaut 5) : au-delà, le pending_token est détruit, indépendamment du rate-limit par IP.
  • Sous inscription forcée, impossible de désactiver sa dernière méthode, sauf permission FORGE_MFA_FORCE_DISABLE_PERMISSION.
  • FORGE_MFA_BYPASS_PERMISSION dispense du MFA les rôles qui la détiennent.

WebAuthn

FORGE_WEBAUTHN_RP_ID est le domaine exact vu par le navigateur (sans port ni schéma, localhost en dev) ; FORGE_WEBAUTHN_ORIGIN l'URL complète. Un décalage fait échouer toute vérification.

Page de test avec une vraie clé : http://localhost:8000/webauthn-test (dev uniquement, à retirer en production). Les tests utilisent un authentificateur logiciel (tests/webauthn_helpers.py).

Reset de mot de passe

  1. POST /auth/pwd/forgot {email} : réponse identique que le compte existe ou non. Envoie un lien vers le front (FORGE_FRONTEND_URL + FORGE_FRONTEND_PWD_RESET_PATH).
  2. POST /auth/pwd/reset {token, password} (8 caractères minimum).
  • Un seul lien actif par compte, à usage unique, expiration FORGE_PWD_RESET_TOKEN_TTL_MINUTES (défaut 60).
  • Token haché en base.
  • Sessions ouvertes révoquées au reset (FORGE_PWD_RESET_REVOKES_SESSIONS, défaut activé).
  • Le reset ne connecte pas et ne contourne pas le MFA.
  • Liens expirés purgés chaque nuit par le planificateur (make prune à la demande).

CRUD

Déclaration

books_router = ControllerRouter(BookController, prefix="/books", tags=["books"])
books_router.resource(permissions={"remove": "BOOKS_DELETE", "mass_remove": "BOOKS_DELETE"})

resource() enregistre list, show, add, edit, remove et leurs variantes en masse. Filtres, tri et relations par query string (?filters=, ?sort=, ?with=), avec liste blanche par Repository : docs/FILTERS.md.

Générateur

make crud name=tag fields="name:str,price:float?,author:fk:authors"
make crud name=category table=categories   # depuis une table existante

Génère modèle, Repository, Controller et câble la route dans routes.py (accès : connexion requise).

  • Types : str, text, int, float, bool, datetime ; ? pour nullable ; champ:fk:table[?] pour une relation N-1, champ:m2m:table pour une relation N-N (table d'association générée).
  • Sans fields=, saisie interactive.
  • Nom de table déduit au pluriel anglais ; table= pour un pluriel irrégulier.
  • La migration reste à générer : make migration name="add tags" puis make migrate.

Le code généré est du code Forge standard, à modifier librement. Construction à la main, pas à pas : docs/INTEGRATION.md.

Scoping utilisateur

Repository.owner_column = "user_id" restreint list/show/edit/delete aux lignes de l'utilisateur connecté et assigne l'owner à la création. Jamais implicite. Exemple : example_app/repositories/note.py.

Pour une vue non scopée, câbler l'action list_all sur une route dédiée avec une permission (GET /notes/all, permission NOTES_ADMIN).

Stockage de fichiers

Chaque fichier appartient à l'utilisateur qui l'a envoyé.

Route Rôle
POST /files/ (multipart, champ file) Upload direct
GET /files/ Liste
GET /files/{id} Upload en cours : statut JSON. Terminé : fichier en ligne (inline).
GET /files/{id}/download Téléchargement forcé (attachment)
DELETE /files/{id} Suppression

Types autorisés : FORGE_STORAGE_ALLOWED_TYPES (images, PDF, Word, Excel, texte/CSV par défaut). Taille max : FORGE_STORAGE_MAX_BYTES (25 Mo). Le nom sur disque est un UUID, jamais le nom envoyé par le client.

Upload par chunks

Pour reprendre un gros envoi après coupure :

  1. POST /files/start {filename, content_type, total_bytes} → {id, ...}
  2. PUT /files/{id} avec un morceau en corps brut, autant de fois que nécessaire, dans l'ordre.
  3. GET /files/{id} donne received_bytes pour reprendre.
  4. POST /files/{id}/complete quand tout est reçu.

Taille max d'un chunk : FORGE_STORAGE_CHUNK_SIZE (1 Mo, renvoyée dans max_chunk_size).

Tâches planifiées

Déclarées dans example_app/schedule.py :

schedule = Schedule(get_settings().scheduler_timezone)

schedule.task(
    "prune-password-reset-tokens",
    "0 3 * * *",
    password_reset.purge_expired,
    "Supprime les liens de reset expirés",
)

Cron à 5 champs (listes, intervalles, pas, alias @daily etc.), validé au démarrage. Fuseau : FORGE_SCHEDULER_TIMEZONE (UTC par défaut).

make schedule-list   # tâches et prochaine exécution
make schedule-run    # exécute les tâches dues, puis sort

Le conteneur scheduler exécute python scripts/schedule.py work en continu. Une seule instance. Sans processus permanent, appeler python scripts/schedule.py run chaque minute (cron système, CronJob Kubernetes).

  • Une tâche en erreur est loguée sans bloquer les autres.
  • Une tâche encore en cours à sa prochaine échéance est ignorée.
  • La minute en cours est sautée au démarrage.
  • SIGTERM attend la fin des tâches en cours.

Logs et audit

Logs applicatifs : logging.getLogger("forge.<module>"). FORGE_LOG_BACKEND=file (défaut) écrit dans storage/logs/forge.log avec rotation ; mongo écrit dans une collection MongoDB. Toute exception non gérée y est consignée ; le client reçoit un 500 générique.

Audit : Repository.loggable = True enregistre create/update/delete dans activity_log (modèle, id, action, auteur, champs modifiés). Les actions MFA sensibles y sont aussi tracées. Structure : docs/SCHEMA.md.

i18n

forge/i18n/locales/*.json, résolu par trans(key, locale). Locale issue de Accept-Language, repli sur FORGE_DEFAULT_LOCALE puis sur la clé.

Mailing

BaseMail : layout commun (templates/layout.html, couleur FORGE_BRAND_COLOR) et un template par email, Jinja2, envoi SMTP asynchrone. En dev, Mailpit capture tout.

Configuration

Toute la configuration passe par forge/config.py (pydantic-settings), lue depuis .env (voir .env.example). Les variables d'environnement l'emportent sur le fichier.

Migrations

make migrate                     # applique les migrations
make migration name="add isbn"   # génère depuis les modèles
make migrate-down                # annule la dernière

Le schéma vient exclusivement d'Alembic. alembic/env.py lit DATABASE_URL depuis forge.config.

Tests et CI

make test        # ou hors Docker : pytest

Base dédiée recréée avant chaque test (SQLite test.db, ou TEST_DATABASE_URL), jamais celle de DATABASE_URL : make test ne touche pas la base de dev. Emails interceptés. Les tests couvrent l'ensemble des modules, MFA compris (vrais codes TOTP, signatures WebAuthn réelles).

CI (.github/workflows/ci.yml), à chaque push et pull request :

  • lint : pyflakes.
  • test-sqlite : la suite sur SQLite.
  • test-postgres : migrations (upgrade head, alembic check, downgrade base) puis la suite sur PostgreSQL.
  • docker-build : Dockerfile et Dockerfile.prod.

Déploiement

Dockerfile.prod (deux étapes, utilisateur non root, sans dépendances de test) et docker-compose.prod.yml (sans bind-mount ni outils de dev). Secrets, HTTPS, limites : docs/DEPLOYMENT.md.

Avant de déployer : définir FORGE_APP_KEY, retirer Adminer et la page /webauthn-test.

Structure

forge/
  routing.py                ControllerRouter, politique d'accès obligatoire par route
  controllers/base.py       BaseController
  repositories/base.py      CRUD générique, filtres, tri, relations
  query_context.py          contexte de requête (filtres, tri, includes)
  middleware/               enveloppe de réponse, locale, query string
  errors.py                 exceptions vers réponses JSON
  db.py, models/base.py     SQLAlchemy async
  security/
    models.py               User, Role, Permission, AccessToken, MFA, reset
    auth_controller.py      login, logout, me, reset de mot de passe
    mfa.py                  TOTP, codes email, pending tokens
    mfa_controller.py       routes MFA, administration MFA
    recovery.py             codes de secours
    webauthn.py             WebAuthn / FIDO2
    crypto.py               chiffrement des secrets TOTP
    password_reset.py       liens de reset
    passwords.py, tokens.py bcrypt, tokens opaques hachés
    dependencies.py         get_current_user, require_permission
    rate_limit.py           limitation en mémoire
    routes.py               routeurs auth et MFA
  storage/                  fichiers, upload par chunks
  scheduler.py              planificateur
  generator/                générateur de CRUD
  audit.py                  activity_log
  logs.py                   configuration des logs
  i18n/                     traductions
  mail/                     BaseMail et templates
  config.py                 configuration centrale

example_app/                application d'exemple (Book, Author, Note)
alembic/                    migrations
scripts/                    seed, permissions, rôles, utilisateurs, planificateur, make:crud
tests/                      suite pytest
bruno/                      collection HTTP
docs/                       FILTERS, RESPONSES, SCHEMA, INTEGRATION, DEPLOYMENT, RIVET

Licence

MIT

About

Framework CRUD/RBAC par convention pour FastAPI : Router/Controller/Repository, RBAC deny-by-default, MFA (TOTP, email, WebAuthn), stockage de fichiers, planificateur et générateur de CRUD.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages