diff --git a/services/identity/src/database.rs b/services/identity/src/database.rs --- a/services/identity/src/database.rs +++ b/services/identity/src/database.rs @@ -58,7 +58,9 @@ mod one_time_keys; mod token; mod workflows; -pub use device_list::{DeviceListRow, DeviceListUpdate, DeviceRow}; +pub use device_list::{ + DeviceListRow, DeviceListUpdate, DeviceRow, PlatformDetails, +}; use self::device_list::Prekey; 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 @@ -901,6 +901,40 @@ Ok(()) } + #[tracing::instrument(skip_all)] + pub async fn update_device_platform_details( + &self, + user_id: impl Into, + device_id: impl Into, + platform_details: PlatformDetails, + ) -> Result<(), Error> { + self + .client + .update_item() + .table_name(devices_table::NAME) + .key(ATTR_USER_ID, AttributeValue::S(user_id.into())) + .key(ATTR_ITEM_ID, DeviceIDAttribute(device_id.into()).into()) + .condition_expression( + "attribute_exists(#user_id) AND attribute_exists(#item_id)", + ) + .update_expression("SET #platform_details = :platform_details") + .expression_attribute_names("#user_id", ATTR_USER_ID) + .expression_attribute_names("#item_id", ATTR_ITEM_ID) + .expression_attribute_names("#platform_details", ATTR_PLATFORM_DETAILS) + .expression_attribute_values(":platform_details", platform_details.into()) + .send() + .await + .map_err(|e| { + error!( + errorType = error_types::DEVICE_LIST_DB_LOG, + "Failed to update device platform details: {:?}", e + ); + Error::AwsSdk(e.into()) + })?; + + Ok(()) + } + #[tracing::instrument(skip_all)] pub async fn get_current_device_list( &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 @@ -1,13 +1,13 @@ use std::collections::HashMap; use crate::config::CONFIG; -use crate::database::DeviceListUpdate; +use crate::database::{DeviceListUpdate, PlatformDetails}; use crate::device_list::SignedDeviceList; use crate::{ client_service::{handle_db_error, UpdateState, WorkflowInProgress}, constants::{error_types, request_metadata}, database::DatabaseClient, - grpc_services::shared::get_value, + grpc_services::shared::{get_platform_metadata, get_value}, }; use chrono::DateTime; use comm_opaque2::grpc::protocol_error_to_grpc_status; @@ -699,6 +699,25 @@ }; return Ok(Response::new(response)); } + + #[tracing::instrument(skip_all)] + async fn sync_platform_details( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let (user_id, device_id) = get_user_and_device_id(&request)?; + let platform_metadata = get_platform_metadata(&request)?; + let platform_details = PlatformDetails::new(platform_metadata, None) + .map_err(|_| Status::invalid_argument("Invalid platform metadata"))?; + + self + .db_client + .update_device_platform_details(user_id, device_id, platform_details) + .await + .map_err(handle_db_error)?; + + Ok(Response::new(Empty {})) + } } #[allow(dead_code)] diff --git a/shared/protos/identity_auth.proto b/shared/protos/identity_auth.proto --- a/shared/protos/identity_auth.proto +++ b/shared/protos/identity_auth.proto @@ -84,6 +84,10 @@ /* Miscellaneous actions */ rpc FindUserIdentities(UserIdentitiesRequest) returns (UserIdentitiesResponse) {} + + // Updates current device PlatformDetails stored on Identity Service. + // It doesn't require any input - all values are passed via request metadata. + rpc SyncPlatformDetails(identity.unauth.Empty) returns (identity.unauth.Empty) {} } // Helper types diff --git a/web/protobufs/identity-auth-client.cjs b/web/protobufs/identity-auth-client.cjs --- a/web/protobufs/identity-auth-client.cjs +++ b/web/protobufs/identity-auth-client.cjs @@ -1176,5 +1176,66 @@ }; +/** + * @const + * @type {!grpc.web.MethodDescriptor< + * !proto.identity.unauth.Empty, + * !proto.identity.unauth.Empty>} + */ +const methodDescriptor_IdentityClientService_SyncPlatformDetails = new grpc.web.MethodDescriptor( + '/identity.auth.IdentityClientService/SyncPlatformDetails', + grpc.web.MethodType.UNARY, + identity_unauth_pb.Empty, + identity_unauth_pb.Empty, + /** + * @param {!proto.identity.unauth.Empty} request + * @return {!Uint8Array} + */ + function(request) { + return request.serializeBinary(); + }, + identity_unauth_pb.Empty.deserializeBinary +); + + +/** + * @param {!proto.identity.unauth.Empty} request The + * request proto + * @param {?Object} metadata User defined + * call metadata + * @param {function(?grpc.web.RpcError, ?proto.identity.unauth.Empty)} + * callback The callback function(error, response) + * @return {!grpc.web.ClientReadableStream|undefined} + * The XHR Node Readable Stream + */ +proto.identity.auth.IdentityClientServiceClient.prototype.syncPlatformDetails = + function(request, metadata, callback) { + return this.client_.rpcCall(this.hostname_ + + '/identity.auth.IdentityClientService/SyncPlatformDetails', + request, + metadata || {}, + methodDescriptor_IdentityClientService_SyncPlatformDetails, + callback); +}; + + +/** + * @param {!proto.identity.unauth.Empty} request The + * request proto + * @param {?Object=} metadata User defined + * call metadata + * @return {!Promise} + * Promise that resolves to the response + */ +proto.identity.auth.IdentityClientServicePromiseClient.prototype.syncPlatformDetails = + function(request, metadata) { + return this.client_.unaryCall(this.hostname_ + + '/identity.auth.IdentityClientService/SyncPlatformDetails', + request, + metadata || {}, + methodDescriptor_IdentityClientService_SyncPlatformDetails); +}; + + module.exports = proto.identity.auth; diff --git a/web/protobufs/identity-auth-client.cjs.flow b/web/protobufs/identity-auth-client.cjs.flow --- a/web/protobufs/identity-auth-client.cjs.flow +++ b/web/protobufs/identity-auth-client.cjs.flow @@ -136,6 +136,13 @@ callback: (err: grpcWeb.RpcError, response: identityAuthStructs.UserIdentitiesResponse) => void ): grpcWeb.ClientReadableStream; + + syncPlatformDetails( + request: identityStructs.Empty, + metadata: grpcWeb.Metadata | void, + callback: (err: grpcWeb.RpcError, + response: identityStructs.Empty) => void + ): grpcWeb.ClientReadableStream; } declare export class IdentityClientServicePromiseClient { @@ -232,5 +239,11 @@ request: identityAuthStructs.UserIdentitiesRequest, metadata?: grpcWeb.Metadata ): Promise; + + syncPlatformDetails( + request: identityStructs.Empty, + metadata?: grpcWeb.Metadata + ): Promise; + }