El middleware maneja preocupaciones transversales a nivel de directorio. Pero a veces necesitas control de acceso detallado a nivel de ruta. Un panel de administración podría requerir autenticación para todas las rutas (middleware), pero solo permitir el método DELETE para usuarios con el rol "superadmin". Una API pública podría permitir solicitudes GET ilimitadas pero limitar las solicitudes POST a 10 por minuto.
El sistema de guards de FLIN proporciona esta granularidad. Los guards son declaraciones de una línea al principio de un archivo de ruta o dentro de un bloque de ruta que imponen restricciones de seguridad antes de que se ejecute el manejador. Si un guard falla, la solicitud se rechaza con un código de estado HTTP apropiado. El manejador nunca se ejecuta.
Sintaxis de guards
Los guards se declaran con la palabra clave guard seguida del nombre del guard y parámetros opcionales:
flin// app/api/admin/users.flin
guard auth // Debe estar autenticado
guard role("admin", "superadmin") // Debe tener uno de estos roles
guard rate_limit(50, 60) // 50 solicitudes por 60 segundos
route GET {
User.all
}
route DELETE {
guard owner(params.id) // Guard adicional solo para DELETE
user = User.find(params.id)
delete user
{ success: true }
}Los guards a nivel de archivo se aplican a cada bloque de ruta en el archivo. Los guards a nivel de ruta se aplican solo a su bloque específico. Ambos se evalúan antes de que se ejecute el código del manejador.
Los nueve guards integrados
FLIN viene con nueve guards que cubren los patrones de control de acceso más comunes:
guard auth
Requiere que la solicitud esté autenticada. Verifica session.user o la cabecera Authorization en busca de un token válido.
flinguard auth
route GET {
// Solo usuarios autenticados llegan aquí
User.find(session.userId)
}Respuesta de fallo: 401 Unauthorized
guard role
Requiere que el usuario autenticado tenga uno de los roles especificados:
flinguard auth
guard role("admin", "moderator")
route GET {
// Solo administradores y moderadores
User.all
}Respuesta de fallo: 403 Forbidden
guard owner
Requiere que el usuario autenticado sea propietario del recurso al que se accede, o que sea administrador:
flinguard auth
guard owner(params.id)
route PUT {
user = User.find(params.id)
user.name = body.name
save user
user
}El guard owner compara el ID del usuario autenticado con el campo user_id o id del recurso. Los administradores omiten la verificación.
Respuesta de fallo: 403 Forbidden
guard rate_limit
Impone limitación de velocidad basada en la dirección IP del cliente:
flinguard rate_limit(10, 60) // 10 solicitudes por 60 segundos
route POST {
// Protegido contra abuso
}El limitador de velocidad usa un algoritmo de ventana deslizante. El primer parámetro es el número máximo de solicitudes, el segundo es la duración de la ventana en segundos.
Respuesta de fallo: 429 Too Many Requests con cabecera Retry-After
guard csrf
Requiere un token CSRF válido en el cuerpo de la solicitud o las cabeceras. Se agrega automáticamente a los formularios cuando este guard está activo:
flinguard csrf
route POST {
// Token CSRF validado antes de que esto se ejecute
}Respuesta de fallo: 403 Forbidden
guard api_key
Requiere una clave API válida en la cabecera X-API-Key:
flinguard api_key
route GET {
// Solo solicitudes con clave API válida
}Respuesta de fallo: 401 Unauthorized
guard ip
Restringe el acceso a direcciones IP específicas:
flinguard ip("10.0.0.1", "10.0.0.2", "192.168.1.0/24")
route GET {
// Solo desde IPs en la lista blanca
}Respuesta de fallo: 403 Forbidden
guard time
Restringe el acceso a ventanas de tiempo específicas:
flinguard time("09:00", "17:00")
route POST {
// Solo durante horas laborales (zona horaria del servidor)
}Respuesta de fallo: 403 Forbidden con mensaje indicando cuándo el acceso estará disponible
guard method
Restringe qué métodos HTTP están permitidos:
flinguard method("GET", "POST")
route DELETE {
// Esta ruta es inalcanzable porque DELETE no está en los métodos permitidos
}Respuesta de fallo: 405 Method Not Allowed
Composición de guards
Los guards se componen con lógica AND. Todos los guards deben pasar para que el manejador se ejecute:
flinguard auth // 1. Debe estar autenticado
guard role("admin") // 2. Debe ser administrador
guard rate_limit(10, 60) // 3. Dentro del límite de velocidad
guard time("09:00", "17:00") // 4. Durante horas laborales
route DELETE {
// Requiere que las CUATRO condiciones sean verdaderas
user = User.find(params.id)
delete user
{ success: true }
}Los guards se evalúan en orden de declaración. Si el guard #2 falla, los guards #3 y #4 no se evalúan. Este comportamiento de cortocircuito es tanto una optimización de rendimiento como una característica de seguridad: una solicitud no autenticada con límite de velocidad no consume un token de límite de velocidad.
Guards personalizados
Los guards integrados cubren casos comunes, pero las aplicaciones a menudo necesitan control de acceso específico del dominio. FLIN soporta definiciones de guards personalizados:
flin// Definir un guard personalizado
guard_definition verified_email {
user = request.user
if !user || !user.email_verified {
return response {
status: 403
body: { error: "Email not verified" }
}
}
}
guard_definition subscription(plan) {
user = request.user
if !user || user.plan != plan {
return response {
status: 403
body: { error: "Requires " + plan + " subscription" }
}
}
}Los guards personalizados se usan exactamente como los guards integrados:
flinguard auth
guard verified_email
guard subscription("pro")
route POST {
// Solo suscriptores pro verificados
}Implementación de guards
Internamente, los guards se compilan en funciones predicado simples que devuelven Ok(()) (pasa) o Err(Response) (falla):
rustpub enum GuardResult {
Pass,
Fail(Response),
}
pub struct CompiledGuard {
name: String,
params: Vec<Value>,
check: fn(&RequestContext, &[Value]) -> GuardResult,
}
fn evaluate_guards(
guards: &[CompiledGuard],
ctx: &RequestContext,
) -> Result<(), Response> {
for guard in guards {
match (guard.check)(ctx, &guard.params) {
GuardResult::Pass => continue,
GuardResult::Fail(response) => return Err(response),
}
}
Ok(())
}La implementación del guard auth es sencilla:
rustfn guard_auth(ctx: &RequestContext, _params: &[Value]) -> GuardResult {
// Verificar sesión
if ctx.session.get("user").is_some() {
return GuardResult::Pass;
}
// Verificar cabecera Authorization (token Bearer)
if let Some(auth) = ctx.headers.get("authorization") {
if auth.starts_with("Bearer ") {
let token = &auth[7..];
if verify_jwt(token, &ctx.jwt_secret).is_ok() {
return GuardResult::Pass;
}
}
}
GuardResult::Fail(Response::unauthorized("Authentication required"))
}Guards vs middleware: cuándo usar cada uno
Los guards y el middleware sirven propósitos diferentes, y usar la herramienta correcta importa:
Usa middleware para preocupaciones transversales que se aplican a muchas rutas: registro, cabeceras CORS, generación de ID de solicitud, hidratación de sesión. El middleware vive en _middleware.flin y se aplica a árboles de directorios completos.
Usa guards para control de acceso en rutas o métodos específicos: autenticación, autorización, limitación de velocidad, protección CSRF. Los guards viven en el archivo de ruta y son visibles junto al código que protegen.
La distinción es práctica, no teórica. La verificación de autenticación podría hacerse con middleware o guards. Pero colocarla en un guard significa que el requisito de acceso es visible cuando lees el archivo de ruta -- no necesitas rastrear el árbol de directorios para encontrar el middleware que lo impone.
flin// Claro: los requisitos de acceso son visibles en el archivo
guard auth
guard role("admin")
route DELETE {
delete User.find(params.id)
}Versus:
flin// Poco claro: ¿está protegido? Necesitas verificar _middleware.flin
route DELETE {
delete User.find(params.id)
}En la práctica, la mayoría de las aplicaciones FLIN usan middleware para la verificación de autenticación (redirección al login) y guards para control de acceso basado en roles (devolver 403). Esta combinación significa que los usuarios no autenticados son redirigidos mientras que los usuarios autenticados pero no autorizados reciben un error claro.
Patrones de guards del mundo real
API pública con límites escalonados
flin// app/api/search.flin
guard rate_limit(100, 60) // Nivel gratuito: 100/min
route GET {
if request.user && request.user.plan == "pro" {
// Los usuarios pro obtienen límites más altos (verificado a nivel de aplicación)
}
search(query.q)
}Operaciones destructivas solo para administradores
flin// app/api/users/[id].flin
guard auth
route GET {
User.find(params.id) // Cualquier usuario autenticado puede leer
}
route PUT {
guard owner(params.id) // Debe ser propietario del recurso para actualizar
// ...
}
route DELETE {
guard role("admin") // Solo administradores pueden eliminar
// ...
}Operaciones financieras con restricción de tiempo
flin// app/api/transactions.flin
guard auth
guard role("accountant", "admin")
guard time("08:00", "18:00")
route POST {
// Transacciones financieras solo durante horas laborales
}Los guards hacen que la seguridad sea visible, componible e imposible de olvidar. Cuando un nuevo desarrollador lee un archivo de ruta, los requisitos de acceso son lo primero que ve. Cuando un auditor de seguridad revisa la base de código, cada protección está declarada explícitamente en el punto de uso.
En el próximo artículo, exploramos el soporte WebSocket integrado de FLIN -- comunicación en tiempo real sin un servidor o biblioteca WebSocket separado.
Esta es la Parte 102 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA diseñaron y construyeron un lenguaje de programación desde cero.
Navegación de la serie: - [100] Inyección de contexto de solicitud - [101] El sistema de middleware - [102] Guards: seguridad declarativa para rutas (estás aquí) - [103] Soporte WebSocket integrado en el lenguaje