Saltar al contenido

Nombrar comandos: operaciones, no mecanismos

El nombre de un comando decide cuánta autoridad cede el core. La diferencia entre un verbo de dominio y un verbo de sistema, las tres señales de un nombre peligroso, y por qué un buen nombre es una medida de seguridad.

Dominio frente a mecanismo

La columna de la izquierda tiene algo en común: son las operaciones del sistema, con sus nombres. La de la derecha son las operaciones de tu producto. Un core cuyos comandos se leen como la lista de cosas que la aplicación hace es un core de dominio; uno cuyos comandos se leen como una biblioteca estándar es un puente hacia el sistema operativo con pasos intermedios.

Las tres señales

Un nombre es sospechoso si cumple alguna:

1 · El verbo es del sistema y no del producto. leer, escribir, ejecutar, borrar, listar, peticion. Si el nombre podría estar en la biblioteca estándar de cualquier lenguaje, no es de tu dominio.

2 · Un argumento es una ruta, una URL, una sentencia o un nombre de programa. Cualquier cosa que el core vaya a interpretar como una instrucción sobre el mundo. Un identificador de tu dominio, en cambio, es un dato: el core decide qué significa.

3 · El comando serviría igual en otra aplicación. guardar_json(ruta, datos) vale para cualquier proyecto. Eso no es reutilización: es que no dice nada de tu dominio, y por tanto no acota nada.

Convertir uno peligroso

El procedimiento es siempre el mismo: subir el nivel de abstracción hasta que el core recupere la decisión.

Rust
// ANTES: el frontend decide qué se lee.#[tauri::command]async fn leer_fichero(ruta: String) -> Result<String, ErrorPayload> {    Ok(std::fs::read_to_string(&ruta)?)}
Rust
// DESPUÉS: el frontend pide una operación; el core decide el qué y el dónde.#[tauri::command]async fn cargar_proyecto(app: tauri::AppHandle, id: ProyectoId) -> Result<Proyecto, ErrorPayload> {    let base = app.path().app_data_dir()?;         // el core elige el directorio    let ruta = base.join("proyectos").join(id.como_nombre_de_fichero());    // el identificador es un tipo propio, así que no puede ser "../../etc/passwd"    Ok(Proyecto::leer(&ruta).await?)}

Dos detalles del después que hacen el trabajo:

  • El directorio lo pone el core, a partir de su identidad —nivel 7.1— y no de nada que venga de fuera.
  • El identificador es un tipo propio, no una cadena. Un tipo que solo se puede construir validando descarta de raíz la familia de ataques por travesía de rutas, porque el valor peligroso ni siquiera se puede representar.

Ese segundo punto es donde el sistema de tipos de Rust devuelve lo que costó aprenderlo.

Por qué esto es seguridad

Merece decirlo explícito, porque suena a preferencia de estilo. Un comando de dominio limita el daño de un frontend comprometido a las operaciones que tu aplicación de verdad hace; uno de mecanismo lo limita a lo que el usuario puede hacer con su cuenta.

Y hay una consecuencia práctica en la fase V: los nombres de tus comandos acaban en el fichero de permisos. Un capabilities/ que dice allow-cargar-proyecto describe una superficie; uno que dice allow-leer-fichero no describe nada, porque leer un fichero es todos los ficheros. Un mal nombre hace que la auditoría del nivel 33 sea imposible de hacer bien, por muy en serio que se la tome quien la haga.