Les neuf gardes intégrés de FLIN couvrent les schémas de contrôle d'accès les plus courants : authentification, rôles, limitation de débit, CSRF, clés API, listes blanches IP, restrictions temporelles, propriété des ressources et filtrage de méthodes. Mais les applications réelles ont des exigences de sécurité spécifiques au domaine qu'aucun framework ne peut anticiper à l'avance.
Une application SaaS doit vérifier les niveaux d'abonnement. Une plateforme éducative doit vérifier le statut d'inscription. Une application financière a besoin de journalisation d'audit sur chaque mutation. Un système multi-tenant doit s'assurer que les utilisateurs n'accèdent qu'aux données de leur propre tenant.
La syntaxe guard_definition et le système de middleware de FLIN permettent aux développeurs de créer ces couches de sécurité personnalisées avec la même API déclarative que les gardes intégrés.
Définir des gardes personnalisés
Le mot-clé guard_definition crée un nouveau type de garde qui peut être utilisé partout où les gardes intégrés sont utilisés :
flin// Définir un garde personnalisé
guard_definition verified_email {
user = request.user
if user == none || !user.emailVerified {
return response {
status: 403
body: { error: "Email verification required" }
}
}
}La définition du garde contient la logique de vérification. Si la fonction retourne une réponse, le garde échoue et cette réponse est envoyée au client. Si la fonction ne retourne rien (passe au travers), le garde réussit.
Utilisez le garde personnalisé exactement comme un garde intégré :
flinguard auth
guard verified_email
route POST {
// Seuls les utilisateurs authentifiés avec un e-mail vérifié arrivent ici
}Gardes personnalisés paramétrés
Les gardes personnalisés peuvent accepter des paramètres pour un comportement configurable :
flinguard_definition subscription(required_plan) {
user = request.user
if user == none {
return response {
status: 401
body: { error: "Authentication required" }
}
}
plan_levels = { "free": 0, "starter": 1, "pro": 2, "enterprise": 3 }
user_level = plan_levels[user.plan] || 0
required_level = plan_levels[required_plan] || 0
if user_level < required_level {
return response {
status: 403
body: {
error: "This feature requires the " + required_plan + " plan",
current_plan: user.plan,
required_plan: required_plan,
upgrade_url: "/pricing"
}
}
}
}
// Utilisation
guard auth
guard subscription("pro")
route POST {
// Seuls les utilisateurs pro et enterprise
}La réponse d'erreur inclut le contexte que le frontend peut utiliser pour afficher une invite de mise à niveau.
Schémas de gardes personnalisés réels
Vérification d'inscription pour plateforme éducative
flinguard_definition enrolled(course_id_param) {
user = request.user
if user == none {
return response { status: 401, body: { error: "Authentication required" } }
}
course_id = to_int(params[course_id_param])
enrollment = Enrollment.where(
user_id == user.id && course_id == course_id
).first
if enrollment == none {
return response {
status: 403
body: {
error: "You are not enrolled in this course",
course_id: course_id,
enroll_url: "/courses/" + to_text(course_id) + "/enroll"
}
}
}
// Attacher l'inscription à la requête pour utilisation en aval
request.enrollment = enrollment
}
// app/api/courses/[course_id]/lessons/[id].flin
guard auth
guard enrolled("course_id")
route GET {
// request.enrollment est disponible
lesson = Lesson.find(params.id)
lesson
}Isolation multi-tenant
flinguard_definition tenant_member {
user = request.user
if user == none {
return response { status: 401, body: { error: "Authentication required" } }
}
tenant_id = to_int(params.tenant_id || headers["X-Tenant-ID"])
membership = TenantMember.where(
user_id == user.id && tenant_id == tenant_id
).first
if membership == none {
return response {
status: 403
body: { error: "You do not have access to this tenant" }
}
}
request.tenant_id = tenant_id
request.tenant_role = membership.role
}
guard_definition tenant_admin {
if request.tenant_role != "admin" && request.tenant_role != "owner" {
return response {
status: 403
body: { error: "Admin access required for this tenant" }
}
}
}
// app/api/tenants/[tenant_id]/settings.flin
guard auth
guard tenant_member
guard tenant_admin
route PUT {
// Seuls les admins et propriétaires du tenant
}Feature flags
flinguard_definition feature(flag_name) {
flag = FeatureFlag.where(name == flag_name).first
if flag == none || !flag.enabled {
return response {
status: 404
body: { error: "This feature is not available" }
}
}
// Vérifier le déploiement spécifique à l'utilisateur
if flag.rollout_percent < 100 {
user = request.user
if user != none {
user_hash = hash_string(to_text(user.id) + flag_name) % 100
if user_hash >= flag.rollout_percent {
return response {
status: 404
body: { error: "This feature is not available" }
}
}
}
}
}
// app/api/beta/ai-search.flin
guard auth
guard feature("ai_search_beta")
route POST {
// Disponible uniquement quand le flag ai_search_beta est activé
// et que l'utilisateur est dans le pourcentage de déploiement
}Schémas de middleware de sécurité
Alors que les gardes gèrent le contrôle d'accès par route, le middleware gère les préoccupations de sécurité transversales. Le middleware personnalisé est défini dans les fichiers _middleware.flin.
Middleware de journalisation d'audit
flin// app/api/_middleware.flin
middleware {
start = now()
next()
// Journaliser après la fin du gestionnaire
if request.method != "GET" && request.method != "OPTIONS" {
save AuditLog {
user_id: request.user_id || 0,
method: request.method,
path: request.path,
ip: request.ip,
status: response.status,
duration_ms: (now() - start),
user_agent: request.user_agent,
body_summary: truncate(to_text(body), 500)
}
}
}Ce middleware journalise chaque requête mutatrice (POST, PUT, DELETE, PATCH) avec l'utilisateur, le point de terminaison, le statut de réponse et un résumé du corps de la requête. La piste d'audit est automatique pour chaque point de terminaison API.
Middleware de signature de requête
Pour les API nécessitant la signature de requête (récepteurs de webhooks, API partenaires) :
flin// app/api/webhooks/_middleware.flin
middleware {
signature = headers["X-Signature-256"]
if signature == "" {
return response { status: 401, body: { error: "Missing signature" } }
}
expected = hmac_sha256(request.raw_body, env("WEBHOOK_SECRET"))
if !constant_time_eq(signature, "sha256=" + expected) {
return response { status: 401, body: { error: "Invalid signature" } }
}
next()
}Middleware de géoblocage
flin// app/api/restricted/_middleware.flin
middleware {
country = geoip_country(request.ip)
blocked = ["XX", "YY"] // Pays sanctionnés
if blocked.contains(country) {
return response {
status: 403
body: { error: "Service not available in your region" }
}
}
request.country = country
next()
}Schémas de composition de gardes
Les gardes personnalisés se composent naturellement avec les gardes intégrés en utilisant la logique ET :
flin// Sécurité maximale pour les opérations financières
guard auth
guard role("accountant", "admin")
guard verified_email
guard subscription("enterprise")
guard tenant_member
guard rate_limit(10, 60)
guard time("08:00", "18:00")
route POST {
// Ce point de terminaison nécessite :
// 1. Authentification
// 2. Rôle comptable ou admin
// 3. Adresse e-mail vérifiée
// 4. Abonnement enterprise
// 5. Appartenance au tenant
// 6. Sous la limite de débit
// 7. Pendant les heures ouvrables
}Sept gardes, sept lignes. Chacun est évalué dans l'ordre. Si l'un échoue, la requête est rejetée avec le code de statut et le message d'erreur appropriés.
Tester les gardes personnalisés
Les gardes personnalisés doivent être testés avec la même rigueur que les gardes intégrés :
flin// test/guards/subscription_guard_test.flin
test "subscription guard rejects free user for pro feature" {
ctx = mock_request_context({ user: { plan: "free" } })
result = guard_subscription(ctx, ["pro"])
assert result.status == 403
}
test "subscription guard accepts pro user for pro feature" {
ctx = mock_request_context({ user: { plan: "pro" } })
result = guard_subscription(ctx, ["pro"])
assert result == pass
}
test "subscription guard accepts enterprise user for pro feature" {
ctx = mock_request_context({ user: { plan: "enterprise" } })
result = guard_subscription(ctx, ["pro"])
assert result == pass
}L'architecture de la sécurité extensible
L'architecture de sécurité de FLIN suit un principe : les valeurs par défaut sont non négociables, les extensions sont illimitées.
Les neuf gardes intégrés et les en-têtes de sécurité automatiques ne peuvent pas être supprimés ou affaiblis. Ils fournissent un socle que chaque application FLIN hérite. Mais au-dessus de ce socle, les développeurs peuvent ajouter autant de gardes personnalisés, de middleware personnalisés et de règles de validation personnalisées qu'ils le souhaitent pour implémenter leurs exigences de sécurité spécifiques.
Cette architecture signifie : - Un nouveau développeur FLIN obtient une sécurité de niveau entreprise dès le départ. - Un développeur expérimenté peut étendre le modèle de sécurité sans combattre le framework. - Les fonctionnalités de sécurité sont visibles, déclaratives et testables. - La surface d'attaque est connue et bornée.
Ceci conclut l'arc 10 -- les fonctionnalités de sécurité de FLIN. Dix articles couvrant la couverture OWASP Top 10, le hachage de mots de passe Argon2, l'authentification JWT, la limitation de débit, les en-têtes de sécurité, la 2FA, OAuth2, WhatsApp OTP, la validation des entrées, les tests de sécurité et les gardes personnalisés. Dans l'arc 11, nous entrons dans la partie la plus innovante de FLIN : l'IA et le moteur d'intentions, où le langage naturel rencontre les requêtes de base de données.
Ceci est la partie 115 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.
Navigation de la série : - [114] 75 tests de sécurité : comment nous avons tout vérifié - [115] Gardes personnalisés et middleware de sécurité (vous êtes ici) - [116] Le moteur d'intentions : requêtes base de données en langage naturel - [117] Recherche sémantique et stockage vectoriel