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 @@ -144,6 +144,22 @@ last_primary_signature: update_info.last_signature.clone(), } } + + pub fn has_device(&self, device_id: &String) -> bool { + self.device_ids.contains(device_id) + } + + pub fn has_primary_device(&self, device_id: &String) -> bool { + self + .device_ids + .first() + .filter(|it| *it == device_id) + .is_some() + } + + pub fn has_secondary_device(&self, device_id: &String) -> bool { + self.has_device(device_id) && !self.has_primary_device(device_id) + } } // helper structs for converting to/from attribute values for sort key (a.k.a itemID) @@ -816,6 +832,37 @@ Ok(()) } + /// Removes device data from devices table. If the device doesn't exist, + /// it is a no-op. This does not update the device list; the device ID + /// should be removed from the device list separately. + #[tracing::instrument(skip_all)] + pub async fn remove_device_data( + &self, + user_id: impl Into, + device_id: impl Into, + ) -> Result<(), Error> { + let user_id = user_id.into(); + let device_id = device_id.into(); + + self + .client + .delete_item() + .table_name(devices_table::NAME) + .key(ATTR_USER_ID, AttributeValue::S(user_id)) + .key(ATTR_ITEM_ID, DeviceIDAttribute(device_id).into()) + .send() + .await + .map_err(|e| { + error!( + errorType = error_types::DEVICE_LIST_DB_LOG, + "Failed to delete device data: {:?}", e + ); + Error::AwsSdk(e.into()) + })?; + + Ok(()) + } + /// Registers primary device for user, stores its signed device list pub async fn register_primary_device( &self, 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 @@ -337,7 +337,40 @@ &self, request: tonic::Request, ) -> Result, tonic::Status> { - Err(tonic::Status::unimplemented("")) + let (user_id, device_id) = get_user_and_device_id(&request)?; + + debug!( + "Secondary device logout request for user_id={}, device_id={}", + user_id, device_id + ); + self + .verify_device_on_device_list( + &user_id, + &device_id, + DeviceListItemKind::Secondary, + ) + .await?; + + self + .db_client + .remove_device_data(&user_id, &device_id) + .await + .map_err(handle_db_error)?; + + self + .db_client + .delete_otks_table_rows_for_user_device(&user_id, &device_id) + .await + .map_err(handle_db_error)?; + + self + .db_client + .delete_access_token_data(user_id, device_id) + .await + .map_err(handle_db_error)?; + + let response = Empty {}; + Ok(Response::new(response)) } #[tracing::instrument(skip_all)] @@ -645,6 +678,60 @@ } } +enum DeviceListItemKind { + Any, + Primary, + Secondary, +} + +impl AuthenticatedService { + async fn verify_device_on_device_list( + &self, + user_id: &String, + device_id: &String, + device_kind: DeviceListItemKind, + ) -> Result<(), tonic::Status> { + let device_list = self + .db_client + .get_current_device_list(user_id) + .await + .map_err(|err| { + error!( + user_id, + errorType = error_types::GRPC_SERVICES_LOG, + "Failed fetching device list: {err}" + ); + handle_db_error(err) + })?; + + let Some(device_list) = device_list else { + error!( + user_id, + errorType = error_types::GRPC_SERVICES_LOG, + "User has no device list!" + ); + return Err(Status::failed_precondition("no device list")); + }; + + use DeviceListItemKind as DeviceKind; + let device_on_list = match device_kind { + DeviceKind::Any => device_list.has_device(device_id), + DeviceKind::Primary => device_list.has_primary_device(device_id), + DeviceKind::Secondary => device_list.has_secondary_device(device_id), + }; + + if !device_on_list { + debug!( + "Device {} not on device list for user {}", + device_id, user_id + ); + return Err(Status::permission_denied("device not on device list")); + } + + Ok(()) + } +} + #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct DeletePasswordUserInfo { pub opaque_server_login: comm_opaque2::server::Login,