diff --git a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs --- a/keyserver/addons/rust-node-addon/src/identity_client/mod.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/mod.rs @@ -6,6 +6,7 @@ pub mod upload_one_time_keys; use grpc_clients::identity::authenticated::AuthLayer; +use grpc_clients::identity::shared::{ChainedInterceptor, CodeVersionLayer}; use grpc_clients::identity::protos::unauthenticated as client_proto; use grpc_clients::identity::protos::authenticated::identity_client_service_client::IdentityClientServiceClient as AuthClient; use client_proto::identity_client_service_client::IdentityClientServiceClient; @@ -23,6 +24,10 @@ use tracing::{self, info, instrument, warn, Level}; use tracing_subscriber::EnvFilter; +// This value should not be changed without also changing lib/facts/version.js +pub const CODE_VERSION: u64 = 33; +pub const DEVICE_TYPE: &str = "keyserver"; + lazy_static! { static ref IDENTITY_SERVICE_CONFIG: IdentityServiceConfig = { let filter = EnvFilter::builder() @@ -58,12 +63,15 @@ } } -async fn get_identity_client_service_channel( -) -> Result> { +async fn get_identity_client_service_channel() -> Result< + IdentityClientServiceClient>, +> { info!("Connecting to identity service"); grpc_clients::identity::get_unauthenticated_client( &IDENTITY_SERVICE_CONFIG.identity_socket_addr, + CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(|_| { @@ -78,7 +86,14 @@ user_id: String, device_id: String, access_token: String, -) -> Result>> { +) -> Result< + AuthClient< + InterceptedService< + Channel, + ChainedInterceptor, + >, + >, +> { info!("Connecting to identity service"); grpc_clients::identity::get_auth_client( @@ -86,6 +101,8 @@ user_id, device_id, access_token, + CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(|_| { diff --git a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs --- a/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs +++ b/keyserver/addons/rust-node-addon/src/identity_client/upload_one_time_keys.rs @@ -23,7 +23,10 @@ }; debug!("Sending one time keys to Identity service"); - let result = identity_client.upload_one_time_keys(upload_request).await; + identity_client + .upload_one_time_keys(upload_request) + .await + .map_err(handle_grpc_error)?; Ok(true) } diff --git a/lib/facts/version.js b/lib/facts/version.js --- a/lib/facts/version.js +++ b/lib/facts/version.js @@ -2,4 +2,7 @@ // This file defines the version of both web and keyserver. Today they are // deployed together, so their version are sourced from the same place. + +// This version number should not be changed without also changing CODE_VERSION +// in keyserver/addons/rust-node-addon/src/identity_client/mod.rs export const webAndKeyserverCodeVersion = 33; diff --git a/services/comm-services-lib/src/auth/service.rs b/services/comm-services-lib/src/auth/service.rs --- a/services/comm-services-lib/src/auth/service.rs +++ b/services/comm-services-lib/src/auth/service.rs @@ -13,6 +13,12 @@ const AWSCURRENT: &str = "AWSCURRENT"; const AWSPREVIOUS: &str = "AWSPREVIOUS"; +// Identity service gRPC clients require a code version and device type. +// We can supply some placeholder values for services for the time being, since +// this metadata is only relevant for devices. +const PLACEHOLDER_CODE_VERSION: u64 = 0; +const DEVICE_TYPE: &str = "service"; + #[derive( Debug, derive_more::Display, derive_more::Error, derive_more::From, )] @@ -81,6 +87,8 @@ user_id, device_id, access_token, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), ) .await .map_err(AuthServiceError::from) diff --git a/services/commtest/tests/grpc_client_test.rs b/services/commtest/tests/grpc_client_test.rs --- a/services/commtest/tests/grpc_client_test.rs +++ b/services/commtest/tests/grpc_client_test.rs @@ -4,12 +4,16 @@ async fn verify_access_token() { use grpc_clients::identity::unauthenticated::client::verify_user_access_token; let device_info = create_device().await; + let code_version = 100; + let device_type = "android"; let token_valid = verify_user_access_token( "http://127.0.0.1:50054", &device_info.user_id, &device_info.device_id, &device_info.access_token, + code_version, + device_type.to_string(), ) .await .expect("Failed to call identity's verify_user_access_token endpoint"); @@ -22,6 +26,8 @@ &device_info.user_id, &device_info.device_id, "garbage", + code_version, + device_type.to_string(), ) .await .expect("Failed to call identity's verify_user_access_token endpoint"); diff --git a/services/tunnelbroker/src/identity/mod.rs b/services/tunnelbroker/src/identity/mod.rs --- a/services/tunnelbroker/src/identity/mod.rs +++ b/services/tunnelbroker/src/identity/mod.rs @@ -7,14 +7,24 @@ use crate::config::CONFIG; use crate::error::Error; +// Identity service gRPC clients require a code version and device type. +// We can supply some placeholder values for services for the time being, since +// this metadata is only relevant for devices. +const PLACEHOLDER_CODE_VERSION: u64 = 0; +const DEVICE_TYPE: &str = "service"; + /// Returns true if access token is valid pub async fn verify_user_access_token( user_id: &str, device_id: &str, access_token: &str, ) -> Result { - let mut grpc_client = - get_unauthenticated_client(&CONFIG.identity_endpoint).await?; + let mut grpc_client = get_unauthenticated_client( + &CONFIG.identity_endpoint, + PLACEHOLDER_CODE_VERSION, + DEVICE_TYPE.to_string(), + ) + .await?; let message = VerifyUserAccessTokenRequest { user_id: user_id.to_string(), signing_public_key: device_id.to_string(), diff --git a/shared/grpc_clients/src/identity/authenticated.rs b/shared/grpc_clients/src/identity/authenticated.rs --- a/shared/grpc_clients/src/identity/authenticated.rs +++ b/shared/grpc_clients/src/identity/authenticated.rs @@ -7,7 +7,8 @@ Request, Status, }; -use crate::error::Error; +use crate::identity::shared::{ChainedInterceptor, ToMetadataValueAscii}; +use crate::{error::Error, identity::shared::CodeVersionLayer}; pub struct AuthLayer { user_id: String, @@ -15,10 +16,6 @@ access_token: String, } -trait ToMetadataValueAscii { - fn parse_to_ascii(&self) -> Result, Status>; -} - impl ToMetadataValueAscii for str { fn parse_to_ascii(&self) -> Result, Status> { self.parse().map_err(|e: InvalidMetadataValue| { @@ -45,16 +42,36 @@ user_id: String, device_id: String, access_token: String, -) -> Result>, Error> { + code_version: u64, + device_type: String, +) -> Result< + AuthClient< + InterceptedService< + Channel, + ChainedInterceptor, + >, + >, + Error, +> { use crate::get_grpc_service_channel; let channel = get_grpc_service_channel(url).await?; - let interceptor = AuthLayer { + let auth_interceptor = AuthLayer { user_id, device_id, access_token, }; - Ok(AuthClient::with_interceptor(channel, interceptor)) + let version_interceptor = CodeVersionLayer { + version: code_version, + device_type, + }; + + let chained = ChainedInterceptor { + first: auth_interceptor, + second: version_interceptor, + }; + + Ok(AuthClient::with_interceptor(channel, chained)) } diff --git a/shared/grpc_clients/src/identity/mod.rs b/shared/grpc_clients/src/identity/mod.rs --- a/shared/grpc_clients/src/identity/mod.rs +++ b/shared/grpc_clients/src/identity/mod.rs @@ -1,4 +1,5 @@ pub mod authenticated; +pub mod shared; pub mod unauthenticated; pub mod protos { diff --git a/shared/grpc_clients/src/identity/shared.rs b/shared/grpc_clients/src/identity/shared.rs new file mode 100644 --- /dev/null +++ b/shared/grpc_clients/src/identity/shared.rs @@ -0,0 +1,57 @@ +use tonic::{ + metadata::{errors::InvalidMetadataValue, Ascii, MetadataValue}, + service::Interceptor, + Request, Status, +}; + +pub struct CodeVersionLayer { + pub(crate) version: u64, + pub(crate) device_type: String, +} + +impl Interceptor for CodeVersionLayer { + fn call(&mut self, mut request: Request<()>) -> Result, Status> { + let metadata = request.metadata_mut(); + metadata.insert("code_version", self.version.parse_to_ascii()?); + metadata.insert("device_type", self.device_type.parse_to_ascii()?); + + Ok(request) + } +} + +pub trait ToMetadataValueAscii { + fn parse_to_ascii(&self) -> Result, Status>; +} + +impl ToMetadataValueAscii for u64 { + fn parse_to_ascii(&self) -> Result, Status> { + let ascii_string = self.to_string(); + + ascii_string.parse().map_err(|e: InvalidMetadataValue| { + Status::invalid_argument(format!( + "Non-Ascii character present in metadata value: {}", + e + )) + }) + } +} + +pub struct ChainedInterceptor +where + A: Interceptor + Send + Sync + 'static, + B: Interceptor + Send + Sync + 'static, +{ + pub(crate) first: A, + pub(crate) second: B, +} + +impl Interceptor for ChainedInterceptor +where + A: Interceptor + Send + Sync + 'static, + B: Interceptor + Send + Sync + 'static, +{ + fn call(&mut self, request: Request<()>) -> Result, Status> { + let request = self.first.call(request)?; + self.second.call(request) + } +} diff --git a/shared/grpc_clients/src/identity/unauthenticated/client.rs b/shared/grpc_clients/src/identity/unauthenticated/client.rs --- a/shared/grpc_clients/src/identity/unauthenticated/client.rs +++ b/shared/grpc_clients/src/identity/unauthenticated/client.rs @@ -12,8 +12,11 @@ user_id: &str, device_id: &str, access_token: &str, + code_version: u64, + device_type: String, ) -> Result { - let mut grpc_client = get_unauthenticated_client(identity_url).await?; + let mut grpc_client = + get_unauthenticated_client(identity_url, code_version, device_type).await?; let message = VerifyUserAccessTokenRequest { user_id: user_id.to_string(), @@ -23,5 +26,5 @@ let request = Request::new(message); let response = grpc_client.verify_user_access_token(request).await?; - return Ok(response.into_inner().token_valid); + Ok(response.into_inner().token_valid) } diff --git a/shared/grpc_clients/src/identity/unauthenticated/mod.rs b/shared/grpc_clients/src/identity/unauthenticated/mod.rs --- a/shared/grpc_clients/src/identity/unauthenticated/mod.rs +++ b/shared/grpc_clients/src/identity/unauthenticated/mod.rs @@ -1,13 +1,29 @@ pub mod client; +use tonic::codegen::InterceptedService; use tonic::transport::Channel; -use super::protos::client::identity_client_service_client::IdentityClientServiceClient; +use super::{ + protos::client::identity_client_service_client::IdentityClientServiceClient, + shared::CodeVersionLayer, +}; use crate::error::Error; pub async fn get_unauthenticated_client( url: &str, -) -> Result, Error> { + code_version: u64, + device_type: String, +) -> Result< + IdentityClientServiceClient>, + Error, +> { let channel = crate::get_grpc_service_channel(url).await?; - Ok(IdentityClientServiceClient::new(channel)) + let version_interceptor = CodeVersionLayer { + version: code_version, + device_type, + }; + Ok(IdentityClientServiceClient::with_interceptor( + channel, + version_interceptor, + )) } diff --git a/shared/grpc_clients/src/lib.rs b/shared/grpc_clients/src/lib.rs --- a/shared/grpc_clients/src/lib.rs +++ b/shared/grpc_clients/src/lib.rs @@ -10,7 +10,7 @@ use tonic::transport::{Certificate, Channel, ClientTlsConfig}; use tracing::info; -const CERT_PATHS: &'static [&'static str] = &[ +const CERT_PATHS: &[&str] = &[ // MacOS and newer Ubuntu "/etc/ssl/cert.pem", // Common CA cert paths