diff --git a/services/identity/src/database/device_list.rs b/services/identity/src/database/device_list.rs --- a/services/identity/src/database/device_list.rs +++ b/services/identity/src/database/device_list.rs @@ -72,12 +72,17 @@ pub prekey_signature: String, } -/// A struct representing device list update request -/// payload; issued by the primary device -#[derive(derive_more::Constructor)] +/// A struct representing device list update payload +/// issued by the primary device. +/// For the JSON payload, see [`crate::device_list::SignedDeviceList`] pub struct DeviceListUpdate { pub devices: Vec, pub timestamp: DateTime, + /// Primary device signature. This is `None` for Identity-generated lists. + pub current_primary_signature: Option, + /// Last primary device signature, in case the primary device has changed + /// since last device list update. + pub last_primary_signature: Option, } impl DeviceRow { @@ -925,6 +930,7 @@ let DeviceListUpdate { devices: new_list, timestamp, + .. } = update; self .transact_update_devicelist(user_id, |current_list, _| { diff --git a/services/identity/src/device_list.rs b/services/identity/src/device_list.rs --- a/services/identity/src/device_list.rs +++ b/services/identity/src/device_list.rs @@ -10,22 +10,16 @@ grpc_services::protos::auth::UpdateDeviceListRequest, }; -// raw device list that can be serialized to JSON (and then signed in the future) +// serde helper for serializing/deserializing +// device list JSON payload #[derive(serde::Serialize, serde::Deserialize)] -pub struct RawDeviceList { +struct RawDeviceList { devices: Vec, timestamp: i64, } -impl From for RawDeviceList { - fn from(row: DeviceListRow) -> Self { - Self { - devices: row.device_ids, - timestamp: row.timestamp.timestamp_millis(), - } - } -} - +/// Signed device list payload that is serializable to JSON. +/// For the DDB payload, see [`DeviceListUpdate`] #[derive(serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct SignedDeviceList { @@ -45,23 +39,6 @@ } impl SignedDeviceList { - /// Serialize (and sign in the future) a [`RawDeviceList`] - pub fn try_from_raw(raw: RawDeviceList) -> Result { - let stringified_list = serde_json::to_string(&raw).map_err(|err| { - error!( - errorType = error_types::GRPC_SERVICES_LOG, - "Failed to serialize raw device list: {}", err - ); - tonic::Status::failed_precondition("unexpected error") - })?; - - Ok(Self { - raw_device_list: stringified_list, - cur_primary_signature: None, - last_primary_signature: None, - }) - } - fn as_raw(&self) -> Result { // The device list payload is sent as an escaped JSON payload. // Escaped double quotes need to be trimmed before attempting to deserialize @@ -84,6 +61,30 @@ } } +impl TryFrom for SignedDeviceList { + type Error = tonic::Status; + + fn try_from(row: DeviceListRow) -> Result { + let raw_list = RawDeviceList { + devices: row.device_ids, + timestamp: row.timestamp.timestamp_millis(), + }; + let stringified_list = serde_json::to_string(&raw_list).map_err(|err| { + error!( + errorType = error_types::GRPC_SERVICES_LOG, + "Failed to serialize raw device list: {}", err + ); + tonic::Status::failed_precondition("unexpected error") + })?; + + Ok(Self { + raw_device_list: stringified_list, + cur_primary_signature: row.current_primary_signature, + last_primary_signature: row.last_primary_signature, + }) + } +} + impl TryFrom for SignedDeviceList { type Error = tonic::Status; fn try_from(request: UpdateDeviceListRequest) -> Result { @@ -109,7 +110,12 @@ ); tonic::Status::invalid_argument("invalid timestamp") })?; - Ok(DeviceListUpdate::new(devices, timestamp)) + Ok(DeviceListUpdate { + devices, + timestamp, + current_primary_signature: signed_list.cur_primary_signature, + last_primary_signature: signed_list.last_primary_signature, + }) } } @@ -349,14 +355,14 @@ #[test] fn serialize_device_list_updates() { let raw_updates = vec![ - RawDeviceList { + create_db_row(RawDeviceList { devices: vec!["device1".into()], timestamp: 111111111, - }, - RawDeviceList { + }), + create_db_row(RawDeviceList { devices: vec!["device1".into(), "device2".into()], timestamp: 222222222, - }, + }), ]; let expected_raw_list1 = r#"{"devices":["device1"],"timestamp":111111111}"#; @@ -365,7 +371,7 @@ let signed_updates = raw_updates .into_iter() - .map(SignedDeviceList::try_from_raw) + .map(SignedDeviceList::try_from) .collect::, _>>() .expect("signing device list updates failed"); @@ -439,4 +445,16 @@ "No provided timestamp should pass" ); } + + /// helper for mocking DB rows from raw device list payloads + fn create_db_row(raw_list: RawDeviceList) -> DeviceListRow { + DeviceListRow { + user_id: "".to_string(), + device_ids: raw_list.devices, + timestamp: DateTime::::from_utc_timestamp_millis(raw_list.timestamp) + .unwrap(), + current_primary_signature: None, + last_primary_signature: None, + } + } } diff --git a/services/identity/src/grpc_services/authenticated.rs b/services/identity/src/grpc_services/authenticated.rs --- a/services/identity/src/grpc_services/authenticated.rs +++ b/services/identity/src/grpc_services/authenticated.rs @@ -2,7 +2,7 @@ use crate::config::CONFIG; use crate::database::DeviceListUpdate; -use crate::device_list::{RawDeviceList, SignedDeviceList}; +use crate::device_list::SignedDeviceList; use crate::{ client_service::{handle_db_error, UpdateState, WorkflowInProgress}, constants::{error_types, request_metadata}, @@ -461,8 +461,7 @@ let device_list_updates: Vec = db_result .into_iter() - .map(RawDeviceList::from) - .map(SignedDeviceList::try_from_raw) + .map(SignedDeviceList::try_from) .collect::, _>>()?; let stringified_updates = device_list_updates @@ -496,8 +495,7 @@ while let Some(task_result) = fetch_tasks.join_next().await { match task_result { Ok((user_id, Ok(Some(device_list_row)))) => { - let raw_list = RawDeviceList::from(device_list_row); - let signed_list = SignedDeviceList::try_from_raw(raw_list)?; + let signed_list = SignedDeviceList::try_from(device_list_row)?; let serialized_list = signed_list.as_json_string()?; device_lists.insert(user_id, serialized_list); }