use crate::api::{ApiError, AuthUser}; use crate::audit_log; use crate::control_plane::{ AdminNodesResponse, CreateNetworkRequest as CpCreateNetworkRequest, CreateTokenRequest as CpCreateTokenRequest, CreateTokenResponse, EnrollmentToken, KeyHistoryResponse, KeyPolicyResponse, KeyRotationRequest, KeyRotationResponse, KeyRotationPolicy, NetworkInfo, }; use crate::models::{ControlPlane, Network}; use crate::permissions::{ ensure_permission_global, PERM_ACL_READ, PERM_ACL_WRITE, PERM_KEY_POLICY_READ, PERM_KEY_POLICY_WRITE, PERM_NETWORKS_READ, PERM_NETWORKS_WRITE, PERM_NODES_READ, PERM_NODES_WRITE, PERM_TOKENS_WRITE, PERM_AUDIT_READ, }; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::routing::{get, post}; use axum::{Json, Router}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; #[derive(Debug, Serialize)] struct NetworkSummary { id: Uuid, control_plane_id: Uuid, control_plane_name: String, network_id: String, name: String, dns_domain: Option, overlay_v4: Option, overlay_v6: Option, requires_approval: bool, created_at: OffsetDateTime, updated_at: OffsetDateTime, } #[derive(Debug, Deserialize)] struct CreateNetworkRequest { control_plane_id: Uuid, name: String, overlay_v4: Option, overlay_v6: Option, dns_domain: Option, requires_approval: Option, key_rotation_max_age_seconds: Option, bootstrap_token_ttl_seconds: Option, bootstrap_token_uses: Option, bootstrap_token_tags: Option>, } #[derive(Debug, Serialize)] struct CreateNetworkResult { network: NetworkSummary, bootstrap_token: Option, } #[derive(Debug, Deserialize)] struct CreateTokenRequest { ttl_seconds: u64, uses: u32, tags: Vec, } #[derive(Debug, Deserialize)] struct RevokeTokenRequest { token_id: String, } #[derive(Debug, Deserialize)] struct RotateKeysRequest { machine_public_key: Option, wg_public_key: Option, } #[derive(Debug, Deserialize)] struct UpdateAclRequest { policy: Value, } #[derive(Debug, Deserialize)] struct NetworkAuditQuery { node_id: Option, limit: Option, } pub fn router() -> Router { Router::new() .route("/", get(list_networks).post(create_network)) .route("/:id", get(get_network).delete(delete_network)) .route("/:id/tokens", post(create_token)) .route("/:id/tokens/revoke", post(revoke_token)) .route("/:id/nodes", get(list_nodes)) .route("/:id/nodes/:node_id/approve", post(approve_node)) .route("/:id/nodes/:node_id/revoke", post(revoke_node)) .route("/:id/nodes/:node_id/rotate-keys", post(rotate_keys)) .route("/:id/nodes/:node_id/keys", get(node_keys)) .route("/:id/acl", get(get_acl).put(update_acl)) .route("/:id/key-policy", get(get_key_policy).put(update_key_policy)) .route("/:id/audit", get(network_audit)) } async fn list_networks( State(state): State, AuthUser { user }: AuthUser, ) -> Result>, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NETWORKS_READ).await?; let rows = sqlx::query_as::<_, NetworkRow>( r#" SELECT n.id, n.control_plane_id, n.network_id, n.name, n.dns_domain, n.overlay_v4, n.overlay_v6, n.requires_approval, n.created_at, n.updated_at, c.name AS control_plane_name FROM networks n JOIN control_planes c ON c.id = n.control_plane_id ORDER BY n.created_at DESC "#, ) .fetch_all(&state.pool) .await .map_err(|_| ApiError::Internal)?; let out = rows .into_iter() .map(|row| NetworkSummary { id: row.id, control_plane_id: row.control_plane_id, control_plane_name: row.control_plane_name, network_id: row.network_id, name: row.name, dns_domain: row.dns_domain, overlay_v4: row.overlay_v4, overlay_v6: row.overlay_v6, requires_approval: row.requires_approval, created_at: row.created_at, updated_at: row.updated_at, }) .collect(); Ok(Json(out)) } async fn get_network( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NETWORKS_READ).await?; let row = fetch_network_summary(&state.pool, id).await?; Ok(Json(NetworkSummary { id: row.id, control_plane_id: row.control_plane_id, control_plane_name: row.control_plane_name, network_id: row.network_id, name: row.name, dns_domain: row.dns_domain, overlay_v4: row.overlay_v4, overlay_v6: row.overlay_v6, requires_approval: row.requires_approval, created_at: row.created_at, updated_at: row.updated_at, })) } async fn delete_network( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, ) -> Result { ensure_permission_global(&state.pool, &user, PERM_NETWORKS_WRITE).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); // Send delete request to control plane client .delete_network(&network.network_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; // Delete from database sqlx::query("DELETE FROM networks WHERE id = $1") .bind(id) .execute(&state.pool) .await .map_err(|_| ApiError::Internal)?; audit_log::record( &state.pool, Some(user.id), "network.delete", Some("network"), Some(&network.id.to_string()), Some(serde_json::json!({ "network_id": network.network_id, "control_plane_id": control_plane.id, })), ) .await?; Ok(StatusCode::NO_CONTENT) } async fn create_network( State(state): State, AuthUser { user }: AuthUser, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NETWORKS_WRITE).await?; let control_plane = fetch_control_plane(&state.pool, req.control_plane_id).await?; let client = crate::control_plane::ControlPlaneClient::new( control_plane.base_url.clone(), control_plane.admin_token.clone(), ); let response = client .create_network(CpCreateNetworkRequest { name: req.name, overlay_v4: req.overlay_v4.clone(), overlay_v6: req.overlay_v6.clone(), dns_domain: req.dns_domain.clone(), requires_approval: req.requires_approval, key_rotation_max_age_seconds: req.key_rotation_max_age_seconds, bootstrap_token_ttl_seconds: req.bootstrap_token_ttl_seconds, bootstrap_token_uses: req.bootstrap_token_uses, bootstrap_token_tags: req.bootstrap_token_tags.clone(), }) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; let record = insert_network( &state.pool, &control_plane, &response.network, req.dns_domain, ) .await?; audit_log::record( &state.pool, Some(user.id), "network.create", Some("network"), Some(&record.id.to_string()), Some(serde_json::json!({ "network_id": response.network.id, "control_plane_id": control_plane.id, })), ) .await?; let summary = fetch_network_summary(&state.pool, record.id).await?; Ok(Json(CreateNetworkResult { network: NetworkSummary { id: summary.id, control_plane_id: summary.control_plane_id, control_plane_name: summary.control_plane_name, network_id: summary.network_id, name: summary.name, dns_domain: summary.dns_domain, overlay_v4: summary.overlay_v4, overlay_v6: summary.overlay_v6, requires_approval: summary.requires_approval, created_at: summary.created_at, updated_at: summary.updated_at, }, bootstrap_token: response.bootstrap_token, })) } async fn create_token( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_TOKENS_WRITE).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .create_token( &network.network_id, CpCreateTokenRequest { ttl_seconds: req.ttl_seconds, uses: req.uses, tags: req.tags, owner_user_id: Some(user.id.to_string()), owner_email: Some(user.email.clone()), owner_is_admin: Some(true), }, ) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "token.create", Some("network"), Some(&network.id.to_string()), None, ) .await?; Ok(Json(response)) } async fn revoke_token( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_TOKENS_WRITE).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .revoke_token(&req.token_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "token.revoke", Some("network"), Some(&network.id.to_string()), Some(serde_json::json!({ "token_id": req.token_id })), ) .await?; Ok(Json(response)) } async fn list_nodes( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NODES_READ).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .list_nodes(&network.network_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; Ok(Json(response)) } async fn approve_node( State(state): State, AuthUser { user }: AuthUser, Path((id, node_id)): Path<(Uuid, String)>, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .approve_node(&node_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "node.approve", Some("node"), Some(&node_id), None, ) .await?; Ok(Json(response)) } async fn revoke_node( State(state): State, AuthUser { user }: AuthUser, Path((id, node_id)): Path<(Uuid, String)>, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .revoke_node(&node_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "node.revoke", Some("node"), Some(&node_id), None, ) .await?; Ok(Json(response)) } async fn rotate_keys( State(state): State, AuthUser { user }: AuthUser, Path((id, node_id)): Path<(Uuid, String)>, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NODES_WRITE).await?; let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .rotate_keys( &node_id, KeyRotationRequest { machine_public_key: req.machine_public_key, wg_public_key: req.wg_public_key, }, ) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "node.rotate_keys", Some("node"), Some(&node_id), None, ) .await?; Ok(Json(response)) } async fn node_keys( State(state): State, AuthUser { user }: AuthUser, Path((id, node_id)): Path<(Uuid, String)>, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_NODES_READ).await?; let (_network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .node_keys(&node_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; Ok(Json(response)) } async fn get_acl( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_ACL_READ).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .get_acl(&network.network_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; Ok(Json(response)) } async fn update_acl( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_ACL_WRITE).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .update_acl(&network.network_id, req.policy) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "network.update_acl", Some("network"), Some(&network.id.to_string()), None, ) .await?; Ok(Json(response)) } async fn get_key_policy( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_KEY_POLICY_READ).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .get_key_policy(&network.network_id) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; Ok(Json(response)) } async fn update_key_policy( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, Json(req): Json, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_KEY_POLICY_WRITE).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .update_key_policy(&network.network_id, req) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; audit_log::record( &state.pool, Some(user.id), "network.update_key_policy", Some("network"), Some(&network.id.to_string()), None, ) .await?; Ok(Json(response)) } async fn network_audit( State(state): State, AuthUser { user }: AuthUser, Path(id): Path, Query(query): Query, ) -> Result, ApiError> { ensure_permission_global(&state.pool, &user, PERM_AUDIT_READ).await?; let (network, control_plane) = fetch_network_with_cp(&state.pool, id).await?; let client = client_for(&control_plane); let response = client .audit_log(crate::control_plane::AuditLogQuery { network_id: Some(network.network_id), node_id: query.node_id, limit: query.limit, }) .await .map_err(|_| ApiError::BadRequest("control plane request failed".to_string()))?; Ok(Json(response)) } #[derive(sqlx::FromRow)] struct NetworkRow { id: Uuid, control_plane_id: Uuid, control_plane_name: String, network_id: String, name: String, dns_domain: Option, overlay_v4: Option, overlay_v6: Option, requires_approval: bool, created_at: OffsetDateTime, updated_at: OffsetDateTime, } async fn fetch_network_summary(pool: &PgPool, id: Uuid) -> Result { sqlx::query_as::<_, NetworkRow>( r#" SELECT n.id, n.control_plane_id, n.network_id, n.name, n.dns_domain, n.overlay_v4, n.overlay_v6, n.requires_approval, n.created_at, n.updated_at, c.name AS control_plane_name FROM networks n JOIN control_planes c ON c.id = n.control_plane_id WHERE n.id = $1 "#, ) .bind(id) .fetch_optional(pool) .await .map_err(|_| ApiError::Internal)? .ok_or(ApiError::NotFound) } async fn fetch_network_with_cp( pool: &PgPool, network_id: Uuid, ) -> Result<(Network, ControlPlane), ApiError> { let network = sqlx::query_as::<_, Network>( r#" SELECT id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, requires_approval, created_at, updated_at FROM networks WHERE id = $1 "#, ) .bind(network_id) .fetch_optional(pool) .await .map_err(|_| ApiError::Internal)? .ok_or(ApiError::NotFound)?; let control_plane = fetch_control_plane(pool, network.control_plane_id).await?; Ok((network, control_plane)) } async fn fetch_control_plane(pool: &PgPool, id: Uuid) -> Result { sqlx::query_as::<_, ControlPlane>( r#" SELECT id, name, base_url, admin_token, region, created_at, updated_at FROM control_planes WHERE id = $1 "#, ) .bind(id) .fetch_optional(pool) .await .map_err(|_| ApiError::Internal)? .ok_or(ApiError::NotFound) } async fn insert_network( pool: &PgPool, control_plane: &ControlPlane, network: &NetworkInfo, dns_override: Option, ) -> Result { let now = OffsetDateTime::now_utc(); sqlx::query_as::<_, Network>( r#" INSERT INTO networks ( id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, requires_approval, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9) RETURNING id, control_plane_id, network_id, name, dns_domain, overlay_v4, overlay_v6, requires_approval, created_at, updated_at "#, ) .bind(Uuid::new_v4()) .bind(control_plane.id) .bind(&network.id) .bind(&network.name) .bind(dns_override.or_else(|| Some(network.dns_domain.clone()))) .bind(&network.overlay_v4) .bind(&network.overlay_v6) .bind(network.requires_approval) .bind(now) .fetch_one(pool) .await .map_err(|_| ApiError::Internal) } fn client_for(control_plane: &ControlPlane) -> crate::control_plane::ControlPlaneClient { crate::control_plane::ControlPlaneClient::new( control_plane.base_url.clone(), control_plane.admin_token.clone(), ) }