Complete enroll route

This commit is contained in:
Pierre HUBERT 2024-07-02 22:55:51 +02:00
parent e64a444bd0
commit 01ffe085d7
8 changed files with 121 additions and 23 deletions

View File

@ -25,28 +25,33 @@ pub struct DeviceId(pub String);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Device { pub struct Device {
/// The device ID /// The device ID
id: DeviceId, pub id: DeviceId,
/// Information about the device /// 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 given to the device on the Web UI
name: String, pub name: String,
/// Description given to the device on the Web UI /// 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 /// Specify whether the device is enabled or not
enabled: bool, pub enabled: bool,
/// Specify whether the device has been validated or not
validated: bool,
/// Information about the relays handled by the device /// Information about the relays handled by the device
relays: Vec<DeviceRelay>, pub relays: Vec<DeviceRelay>,
} }
/// Structure that contains information about the minimal expected execution /// Structure that contains information about the minimal expected execution
/// time of a device /// time of a device
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct DailyMinRuntime { pub struct DailyMinRuntime {
min_runtime: usize, pub min_runtime: usize,
reset_time: usize, pub reset_time: usize,
catch_up_hours: Vec<usize>, pub catch_up_hours: Vec<usize>,
} }
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
@ -63,4 +68,5 @@ pub struct DeviceRelay {
minimal_downtime: usize, minimal_downtime: usize,
daily_runtime: Option<DailyMinRuntime>, daily_runtime: Option<DailyMinRuntime>,
depends_on: Vec<DeviceRelay>, depends_on: Vec<DeviceRelay>,
conflicts_with: Vec<DeviceRelay>,
} }

View File

@ -1,7 +1,17 @@
use crate::app_config::AppConfig; 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; 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<DeviceId, Device>); pub struct DevicesList(HashMap<DeviceId, Device>);
impl DevicesList { impl DevicesList {
@ -33,4 +43,51 @@ impl DevicesList {
pub fn exists(&self, id: &DeviceId) -> bool { pub fn exists(&self, id: &DeviceId) -> bool {
self.0.contains_key(id) 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(())
}
} }

View File

@ -1,9 +1,10 @@
use crate::constants; use crate::constants;
use crate::devices::device::DeviceId; use crate::devices::device::{DeviceId, DeviceInfo};
use crate::devices::devices_list::DevicesList; use crate::devices::devices_list::DevicesList;
use crate::energy::consumption; use crate::energy::consumption;
use crate::energy::consumption::EnergyConsumption; use crate::energy::consumption::EnergyConsumption;
use actix::prelude::*; use actix::prelude::*;
use openssl::x509::X509Req;
pub struct EnergyActor { pub struct EnergyActor {
curr_consumption: EnergyConsumption, curr_consumption: EnergyConsumption,
@ -79,3 +80,16 @@ impl Handler<CheckDeviceExists> for EnergyActor {
self.devices.exists(&msg.0) self.devices.exists(&msg.0)
} }
} }
/// Enroll device
#[derive(Message)]
#[rtype(result = "anyhow::Result<()>")]
pub struct EnrollDevice(pub DeviceId, pub DeviceInfo, pub X509Req);
impl Handler<EnrollDevice> for EnergyActor {
type Result = anyhow::Result<()>;
fn handle(&mut self, msg: EnrollDevice, _ctx: &mut Context<Self>) -> Self::Result {
self.devices.enroll(&msg.0, &msg.1, &msg.2)
}
}

View File

@ -1,4 +1,3 @@
use crate::crypto::pki;
use crate::devices::device::{DeviceId, DeviceInfo}; use crate::devices::device::{DeviceId, DeviceInfo};
use crate::energy::energy_actor; use crate::energy::energy_actor;
use crate::server::custom_error::HttpResult; use crate::server::custom_error::HttpResult;
@ -51,18 +50,24 @@ pub async fn enroll(req: web::Json<EnrollRequest>, actor: WebEnergyActor) -> Htt
} }
let device_id = DeviceId(cn); 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 if actor
.send(energy_actor::CheckDeviceExists(device_id.clone())) .send(energy_actor::CheckDeviceExists(device_id.clone()))
.await? .await?
{ {
log::error!("Device could not be enrolled: it already exists!"); 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..."); actor
let cert = pki::gen_certificate_for_device(&csr)?; .send(energy_actor::EnrollDevice(device_id, req.0.info, csr))
.await??;
Ok(HttpResponse::Ok().body(cert)) Ok(HttpResponse::Accepted().json("Device successfully enrolled"))
} }

View File

@ -1,7 +1,14 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
/// Get the current time since 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 { pub fn time_millis() -> u128 {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)

View File

@ -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( res = requests.post(
f"{args.secure_origin}/devices_api/mgmt/enroll", f"{args.secure_origin}/devices_api/mgmt/enroll",
json={"csr": csr, "info": device_info()}, 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: if res.status_code < 200 or res.status_code > 299:
print(res.text) print(res.text)
raise Exception(f"Enrollment failed with status {res.status_code}") raise Exception(f"Enrollment failed with status {res.status_code}")
return res.text

View File

@ -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.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_priv_key_path = os.path.join(args.storage, "dev.key")
args.dev_csr_path = os.path.join(args.storage, "dev.csr") 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.dev_crt_path = os.path.join(args.storage, "dev.crt")
args.relay_gpios_list = list(map(lambda x: int(x), args.relay_gpios.split(","))) args.relay_gpios_list = list(map(lambda x: int(x), args.relay_gpios.split(",")))

View File

@ -44,10 +44,14 @@ if not os.path.isfile(args.dev_csr_path):
f.write(csr) f.write(csr)
print("Check device enrollment...") 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: with open(args.dev_csr_path, "r") as f:
csr = "".join(f.read()) csr = "".join(f.read())
print("Enrolling device...") print("Enrolling device...")
crt = api.enroll_device(csr) 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)