From 01ffe085d708ee54338318f0fa3e147d4a12b9ef Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Tue, 2 Jul 2024 22:55:51 +0200 Subject: [PATCH] Complete enroll route --- central_backend/src/devices/device.rs | 28 +++++---- central_backend/src/devices/devices_list.rs | 59 ++++++++++++++++++- central_backend/src/energy/energy_actor.rs | 16 ++++- .../src/server/devices_api/mgmt_controller.rs | 17 ++++-- central_backend/src/utils/time_utils.rs | 7 +++ python_device/src/api.py | 8 ++- python_device/src/args.py | 1 + python_device/src/main.py | 8 ++- 8 files changed, 121 insertions(+), 23 deletions(-) diff --git a/central_backend/src/devices/device.rs b/central_backend/src/devices/device.rs index d4b2f3f..400ca23 100644 --- a/central_backend/src/devices/device.rs +++ b/central_backend/src/devices/device.rs @@ -25,28 +25,33 @@ pub struct DeviceId(pub String); #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct Device { /// The device ID - id: DeviceId, + pub id: DeviceId, /// Information about the device - device: DeviceInfo, + pub info: DeviceInfo, + /// Time at which device was initially enrolled + pub time_create: u64, + /// Time at which device was last updated + pub time_update: u64, /// Name given to the device on the Web UI - name: String, + pub name: String, /// Description given to the device on the Web UI - description: String, + pub description: String, + /// Specify whether the device has been validated or not. Validated devices are given a + /// certificate + pub validated: bool, /// Specify whether the device is enabled or not - enabled: bool, - /// Specify whether the device has been validated or not - validated: bool, + pub enabled: bool, /// Information about the relays handled by the device - relays: Vec, + pub relays: Vec, } /// Structure that contains information about the minimal expected execution /// time of a device #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DailyMinRuntime { - min_runtime: usize, - reset_time: usize, - catch_up_hours: Vec, + pub min_runtime: usize, + pub reset_time: usize, + pub catch_up_hours: Vec, } #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -63,4 +68,5 @@ pub struct DeviceRelay { minimal_downtime: usize, daily_runtime: Option, depends_on: Vec, + conflicts_with: Vec, } diff --git a/central_backend/src/devices/devices_list.rs b/central_backend/src/devices/devices_list.rs index 22ab413..39f5c11 100644 --- a/central_backend/src/devices/devices_list.rs +++ b/central_backend/src/devices/devices_list.rs @@ -1,7 +1,17 @@ use crate::app_config::AppConfig; -use crate::devices::device::{Device, DeviceId}; +use crate::devices::device::{Device, DeviceId, DeviceInfo}; +use crate::utils::time_utils::time_secs; +use openssl::x509::X509Req; use std::collections::HashMap; +#[derive(thiserror::Error, Debug)] +pub enum DevicesListError { + #[error("Enrollment failed: a device with the same ID was already registered!")] + EnrollFailedDeviceAlreadyExists, + #[error("Persist device config failed: the configuration of the device was not found!")] + PersistFailedDeviceNotFound, +} + pub struct DevicesList(HashMap); impl DevicesList { @@ -33,4 +43,51 @@ impl DevicesList { pub fn exists(&self, id: &DeviceId) -> bool { self.0.contains_key(id) } + + /// Enroll a new device + pub fn enroll( + &mut self, + id: &DeviceId, + info: &DeviceInfo, + csr: &X509Req, + ) -> anyhow::Result<()> { + if self.exists(id) { + return Err(DevicesListError::EnrollFailedDeviceAlreadyExists.into()); + } + + let device = Device { + id: id.clone(), + info: info.clone(), + time_create: time_secs(), + time_update: time_secs(), + name: id.0.to_string(), + description: "".to_string(), + validated: false, + enabled: false, + relays: vec![], + }; + + // First, write CSR + std::fs::write(AppConfig::get().device_csr_path(id), csr.to_pem()?)?; + + self.0.insert(id.clone(), device); + self.persist_dev_config(id)?; + + Ok(()) + } + + /// Persist a device configuration on the filesystem + fn persist_dev_config(&self, id: &DeviceId) -> anyhow::Result<()> { + let dev = self + .0 + .get(id) + .ok_or_else(|| DevicesListError::PersistFailedDeviceNotFound)?; + + std::fs::write( + AppConfig::get().device_config_path(id), + serde_json::to_string_pretty(dev)?, + )?; + + Ok(()) + } } diff --git a/central_backend/src/energy/energy_actor.rs b/central_backend/src/energy/energy_actor.rs index d60285a..50b1c38 100644 --- a/central_backend/src/energy/energy_actor.rs +++ b/central_backend/src/energy/energy_actor.rs @@ -1,9 +1,10 @@ use crate::constants; -use crate::devices::device::DeviceId; +use crate::devices::device::{DeviceId, DeviceInfo}; use crate::devices::devices_list::DevicesList; use crate::energy::consumption; use crate::energy::consumption::EnergyConsumption; use actix::prelude::*; +use openssl::x509::X509Req; pub struct EnergyActor { curr_consumption: EnergyConsumption, @@ -79,3 +80,16 @@ impl Handler for EnergyActor { self.devices.exists(&msg.0) } } + +/// Enroll device +#[derive(Message)] +#[rtype(result = "anyhow::Result<()>")] +pub struct EnrollDevice(pub DeviceId, pub DeviceInfo, pub X509Req); + +impl Handler for EnergyActor { + type Result = anyhow::Result<()>; + + fn handle(&mut self, msg: EnrollDevice, _ctx: &mut Context) -> Self::Result { + self.devices.enroll(&msg.0, &msg.1, &msg.2) + } +} diff --git a/central_backend/src/server/devices_api/mgmt_controller.rs b/central_backend/src/server/devices_api/mgmt_controller.rs index 089242e..648cbfe 100644 --- a/central_backend/src/server/devices_api/mgmt_controller.rs +++ b/central_backend/src/server/devices_api/mgmt_controller.rs @@ -1,4 +1,3 @@ -use crate::crypto::pki; use crate::devices::device::{DeviceId, DeviceInfo}; use crate::energy::energy_actor; use crate::server::custom_error::HttpResult; @@ -51,18 +50,24 @@ pub async fn enroll(req: web::Json, actor: WebEnergyActor) -> Htt } let device_id = DeviceId(cn); - log::info!("Received enrollment request for device with ID {device_id:?}",); + log::info!( + "Received enrollment request for device with ID {device_id:?} - {:#?}", + req.info + ); if actor .send(energy_actor::CheckDeviceExists(device_id.clone())) .await? { log::error!("Device could not be enrolled: it already exists!"); - return Ok(HttpResponse::Conflict().json("Device ")); + return Ok( + HttpResponse::Conflict().json("A device with the same ID has already been enrolled!") + ); } - log::info!("Issue certificate for device..."); - let cert = pki::gen_certificate_for_device(&csr)?; + actor + .send(energy_actor::EnrollDevice(device_id, req.0.info, csr)) + .await??; - Ok(HttpResponse::Ok().body(cert)) + Ok(HttpResponse::Accepted().json("Device successfully enrolled")) } diff --git a/central_backend/src/utils/time_utils.rs b/central_backend/src/utils/time_utils.rs index 4c73ed9..4f1a368 100644 --- a/central_backend/src/utils/time_utils.rs +++ b/central_backend/src/utils/time_utils.rs @@ -1,7 +1,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; /// Get the current time since epoch +pub fn time_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} +/// Get the current time since epoch pub fn time_millis() -> u128 { SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/python_device/src/api.py b/python_device/src/api.py index 7b50980..34370b9 100644 --- a/python_device/src/api.py +++ b/python_device/src/api.py @@ -28,7 +28,12 @@ def device_info(): } -def enroll_device(csr: str) -> str: +def enroll_device(csr: str): + """ + Enroll device, ie. submit CSR to API. + + Certificate cannot be retrieved before device is validated. + """ res = requests.post( f"{args.secure_origin}/devices_api/mgmt/enroll", json={"csr": csr, "info": device_info()}, @@ -37,4 +42,3 @@ def enroll_device(csr: str) -> str: if res.status_code < 200 or res.status_code > 299: print(res.text) raise Exception(f"Enrollment failed with status {res.status_code}") - return res.text diff --git a/python_device/src/args.py b/python_device/src/args.py index c5ae135..2f6b277 100644 --- a/python_device/src/args.py +++ b/python_device/src/args.py @@ -21,5 +21,6 @@ args.secure_origin_path = os.path.join(args.storage, "SECURE_ORIGIN") args.root_ca_path = os.path.join(args.storage, "root_ca.crt") args.dev_priv_key_path = os.path.join(args.storage, "dev.key") args.dev_csr_path = os.path.join(args.storage, "dev.csr") +args.dev_enroll_marker = os.path.join(args.storage, "ENROLL_SUBMITTED") args.dev_crt_path = os.path.join(args.storage, "dev.crt") args.relay_gpios_list = list(map(lambda x: int(x), args.relay_gpios.split(","))) diff --git a/python_device/src/main.py b/python_device/src/main.py index 67b92b0..9dd1c9a 100644 --- a/python_device/src/main.py +++ b/python_device/src/main.py @@ -44,10 +44,14 @@ if not os.path.isfile(args.dev_csr_path): f.write(csr) print("Check device enrollment...") -if not os.path.isfile(args.dev_crt_path): +if not os.path.isfile(args.dev_enroll_marker): with open(args.dev_csr_path, "r") as f: csr = "".join(f.read()) print("Enrolling device...") crt = api.enroll_device(csr) - print("res" + crt) + + with open(args.dev_enroll_marker, "w") as f: + f.write("submitted") + +# TODO : "intelligent" enrollment management (re-enroll if cancelled) \ No newline at end of file