feat(models): add domain structs and CIDR validation
Add shared models (Network, Host, Port, Application, ApplicationPort) with serde derives for Leptos server function serialization. Add server/validation.rs with valider_ip_dans_reseau() and 5 unit tests. Gate SSR-only modules (config, validation) with #[cfg(feature = "ssr")]. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -836,6 +836,15 @@ dependencies = [
|
|||||||
"rustversion",
|
"rustversion",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ipnetwork"
|
||||||
|
version = "0.20.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
@@ -1597,10 +1606,12 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
|
"ipnetwork",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
|
"serde",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http 0.5.2",
|
"tower-http 0.5.2",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ ssr = [
|
|||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
"dep:tracing-subscriber",
|
"dep:tracing-subscriber",
|
||||||
"dep:dotenvy",
|
"dep:dotenvy",
|
||||||
|
"dep:ipnetwork",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_meta/ssr",
|
"leptos_meta/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
@@ -47,6 +48,9 @@ leptos_router = { version = "0.7", features = [] }
|
|||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
# Macros pour les logs : tracing::info!(), tracing::error!()...
|
# Macros pour les logs : tracing::info!(), tracing::error!()...
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
# Sérialisation/désérialisation — nécessaire pour les server functions Leptos
|
||||||
|
# (les types de retour doivent traverser la frontière server ↔ client)
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
# --- Dépendances serveur uniquement (activées par la feature "ssr") ---
|
# --- Dépendances serveur uniquement (activées par la feature "ssr") ---
|
||||||
|
|
||||||
@@ -62,6 +66,8 @@ tower-http = { version = "0.5", features = ["fs"], optional = true }
|
|||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"], optional = true }
|
||||||
# Charge automatiquement le fichier .env au démarrage du serveur
|
# Charge automatiquement le fichier .env au démarrage du serveur
|
||||||
dotenvy = { version = "0.15", optional = true }
|
dotenvy = { version = "0.15", optional = true }
|
||||||
|
# Parsing et calcul de plages d'adresses IP (CIDR) — ex: 192.168.1.0/24
|
||||||
|
ipnetwork = { version = "0.20", optional = true }
|
||||||
|
|
||||||
# --- Dépendances client uniquement (activées par la feature "hydrate") ---
|
# --- Dépendances client uniquement (activées par la feature "hydrate") ---
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
// `pub` les rend accessibles depuis main.rs et d'autres crates.
|
// `pub` les rend accessibles depuis main.rs et d'autres crates.
|
||||||
pub mod app; // Composant racine App() et configuration du routeur
|
pub mod app; // Composant racine App() et configuration du routeur
|
||||||
pub mod client; // Pages et composants de l'interface utilisateur
|
pub mod client; // Pages et composants de l'interface utilisateur
|
||||||
|
pub mod models; // Structs de données partagés server + client (Network, Host, Port, Application)
|
||||||
pub mod server; // Handlers HTTP et logique métier côté serveur
|
pub mod server; // Handlers HTTP et logique métier côté serveur
|
||||||
|
|
||||||
// Point d'entrée WebAssembly — exécuté par le navigateur au chargement du bundle .wasm
|
// Point d'entrée WebAssembly — exécuté par le navigateur au chargement du bundle .wasm
|
||||||
|
|||||||
134
src/models.rs
Normal file
134
src/models.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
// models.rs — Modèles de données partagés (server + client)
|
||||||
|
//
|
||||||
|
// Ce module définit les structs qui représentent les entités métier du projet IPAM.
|
||||||
|
// Ils sont compilés côté serveur ET côté WASM car Leptos en a besoin des deux côtés :
|
||||||
|
// - Serveur : pour lire/écrire en BDD et rendre le HTML
|
||||||
|
// - Client : pour afficher les données dans les composants Leptos
|
||||||
|
//
|
||||||
|
// Chaque struct dérive `Serialize` et `Deserialize` de serde.
|
||||||
|
// C'est obligatoire pour que Leptos puisse transférer les données entre
|
||||||
|
// le serveur et le navigateur via les "server functions" (#[server]).
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// ─── Réseau ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Un réseau IP défini par sa plage CIDR.
|
||||||
|
///
|
||||||
|
/// Exemple : { id: 1, cidr: "192.168.1.0/24" }
|
||||||
|
/// → plage de 192.168.1.0 à 192.168.1.255 (254 hôtes utilisables)
|
||||||
|
///
|
||||||
|
/// La notation CIDR (Classless Inter-Domain Routing) combine l'adresse réseau
|
||||||
|
/// et le masque en un seul champ : <adresse>/<longueur du préfixe>.
|
||||||
|
/// /24 = 24 bits de masque = 255.255.255.0
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Network {
|
||||||
|
/// Identifiant unique auto-incrémenté par la base de données.
|
||||||
|
/// `i64` est le type entier signé 64 bits — correspond à `BIGINT` en SQL.
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// Plage d'adresses en notation CIDR.
|
||||||
|
/// Ex: "10.0.0.0/8", "172.16.0.0/12", "192.168.1.0/24"
|
||||||
|
pub cidr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Hôte ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Un hôte (machine, serveur, équipement réseau) appartenant à un réseau.
|
||||||
|
///
|
||||||
|
/// Contrainte : l'IP doit appartenir à la plage CIDR du réseau référencé
|
||||||
|
/// par `network_id`. Cette contrainte est vérifiée à la création/modification.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Host {
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// Nom descriptif de l'hôte. Ex: "serveur-web-01", "routeur-principal"
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
/// Adresse IPv4 de l'hôte, stockée en texte.
|
||||||
|
/// Ex: "192.168.1.10"
|
||||||
|
/// On utilise String plutôt que IpAddr pour simplifier la sérialisation
|
||||||
|
/// et le stockage en base de données.
|
||||||
|
pub ip: String,
|
||||||
|
|
||||||
|
/// Référence vers le réseau auquel appartient cet hôte.
|
||||||
|
/// C'est une "clé étrangère" (Foreign Key) vers la table `networks`.
|
||||||
|
pub network_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Port ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Un port réseau ouvert sur un hôte, avec sa description probable.
|
||||||
|
///
|
||||||
|
/// Les numéros de ports standards (0–1023) sont des "well-known ports".
|
||||||
|
/// Un port peut être associé à plusieurs applications (association non-stricte).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Port {
|
||||||
|
/// Numéro de port TCP/UDP.
|
||||||
|
/// `u16` : entier non signé 16 bits → plage 0 à 65535.
|
||||||
|
/// C'est exactement la plage valide pour les ports réseau.
|
||||||
|
pub number: u16,
|
||||||
|
|
||||||
|
/// Description du protocole probable sur ce port.
|
||||||
|
/// `Option<String>` : peut être absent (None) si le protocole est inconnu.
|
||||||
|
/// Ex: Some("SSH"), Some("HTTPS"), None
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
/// Hôte sur lequel ce port est ouvert.
|
||||||
|
pub host_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Port {
|
||||||
|
/// Retourne la description standard pour les ports les plus courants.
|
||||||
|
/// Utilisé pour pré-remplir la description lors de l'ajout d'un port.
|
||||||
|
///
|
||||||
|
/// `match` est l'équivalent Rust d'un switch/case, mais exhaustif :
|
||||||
|
/// le compilateur oblige à gérer tous les cas possibles.
|
||||||
|
pub fn protocole_connu(numero: u16) -> Option<&'static str> {
|
||||||
|
// `&'static str` : référence vers une chaîne qui vit toute la durée du programme
|
||||||
|
// (les littéraux de chaînes comme "SSH" sont stockés dans le binaire compilé)
|
||||||
|
match numero {
|
||||||
|
21 => Some("FTP"),
|
||||||
|
22 => Some("SSH"),
|
||||||
|
23 => Some("Telnet"),
|
||||||
|
25 => Some("SMTP"),
|
||||||
|
53 => Some("DNS"),
|
||||||
|
80 => Some("HTTP"),
|
||||||
|
110 => Some("POP3"),
|
||||||
|
143 => Some("IMAP"),
|
||||||
|
443 => Some("HTTPS"),
|
||||||
|
3306 => Some("MySQL"),
|
||||||
|
5432 => Some("PostgreSQL"),
|
||||||
|
6379 => Some("Redis"),
|
||||||
|
8080 => Some("HTTP alternatif"),
|
||||||
|
_ => None, // `_` est le pattern "tout le reste" (wildcard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Application ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Une application qui utilise un ou plusieurs ports.
|
||||||
|
///
|
||||||
|
/// L'association entre application et port est non-stricte :
|
||||||
|
/// un même port peut être partagé par plusieurs applications.
|
||||||
|
/// Ex: le port 80 peut être utilisé par Nginx ET par un proxy applicatif.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Application {
|
||||||
|
pub id: i64,
|
||||||
|
|
||||||
|
/// Nom de l'application. Ex: "Nginx", "PostgreSQL", "Prometheus"
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Association Application ↔ Port ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Lien entre une application et un port (relation many-to-many).
|
||||||
|
///
|
||||||
|
/// On utilise un struct dédié plutôt qu'un Vec<Port> dans Application
|
||||||
|
/// pour correspondre directement à la table de jointure en base de données.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ApplicationPort {
|
||||||
|
pub application_id: i64,
|
||||||
|
pub port_number: u16,
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
// server/mod.rs — Module serveur
|
// server/mod.rs — Module serveur
|
||||||
//
|
//
|
||||||
// Contient tout le code qui s'exécute uniquement côté serveur :
|
// Contient tout le code qui s'exécute uniquement côté serveur.
|
||||||
// - Handlers HTTP additionnels (routes.rs)
|
|
||||||
// - Fonctions serveur Leptos : accès à la base de données, authentification...
|
|
||||||
// - Logique métier qui ne doit JAMAIS être exposée dans le bundle WASM
|
|
||||||
//
|
//
|
||||||
// Les "server functions" Leptos (marquées #[server]) sont déclarées ici.
|
// Certains sous-modules utilisent des crates SSR-only (dotenvy, ipnetwork...)
|
||||||
// Elles apparaissent comme des appels asynchrones normaux côté client,
|
// et sont donc protégés par `#[cfg(feature = "ssr")]` pour ne pas être
|
||||||
// mais s'exécutent exclusivement sur le serveur — transparence totale.
|
// compilés dans le bundle WASM.
|
||||||
|
|
||||||
pub mod config;
|
// Accessible des deux côtés (handlers HTTP sont SSR mais le module est déclaré shared)
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
|
||||||
|
// Modules SSR uniquement — utilisent des crates non disponibles en WASM
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub mod validation;
|
||||||
|
|||||||
110
src/server/validation.rs
Normal file
110
src/server/validation.rs
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// server/validation.rs — Validation des données métier
|
||||||
|
//
|
||||||
|
// Ce module vérifie les règles métier qui ne peuvent pas être exprimées
|
||||||
|
// uniquement par les types Rust. Il s'exécute côté serveur uniquement
|
||||||
|
// car il utilise `ipnetwork` pour les calculs CIDR.
|
||||||
|
|
||||||
|
use ipnetwork::IpNetwork;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
// ─── Erreurs de validation ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ValidationError {
|
||||||
|
/// Le format CIDR est invalide (ex: "192.168.1/24" au lieu de "192.168.1.0/24")
|
||||||
|
#[error("CIDR invalide '{0}' : {1}")]
|
||||||
|
CidrInvalide(String, ipnetwork::IpNetworkError),
|
||||||
|
|
||||||
|
/// L'adresse IP n'a pas le bon format
|
||||||
|
#[error("Adresse IP invalide '{0}' : {1}")]
|
||||||
|
IpInvalide(String, std::net::AddrParseError),
|
||||||
|
|
||||||
|
/// L'IP de l'hôte n'appartient pas à la plage du réseau
|
||||||
|
#[error("L'adresse IP {ip} n'appartient pas au réseau {cidr}")]
|
||||||
|
IpHorsReseau { ip: String, cidr: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Validation du CIDR ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Vérifie qu'une chaîne est un CIDR valide.
|
||||||
|
/// Retourne le réseau parsé si valide.
|
||||||
|
///
|
||||||
|
/// `&str` : on emprunte la chaîne sans en prendre possession (pas de copie).
|
||||||
|
/// `Result<T, E>` : soit une valeur T (succès), soit une erreur E (échec).
|
||||||
|
pub fn valider_cidr(cidr: &str) -> Result<IpNetwork, ValidationError> {
|
||||||
|
cidr.parse::<IpNetwork>()
|
||||||
|
// `.map_err` transforme l'erreur si `parse` échoue
|
||||||
|
// `|e|` est une closure : une fonction anonyme qui prend `e` en paramètre
|
||||||
|
.map_err(|e| ValidationError::CidrInvalide(cidr.to_string(), e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Validation de l'appartenance IP ↔ Réseau ────────────────────────────────
|
||||||
|
|
||||||
|
/// Vérifie qu'une adresse IP appartient à la plage d'un réseau CIDR.
|
||||||
|
///
|
||||||
|
/// Règle métier clé : un hôte doit toujours être dans le réseau auquel il appartient.
|
||||||
|
/// Ex : 192.168.1.10 ✓ dans 192.168.1.0/24
|
||||||
|
/// 10.0.0.1 ✗ dans 192.168.1.0/24
|
||||||
|
pub fn valider_ip_dans_reseau(ip: &str, cidr: &str) -> Result<(), ValidationError> {
|
||||||
|
// On parse le CIDR et l'IP, propageant les erreurs avec `?`
|
||||||
|
let reseau = valider_cidr(cidr)?;
|
||||||
|
|
||||||
|
let adresse: IpAddr = ip
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ValidationError::IpInvalide(ip.to_string(), e))?;
|
||||||
|
|
||||||
|
// `IpNetwork::contains` retourne true si l'IP est dans la plage
|
||||||
|
if reseau.contains(adresse) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ValidationError::IpHorsReseau {
|
||||||
|
ip: ip.to_string(),
|
||||||
|
cidr: cidr.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// `#[cfg(test)]` : ce bloc n'est compilé que lors de `cargo test`
|
||||||
|
// Écrire les tests directement dans le même fichier est idiomatique en Rust.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
// `super::*` importe tout depuis le module parent (ce fichier)
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ip_dans_reseau_valide() {
|
||||||
|
// `.unwrap()` est acceptable dans les tests — si ça échoue, le test échoue
|
||||||
|
assert!(valider_ip_dans_reseau("192.168.1.10", "192.168.1.0/24").is_ok());
|
||||||
|
assert!(valider_ip_dans_reseau("10.0.0.1", "10.0.0.0/8").is_ok());
|
||||||
|
assert!(valider_ip_dans_reseau("172.16.5.100", "172.16.0.0/12").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ip_hors_reseau() {
|
||||||
|
assert!(valider_ip_dans_reseau("10.0.0.1", "192.168.1.0/24").is_err());
|
||||||
|
assert!(valider_ip_dans_reseau("192.168.2.1", "192.168.1.0/24").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cidr_invalide() {
|
||||||
|
assert!(valider_cidr("pas-un-cidr").is_err());
|
||||||
|
assert!(valider_cidr("192.168.1/24").is_err()); // adresse tronquée
|
||||||
|
assert!(valider_cidr("192.168.1.0/33").is_err()); // préfixe > 32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ip_invalide() {
|
||||||
|
assert!(valider_ip_dans_reseau("999.0.0.1", "192.168.1.0/24").is_err());
|
||||||
|
assert!(valider_ip_dans_reseau("pas-une-ip", "192.168.1.0/24").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocole_connu() {
|
||||||
|
assert_eq!(crate::models::Port::protocole_connu(22), Some("SSH"));
|
||||||
|
assert_eq!(crate::models::Port::protocole_connu(443), Some("HTTPS"));
|
||||||
|
assert_eq!(crate::models::Port::protocole_connu(9999), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user