From bb9c49992894de0ff1f67f89d33fe1040cdb6e19 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Wed, 23 Jul 2025 12:09:45 -0700 Subject: [PATCH 1/4] initial commit --- .../geneva-uploader/Cargo.toml | 3 + .../geneva-uploader/build.rs | 144 ++++++++++ .../examples/msi_auth_example.rs | 186 ++++++++++++ .../geneva-uploader/src/client.rs | 130 ++++++++- .../src/config_service/client.rs | 194 ++++++++++++- .../geneva-uploader/src/config_service/mod.rs | 7 +- .../geneva-uploader/src/lib.rs | 3 +- .../geneva-uploader/src/msi/error.rs | 144 ++++++++++ .../geneva-uploader/src/msi/ffi.rs | 125 ++++++++ .../geneva-uploader/src/msi/mod.rs | 33 +++ .../geneva-uploader/src/msi/native/README.md | 16 ++ .../geneva-uploader/src/msi/native/bridge.cpp | 133 +++++++++ .../geneva-uploader/src/msi/native/wrapper.h | 97 +++++++ .../geneva-uploader/src/msi/token_source.rs | 116 ++++++++ .../geneva-uploader/src/msi/types.rs | 272 ++++++++++++++++++ 15 files changed, 1590 insertions(+), 13 deletions(-) create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/build.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/examples/msi_auth_example.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/error.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/README.md create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/wrapper.h create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs create mode 100644 opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs diff --git a/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml b/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml index b1b1fd5bf..44412f2c4 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml +++ b/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml @@ -25,6 +25,7 @@ futures = "0.3" [features] self_signed_certs = [] # Empty by default for security mock_auth = [] # Disabled by default. Not to be enabled in the prod release. +msi_auth = [] # Enable MSI authentication support (requires MSINATIVE_LIB_PATH at build time) default = ["self_signed_certs"] # TODO - remove this feature before release [dev-dependencies] @@ -39,5 +40,7 @@ lz4_flex = { version = "0.11" } criterion = {version = "0.6"} rand = {version = "0.9"} +# No build dependencies needed for the simplified MSI integration + [lints] workspace = true diff --git a/opentelemetry-exporter-geneva/geneva-uploader/build.rs b/opentelemetry-exporter-geneva/geneva-uploader/build.rs new file mode 100644 index 000000000..8f096e51a --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/build.rs @@ -0,0 +1,144 @@ +//! Build script for Geneva Uploader with optional MSI authentication support + +fn main() { + // Always output cargo rerun instructions for environment changes + println!("cargo:rerun-if-env-changed=MSINATIVE_LIB_PATH"); + println!("cargo:rerun-if-changed=build.rs"); + + // Tell cargo about our custom cfg + println!("cargo:rustc-check-cfg=cfg(msi_native_available)"); + + // Only run MSI build logic if the msi_auth feature is enabled + #[cfg(feature = "msi_auth")] + { + let msi_available = check_msi_library(); + if msi_available { + println!("cargo:rustc-cfg=msi_native_available"); + eprintln!("INFO: MSI native authentication support enabled"); + } else { + eprintln!("INFO: MSI native authentication support disabled - using stub implementation"); + } + } +} + +#[cfg(feature = "msi_auth")] +fn check_msi_library() -> bool { + use std::env; + use std::path::Path; + + // Check if MSINATIVE_LIB_PATH is provided + match env::var("MSINATIVE_LIB_PATH") { + Ok(msinative_lib_path) => { + println!("cargo:rerun-if-changed={}", msinative_lib_path); + + // Check if the path points to a valid static library file + let lib_path = Path::new(&msinative_lib_path); + if !lib_path.exists() { + eprintln!("WARNING: MSINATIVE_LIB_PATH points to non-existent file: {}", msinative_lib_path); + eprintln!("MSI authentication will be disabled."); + return false; + } + + if !lib_path.is_file() { + eprintln!("WARNING: MSINATIVE_LIB_PATH must point to a static library file, not a directory: {}", msinative_lib_path); + eprintln!("MSI authentication will be disabled."); + return false; + } + + // Check file extension to ensure it's a static library + let extension = lib_path.extension().and_then(|ext| ext.to_str()).unwrap_or(""); + let is_static_lib = match extension { + "a" => true, // Unix static library + "lib" => true, // Windows static library + _ => false, + }; + + if !is_static_lib { + eprintln!("WARNING: MSINATIVE_LIB_PATH should point to a static library file (.a or .lib): {}", msinative_lib_path); + eprintln!("Found file with extension: {}", extension); + eprintln!("MSI authentication will be disabled."); + return false; + } + + // Extract library directory and name for linking + if let (Some(lib_dir), Some(lib_name)) = (lib_path.parent(), lib_path.file_stem().and_then(|n| n.to_str())) { + // Add library search path + println!("cargo:rustc-link-search=native={}", lib_dir.display()); + + // Add library to link against (remove 'lib' prefix if present) + let link_name = if lib_name.starts_with("lib") { + &lib_name[3..] + } else { + lib_name + }; + println!("cargo:rustc-link-lib=static={}", link_name); + + // Add platform-specific system libraries that MSI typically depends on + add_platform_libraries(); + + eprintln!("INFO: Successfully configured MSI native library: {}", msinative_lib_path); + true + } else { + eprintln!("WARNING: Could not extract library name from path: {}", msinative_lib_path); + eprintln!("MSI authentication will be disabled."); + false + } + } + Err(_) => { + // MSINATIVE_LIB_PATH not set - provide helpful message but don't fail + eprintln!("INFO: MSINATIVE_LIB_PATH environment variable is not set."); + eprintln!("MSI authentication will use stub implementation (disabled at runtime)."); + eprintln!(""); + eprintln!("To enable full MSI authentication, please:"); + eprintln!("1. Set MSINATIVE_LIB_PATH to point to your pre-built MSI static library file"); + eprintln!("2. Ensure the library file exists and is accessible"); + eprintln!(""); + eprintln!("Example:"); + eprintln!(" export MSINATIVE_LIB_PATH=/path/to/libmsi.a # Linux/macOS"); + eprintln!(" export MSINATIVE_LIB_PATH=/path/to/msi.lib # Windows"); + eprintln!(" cargo build --features msi_auth"); + eprintln!(""); + eprintln!("If you don't need MSI authentication, build without the msi_auth feature:"); + eprintln!(" cargo build"); + + // Return false to indicate MSI native support is not available + false + } + } +} + +#[cfg(feature = "msi_auth")] +fn add_platform_libraries() { + use std::env; + + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + + // Add platform-specific system libraries that MSI authentication typically needs + match target_os.as_str() { + "windows" => { + println!("cargo:rustc-link-lib=advapi32"); + println!("cargo:rustc-link-lib=winhttp"); + println!("cargo:rustc-link-lib=crypt32"); + println!("cargo:rustc-link-lib=ws2_32"); + println!("cargo:rustc-link-lib=secur32"); + println!("cargo:rustc-link-lib=bcrypt"); + } + "linux" => { + println!("cargo:rustc-link-lib=stdc++"); + println!("cargo:rustc-link-lib=pthread"); + println!("cargo:rustc-link-lib=dl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + } + "macos" => { + println!("cargo:rustc-link-lib=c++"); + println!("cargo:rustc-link-lib=pthread"); + println!("cargo:rustc-link-lib=dl"); + println!("cargo:rustc-link-lib=ssl"); + println!("cargo:rustc-link-lib=crypto"); + } + _ => { + eprintln!("WARNING: Unsupported target OS for MSI authentication: {}", target_os); + } + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/examples/msi_auth_example.rs b/opentelemetry-exporter-geneva/geneva-uploader/examples/msi_auth_example.rs new file mode 100644 index 000000000..fc0b6d797 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/examples/msi_auth_example.rs @@ -0,0 +1,186 @@ +//! Example demonstrating MSI (Managed Service Identity) authentication with Geneva uploader +//! +//! This example shows how to configure the Geneva client to use Azure Managed Identity +//! for authentication instead of certificate-based authentication. +//! +//! Run with: cargo run --example msi_auth_example + +use geneva_uploader::{AuthMethod, GenevaClient, GenevaClientConfig, MsiIdentityType}; +use opentelemetry_proto::tonic::logs::v1::{LogRecord, ResourceLogs, ScopeLogs}; +use std::collections::HashMap; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("Geneva MSI Authentication Example"); + println!("================================="); + + // Example 1: System-assigned managed identity + println!("\n1. System-assigned Managed Identity"); + let system_config = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::ManagedIdentity { + identity: None, // System-assigned identity + fallback_to_default: false, + }, + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: None, + }; + + println!("Config: {:?}", system_config); + + // Alternative using builder method + let system_config_builder = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::Certificate { + path: "placeholder.p12".into(), + password: "placeholder".to_string(), + }, // Will be overridden + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: None, + } + .with_system_assigned_identity(); + + println!("Builder config: {:?}", system_config_builder); + + // Example 2: User-assigned managed identity by Client ID + println!("\n2. User-assigned Managed Identity (Client ID)"); + let user_config = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::Certificate { + path: "placeholder.p12".into(), + password: "placeholder".to_string(), + }, // Will be overridden + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: None, + } + .with_user_assigned_client_id("12345678-1234-1234-1234-123456789012".to_string(), true); + + println!("User-assigned config: {:?}", user_config); + + // Example 3: User-assigned managed identity by Object ID + println!("\n3. User-assigned Managed Identity (Object ID)"); + let object_id_config = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::Certificate { + path: "placeholder.p12".into(), + password: "placeholder".to_string(), + }, // Will be overridden + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: None, + } + .with_user_assigned_object_id("87654321-4321-4321-4321-210987654321".to_string(), false); + + println!("Object ID config: {:?}", object_id_config); + + // Example 4: User-assigned managed identity by Resource ID + println!("\n4. User-assigned Managed Identity (Resource ID)"); + let resource_id_config = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::Certificate { + path: "placeholder.p12".into(), + password: "placeholder".to_string(), + }, // Will be overridden + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: None, + } + .with_user_assigned_resource_id( + "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-identity".to_string(), + false, + ); + + println!("Resource ID config: {:?}", resource_id_config); + + // Example 5: Manual construction with MsiIdentityType + println!("\n5. Manual MSI Configuration"); + let manual_config = GenevaClientConfig { + endpoint: "https://geneva.example.com".to_string(), + environment: "prod".to_string(), + account: "myaccount".to_string(), + namespace: "myservice".to_string(), + region: "westus2".to_string(), + config_major_version: 1, + auth_method: AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ClientId( + "abcdef12-3456-7890-abcd-ef1234567890".to_string(), + )), + fallback_to_default: true, // Fallback to system-assigned if user-assigned fails + }, + tenant: "mytenant".to_string(), + role_name: "myrole".to_string(), + role_instance: "instance1".to_string(), + max_concurrent_uploads: Some(8), // Custom concurrency + }; + + println!("Manual config: {:?}", manual_config); + + // Note: In a real application, you would create the client and use it like this: + // + // let client = GenevaClient::new(system_config).await?; + // + // // Create some sample log data + // let log_record = LogRecord { + // body: Some("Test log message".into()), + // severity_text: "INFO".to_string(), + // attributes: vec![], + // ..Default::default() + // }; + // + // let scope_logs = ScopeLogs { + // log_records: vec![log_record], + // ..Default::default() + // }; + // + // let resource_logs = ResourceLogs { + // scope_logs: vec![scope_logs], + // ..Default::default() + // }; + // + // // Upload the logs + // client.upload_logs(&[resource_logs]).await?; + + println!("\n✅ MSI configuration examples completed successfully!"); + println!("\nKey Points:"); + println!("• System-assigned identity: Use `identity: None`"); + println!("• User-assigned identity: Use `identity: Some(MsiIdentityType::...)`"); + println!("• Fallback option: Set `fallback_to_default: true` to try system-assigned if user-assigned fails"); + println!("• Builder methods: Use `.with_system_assigned_identity()`, `.with_user_assigned_client_id()`, etc."); + println!("• The MSI library handles token acquisition and refresh automatically"); + println!("• Existing token caching and expiry logic is reused for MSI tokens"); + + Ok(()) +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs index 72bf5dd42..a9b7741ee 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/client.rs @@ -1,6 +1,6 @@ //! High-level GenevaClient for user code. Wraps config_service and ingestion_service. -use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig}; +use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig, MsiIdentityType}; use crate::ingestion_service::uploader::{GenevaUploader, GenevaUploaderConfig}; use crate::payload_encoder::lz4_chunked_compression::lz4_chunked_compression; use crate::payload_encoder::otlp_encoder::OtlpEncoder; @@ -26,6 +26,134 @@ pub struct GenevaClientConfig { // Add event name/version here if constant, or per-upload if you want them per call. } +impl GenevaClientConfig { + /// Configure the client to use system-assigned managed identity + /// + /// # Example + /// ```rust,no_run + /// # use geneva_uploader::GenevaClientConfig; + /// let config = GenevaClientConfig { + /// endpoint: "https://geneva.example.com".to_string(), + /// environment: "prod".to_string(), + /// account: "myaccount".to_string(), + /// namespace: "myservice".to_string(), + /// region: "westus2".to_string(), + /// config_major_version: 1, + /// auth_method: Default::default(), // placeholder + /// tenant: "mytenant".to_string(), + /// role_name: "myrole".to_string(), + /// role_instance: "instance1".to_string(), + /// max_concurrent_uploads: None, + /// }.with_system_assigned_identity(); + /// ``` + pub fn with_system_assigned_identity(mut self) -> Self { + self.auth_method = AuthMethod::ManagedIdentity { + identity: None, + fallback_to_default: false, + }; + self + } + + /// Configure the client to use user-assigned managed identity by Client ID + /// + /// # Arguments + /// * `client_id` - The Client ID (Application ID) of the user-assigned managed identity + /// * `fallback` - Whether to fallback to system-assigned identity if user-assigned fails + /// + /// # Example + /// ```rust,no_run + /// # use geneva_uploader::GenevaClientConfig; + /// let config = GenevaClientConfig { + /// // ... other fields + /// # endpoint: "https://geneva.example.com".to_string(), + /// # environment: "prod".to_string(), + /// # account: "myaccount".to_string(), + /// # namespace: "myservice".to_string(), + /// # region: "westus2".to_string(), + /// # config_major_version: 1, + /// # auth_method: Default::default(), + /// # tenant: "mytenant".to_string(), + /// # role_name: "myrole".to_string(), + /// # role_instance: "instance1".to_string(), + /// # max_concurrent_uploads: None, + /// }.with_user_assigned_client_id("12345678-1234-1234-1234-123456789012".to_string(), true); + /// ``` + pub fn with_user_assigned_client_id(mut self, client_id: String, fallback: bool) -> Self { + self.auth_method = AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ClientId(client_id)), + fallback_to_default: fallback, + }; + self + } + + /// Configure the client to use user-assigned managed identity by Object ID + /// + /// # Arguments + /// * `object_id` - The Object ID of the user-assigned managed identity in Azure AD + /// * `fallback` - Whether to fallback to system-assigned identity if user-assigned fails + /// + /// # Example + /// ```rust,no_run + /// # use geneva_uploader::GenevaClientConfig; + /// let config = GenevaClientConfig { + /// // ... other fields + /// # endpoint: "https://geneva.example.com".to_string(), + /// # environment: "prod".to_string(), + /// # account: "myaccount".to_string(), + /// # namespace: "myservice".to_string(), + /// # region: "westus2".to_string(), + /// # config_major_version: 1, + /// # auth_method: Default::default(), + /// # tenant: "mytenant".to_string(), + /// # role_name: "myrole".to_string(), + /// # role_instance: "instance1".to_string(), + /// # max_concurrent_uploads: None, + /// }.with_user_assigned_object_id("87654321-4321-4321-4321-210987654321".to_string(), false); + /// ``` + pub fn with_user_assigned_object_id(mut self, object_id: String, fallback: bool) -> Self { + self.auth_method = AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ObjectId(object_id)), + fallback_to_default: fallback, + }; + self + } + + /// Configure the client to use user-assigned managed identity by Resource ID + /// + /// # Arguments + /// * `resource_id` - The full ARM Resource ID of the user-assigned managed identity + /// * `fallback` - Whether to fallback to system-assigned identity if user-assigned fails + /// + /// # Example + /// ```rust,no_run + /// # use geneva_uploader::GenevaClientConfig; + /// let config = GenevaClientConfig { + /// // ... other fields + /// # endpoint: "https://geneva.example.com".to_string(), + /// # environment: "prod".to_string(), + /// # account: "myaccount".to_string(), + /// # namespace: "myservice".to_string(), + /// # region: "westus2".to_string(), + /// # config_major_version: 1, + /// # auth_method: Default::default(), + /// # tenant: "mytenant".to_string(), + /// # role_name: "myrole".to_string(), + /// # role_instance: "instance1".to_string(), + /// # max_concurrent_uploads: None, + /// }.with_user_assigned_resource_id( + /// "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-identity".to_string(), + /// false + /// ); + /// ``` + pub fn with_user_assigned_resource_id(mut self, resource_id: String, fallback: bool) -> Self { + self.auth_method = AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ResourceId(resource_id)), + fallback_to_default: fallback, + }; + self + } +} + /// Main user-facing client for Geneva ingestion. #[derive(Clone)] pub struct GenevaClient { diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs index 123b814af..f2a7c5c94 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/client.rs @@ -1,4 +1,4 @@ -// Geneva Config Client with TLS (PKCS#12) and TODO: Managed Identity support +// Geneva Config Client with TLS (PKCS#12) and MSI support use base64::{engine::general_purpose, Engine as _}; use reqwest::{ @@ -18,11 +18,15 @@ use std::fs; use std::path::PathBuf; use std::sync::RwLock; +#[cfg(feature = "msi_auth")] +use crate::msi; + /// Authentication methods for the Geneva Config Client. /// -/// The client supports two authentication methods: +/// The client supports three authentication methods: /// - Certificate-based authentication using PKCS#12 (.p12) files -/// - Managed Identity (Azure) - planned for future implementation +/// - Azure Managed Identity (MSI) authentication with support for both system-assigned and user-assigned identities +/// - Mock authentication for testing purposes /// /// # Certificate Format /// Certificates should be in PKCS#12 (.p12) format for client TLS authentication. @@ -55,12 +59,45 @@ pub enum AuthMethod { Certificate { path: PathBuf, password: String }, /// Azure Managed Identity authentication /// - /// Note(TODO): This is not yet implemented. - ManagedIdentity, + /// Supports both system-assigned and user-assigned managed identities. + /// When `identity` is `None`, uses system-assigned managed identity. + /// When `identity` is `Some(...)`, uses the specified user-assigned managed identity. + /// + /// # Arguments + /// * `identity` - Optional user-assigned identity specification. If None, uses system-assigned identity. + /// * `fallback_to_default` - If true and user-assigned identity fails, fallback to system-assigned identity. + ManagedIdentity { + identity: Option, + fallback_to_default: bool, + }, #[cfg(feature = "mock_auth")] MockAuth, // No authentication, used for testing purposes } +/// Types of managed identity that can be used for authentication +#[allow(dead_code)] +#[derive(Clone, Debug)] +pub enum MsiIdentityType { + /// User-assigned managed identity specified by Object ID + ObjectId(String), + /// User-assigned managed identity specified by Client ID + ClientId(String), + /// User-assigned managed identity specified by full ARM Resource ID + ResourceId(String), +} + +impl MsiIdentityType { + /// Convert to the MSI library's ManagedIdentity type + #[cfg(feature = "msi_auth")] + pub fn to_managed_identity(&self) -> msi::ManagedIdentity { + match self { + MsiIdentityType::ObjectId(id) => msi::ManagedIdentity::ObjectId(id.clone()), + MsiIdentityType::ClientId(id) => msi::ManagedIdentity::ClientId(id.clone()), + MsiIdentityType::ResourceId(id) => msi::ManagedIdentity::ResourceId(id.clone()), + } + } +} + #[derive(Debug, Error)] pub(crate) enum GenevaConfigClientError { // Authentication-related errors @@ -72,6 +109,12 @@ pub(crate) enum GenevaConfigClientError { JwtTokenError(String), #[error("Certificate error: {0}")] Certificate(String), + #[error("MSI authentication failed: {0}")] + #[cfg_attr(not(feature = "msi_auth"), allow(dead_code))] + MsiAuthenticationFailed(String), + #[error("MSI token refresh failed: {0}")] + #[cfg_attr(not(feature = "msi_auth"), allow(dead_code))] + MsiTokenRefreshFailed(String), // Networking / HTTP / TLS #[error("HTTP error: {0}")] @@ -246,10 +289,10 @@ impl GenevaConfigClient { .map_err(|e| GenevaConfigClientError::Certificate(e.to_string()))?; client_builder = client_builder.use_preconfigured_tls(tls_connector); } - AuthMethod::ManagedIdentity => { - return Err(GenevaConfigClientError::AuthMethodNotImplemented( - "Managed Identity authentication is not implemented yet".into(), - )); + AuthMethod::ManagedIdentity { .. } => { + // For MSI auth, we don't need special TLS configuration + // Authentication is done via Bearer tokens in HTTP headers + // Use default HTTPS client configuration } #[cfg(feature = "mock_auth")] AuthMethod::MockAuth => { @@ -418,6 +461,22 @@ impl GenevaConfigClient { /// Internal method that actually fetches data from Geneva Config Service async fn fetch_ingestion_info(&self) -> Result<(IngestionGatewayInfo, MonikerInfo)> { + match &self.config.auth_method { + AuthMethod::Certificate { .. } => { + self.fetch_with_certificate().await + } + AuthMethod::ManagedIdentity { identity, fallback_to_default } => { + self.fetch_with_msi(identity, *fallback_to_default).await + } + #[cfg(feature = "mock_auth")] + AuthMethod::MockAuth => { + self.fetch_with_certificate().await // Use same logic for mock auth + } + } + } + + /// Fetch ingestion info using certificate-based authentication + async fn fetch_with_certificate(&self) -> Result<(IngestionGatewayInfo, MonikerInfo)> { let tag_id = Uuid::new_v4().to_string(); //TODO - uuid is costly, check if counter is enough? let mut url = String::with_capacity(self.precomputed_url_prefix.len() + 50); // Pre-allocate with reasonable capacity write!(&mut url, "{}&TagId={tag_id}", self.precomputed_url_prefix).map_err(|e| { @@ -470,6 +529,123 @@ impl GenevaConfigClient { }) } } + + /// Fetch ingestion info using MSI authentication + #[cfg(feature = "msi_auth")] + async fn fetch_with_msi( + &self, + identity: &Option, + fallback_to_default: bool, + ) -> Result<(IngestionGatewayInfo, MonikerInfo)> { + // First attempt: try with specified identity (or system-assigned if None) + let result = self.try_msi_request(identity).await; + + match result { + Ok(info) => Ok(info), + Err(e) => { + // If fallback is enabled and we were using a user-assigned identity, try system-assigned + if fallback_to_default && identity.is_some() { + self.try_msi_request(&None).await.map_err(|fallback_err| { + GenevaConfigClientError::MsiAuthenticationFailed(format!( + "Primary identity failed: {}, Fallback also failed: {}", + e, fallback_err + )) + }) + } else { + Err(e) + } + } + } + } + + /// Fetch ingestion info using MSI authentication (stub when feature is disabled) + #[cfg(not(feature = "msi_auth"))] + async fn fetch_with_msi( + &self, + _identity: &Option, + _fallback_to_default: bool, + ) -> Result<(IngestionGatewayInfo, MonikerInfo)> { + Err(GenevaConfigClientError::AuthMethodNotImplemented( + "MSI authentication support is not enabled. Enable the 'msi_auth' feature to use MSI authentication.".into(), + )) + } + + /// Try MSI request with a specific identity configuration + #[cfg(feature = "msi_auth")] + async fn try_msi_request( + &self, + identity: &Option, + ) -> Result<(IngestionGatewayInfo, MonikerInfo)> { + // Get MSI token for Azure Monitor + let msi_identity = identity.as_ref().map(|id| id.to_managed_identity()); + let msi_token = msi::get_msi_access_token( + msi::resources::AZURE_MONITOR_PUBLIC, + msi_identity.as_ref(), + false, // not AntMds + ) + .map_err(|e| { + GenevaConfigClientError::MsiAuthenticationFailed(format!( + "Failed to get MSI token: {}", e + )) + })?; + + // Build the request URL + let tag_id = Uuid::new_v4().to_string(); + let mut url = String::with_capacity(self.precomputed_url_prefix.len() + 50); + write!(&mut url, "{}&TagId={tag_id}", self.precomputed_url_prefix).map_err(|e| { + GenevaConfigClientError::InternalError(format!("Failed to write URL: {e}")) + })?; + + let req_id = Uuid::new_v4().to_string(); + + // Make authenticated request using MSI token + let mut request = self + .http_client + .get(&url) + .headers(self.static_headers.clone()) + .header("Authorization", format!("Bearer {}", msi_token)) + .header("x-ms-client-request-id", req_id); + + let response = request + .send() + .await + .map_err(GenevaConfigClientError::Http)?; + + // Process response + let status = response.status(); + let body = response.text().await?; + + if status.is_success() { + let parsed = match serde_json::from_str::(&body) { + Ok(response) => response, + Err(e) => { + return Err(GenevaConfigClientError::AuthInfoNotFound(format!( + "Failed to parse response: {e}" + ))); + } + }; + + for account in parsed.storage_account_keys { + if account.is_primary_moniker && account.account_moniker_name.contains("diag") { + let moniker_info = MonikerInfo { + name: account.account_moniker_name, + account_group: account.account_group_name, + }; + + return Ok((parsed.ingestion_gateway_info, moniker_info)); + } + } + + Err(GenevaConfigClientError::MonikerNotFound( + "No primary diag moniker found in storage accounts".to_string(), + )) + } else { + Err(GenevaConfigClientError::RequestFailed { + status: status.as_u16(), + message: body, + }) + } + } } #[inline] diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs index 29d027d43..91e929377 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/config_service/mod.rs @@ -20,12 +20,15 @@ mod tests { namespace: "ns".to_string(), region: "region".to_string(), config_major_version: 1, - auth_method: AuthMethod::ManagedIdentity, + auth_method: AuthMethod::ManagedIdentity { + identity: None, + fallback_to_default: false, + }, }; assert_eq!(config.environment, "env"); assert_eq!(config.account, "acct"); - assert!(matches!(config.auth_method, AuthMethod::ManagedIdentity)); + assert!(matches!(config.auth_method, AuthMethod::ManagedIdentity { .. })); } fn generate_self_signed_p12() -> (NamedTempFile, String) { diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs index e322626cc..54a3e6c17 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/lib.rs @@ -1,6 +1,7 @@ mod config_service; mod ingestion_service; pub mod payload_encoder; +mod msi; pub mod client; @@ -18,4 +19,4 @@ pub(crate) use ingestion_service::uploader::{ }; pub use client::{GenevaClient, GenevaClientConfig}; -pub use config_service::client::AuthMethod; +pub use config_service::client::{AuthMethod, MsiIdentityType}; diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/error.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/error.rs new file mode 100644 index 000000000..67701890b --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/error.rs @@ -0,0 +1,144 @@ +//! Error types for MSI authentication + +use std::fmt; +use thiserror::Error; + +#[cfg(feature = "msi_auth")] +use crate::msi::ffi::XPLATRESULT; + +/// Errors that can occur when working with MSI tokens +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum MsiError { + /// MSI token source initialization failed + #[error("Initialization failed")] + InitializationFailed, + + /// General failure occurred + #[error("General failure")] + GeneralFailure, + + /// Azure MSI token request failed + #[error("Azure MSI token request failed")] + AzureMsiFailed, + + /// ARC MSI token request failed + #[error("ARC MSI token request failed")] + ArcMsiFailed, + + /// AntMds MSI token request failed + #[error("AntMds MSI token request failed")] + AntMdsMsiFailed, + + /// MSI authentication failed + #[error("MSI authentication failed: {0}")] + AuthenticationFailed(String), + + /// IMDS endpoint is not accessible + #[error("IMDS endpoint error")] + ImdsEndpointError, + + /// Invalid parameter provided + #[error("Invalid parameter: {0}")] + InvalidParameter(String), + + /// Null pointer encountered in FFI + #[error("Null pointer encountered")] + NullPointer, + + /// String conversion between Rust and C failed + #[error("String conversion failed")] + StringConversionFailed, + + /// Unknown error code from underlying library + #[error("Unknown error code: {0}")] + Unknown(i32), +} + +#[cfg(feature = "msi_auth")] +impl MsiError { + /// Convert an XPLATRESULT to a MsiError + pub fn from_xplat_result(result: XPLATRESULT) -> Self { + // Error codes based on XPlatErrors.h + match result { + 0 => return Self::GeneralFailure, // XPLAT_NO_ERROR should not be an error + code if code < 0 => { + // Extract facility and error code + let facility = ((code as u32) >> 8) & 0xFF; + let error_code = (code as u32) & 0xFF; + + match facility { + 0x1 => { // XPLAT_FACILITY_GENERAL + match error_code { + 0x1 => Self::GeneralFailure, + 0x3 => Self::InitializationFailed, + _ => Self::Unknown(result), + } + }, + 0x2 => { // XPLAT_FACILITY_MSI_TOKEN + match error_code { + 0x1 => Self::AzureMsiFailed, + 0x2 => Self::ArcMsiFailed, + 0x3 => Self::AntMdsMsiFailed, + _ => Self::Unknown(result), + } + }, + 0x3 => { // XPLAT_FACILITY_IMDS + match error_code { + 0x1 => Self::ImdsEndpointError, + _ => Self::Unknown(result), + } + }, + _ => Self::Unknown(result), + } + }, + _ => Self::Unknown(result), + } + } + + /// Check if an XPLATRESULT indicates success + pub fn is_success(result: XPLATRESULT) -> bool { + result >= 0 + } + + /// Convert an XPLATRESULT to a Result type + pub fn check_result(result: XPLATRESULT) -> Result<(), Self> { + if Self::is_success(result) { + Ok(()) + } else { + Err(Self::from_xplat_result(result)) + } + } +} + +/// Result type for MSI operations +pub type MsiResult = Result; + +#[cfg(test)] +#[cfg(feature = "msi_auth")] +mod tests { + use super::*; + + #[test] + fn test_error_conversion() { + // Test success + assert!(MsiError::is_success(0)); + assert!(MsiError::check_result(0).is_ok()); + + // Test simple negative error codes (for mock implementation) + assert_eq!(MsiError::from_xplat_result(-1), MsiError::Unknown(-1)); + assert_eq!(MsiError::from_xplat_result(-2), MsiError::Unknown(-2)); + + // Test that positive codes other than 0 are treated as success + assert!(MsiError::is_success(1)); + assert!(MsiError::check_result(1).is_ok()); + } + + #[test] + fn test_error_display() { + let error = MsiError::InitializationFailed; + assert_eq!(error.to_string(), "Initialization failed"); + + let param_error = MsiError::InvalidParameter("resource".to_string()); + assert_eq!(param_error.to_string(), "Invalid parameter: resource"); + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs new file mode 100644 index 000000000..c755e646c --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs @@ -0,0 +1,125 @@ +//! Low-level FFI bindings to the XPlatLib MSI token functionality + +use std::os::raw::{c_char, c_int, c_long, c_void}; + +// Include the C++ wrapper header for MSI authentication +#[cfg(feature = "msi_auth")] +extern "C" { + #[link_name = "src/msi/native/wrapper.h"] + fn __include_wrapper_header(); +} + +// Platform-specific types +#[cfg(target_os = "windows")] +pub type XPLATRESULT = c_long; + +#[cfg(not(target_os = "windows"))] +pub type XPLATRESULT = c_int; + +/// IMDS Endpoint Types +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ImdsEndpointType { + /// Custom IMDS endpoint + CustomEndpoint = 0, + /// Azure Arc IMDS endpoint + ArcEndpoint = 1, + /// Standard Azure IMDS endpoint + AzureEndpoint = 2, + /// AntMds endpoint for specific environments + AntMdsEndpoint = 3, +} + +#[cfg(feature = "msi_auth")] +extern "C" { + /// Error constants (these will be linked from the C++ library) + pub static XPLAT_NO_ERROR: XPLATRESULT; + pub static XPLAT_FAIL: XPLATRESULT; + pub static XPLAT_INITIALIZED: XPLATRESULT; + pub static XPLAT_INITIALIZATION_FAILED: XPLATRESULT; + pub static XPLAT_AZURE_MSI_FAILED: XPLATRESULT; + pub static XPLAT_ARC_MSI_FAILED: XPLATRESULT; + pub static XPLAT_ANTMDS_MSI_FAILED: XPLATRESULT; + pub static XPLAT_IMDS_ENDPOINT_ERROR: XPLATRESULT; + + /// Simple wrapper for getting MSI access token + pub fn rust_get_msi_access_token( + resource: *const c_char, + managed_id_identifier: *const c_char, + managed_id_value: *const c_char, + is_ant_mds: bool, + token: *mut *mut c_char, + ) -> XPLATRESULT; + + /// Create MSI Token Source + pub fn rust_create_imsi_token_source() -> *mut c_void; + + /// Initialize MSI Token Source + pub fn rust_imsi_token_source_initialize( + token_source: *mut c_void, + resource: *const c_char, + managed_id_identifier: *const c_char, + managed_id_value: *const c_char, + fallback_to_default: bool, + is_ant_mds: bool, + ) -> XPLATRESULT; + + /// Get Access Token + pub fn rust_imsi_token_source_get_access_token( + token_source: *mut c_void, + force_refresh: bool, + access_token: *mut *mut c_char, + ) -> XPLATRESULT; + + /// Get Expires On Seconds + pub fn rust_imsi_token_source_get_expires_on_seconds( + token_source: *mut c_void, + expires_on_seconds: *mut c_long, + ) -> XPLATRESULT; + + /// Set IMDS Host Address + pub fn rust_imsi_token_source_set_imds_host_address( + token_source: *mut c_void, + host_address: *const c_char, + endpoint_type: c_int, + ) -> XPLATRESULT; + + /// Get IMDS Host Address + pub fn rust_imsi_token_source_get_imds_host_address( + token_source: *mut c_void, + host_address: *mut *mut c_char, + ) -> XPLATRESULT; + + /// Stop Token Source + pub fn rust_imsi_token_source_stop(token_source: *mut c_void); + + /// Destroy Token Source + pub fn rust_destroy_imsi_token_source(token_source: *mut c_void); + + /// Free string allocated by the library + pub fn rust_free_string(str: *mut c_char); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ptr; + + #[test] + fn test_endpoint_type_values() { + assert_eq!(ImdsEndpointType::CustomEndpoint as c_int, 0); + assert_eq!(ImdsEndpointType::ArcEndpoint as c_int, 1); + assert_eq!(ImdsEndpointType::AzureEndpoint as c_int, 2); + assert_eq!(ImdsEndpointType::AntMdsEndpoint as c_int, 3); + } + + #[test] + fn test_null_pointers() { + // Test that we can create null pointers safely + let null_ptr: *mut c_void = ptr::null_mut(); + assert!(null_ptr.is_null()); + + let null_char: *mut c_char = ptr::null_mut(); + assert!(null_char.is_null()); + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs new file mode 100644 index 000000000..a47d18406 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs @@ -0,0 +1,33 @@ +//! Azure Managed Service Identity (MSI) authentication module +//! +//! This module provides MSI authentication functionality integrated directly into the Geneva uploader. +//! It contains the essential components from the MSI library needed for Geneva authentication. + +#[cfg(feature = "msi_auth")] +pub mod error; +#[cfg(feature = "msi_auth")] +pub mod ffi; +#[cfg(feature = "msi_auth")] +pub mod token_source; +#[cfg(feature = "msi_auth")] +pub mod types; + +#[cfg(feature = "msi_auth")] +pub use error::{MsiError, MsiResult}; +#[cfg(feature = "msi_auth")] +pub use token_source::get_msi_access_token; +#[cfg(feature = "msi_auth")] +pub use types::ManagedIdentity; + +/// Azure Monitor service endpoints for Geneva authentication +#[cfg(feature = "msi_auth")] +pub mod resources { + /// Azure Monitor endpoint for public Azure cloud (used for Geneva authentication) + pub const AZURE_MONITOR_PUBLIC: &str = "https://monitor.core.windows.net/"; + + /// Azure Monitor endpoint for US Government cloud + pub const AZURE_MONITOR_USGOV: &str = "https://monitor.core.usgovcloudapi.net/"; + + /// Azure Monitor endpoint for China cloud + pub const AZURE_MONITOR_CHINA: &str = "https://monitor.core.chinacloudapi.cn/"; +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/README.md b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/README.md new file mode 100644 index 000000000..96d9dd069 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/README.md @@ -0,0 +1,16 @@ +# MSI Native Interface + +This directory contains the C/C++ native interface files for MSI (Managed Service Identity) authentication support. + +## Files + +- **`wrapper.h`** - C header file defining the FFI interface between Rust and the native MSI library +- **`bridge.cpp`** - C++ implementation that bridges Rust calls to the XPlatLib MSI functionality + +## Purpose + +These files provide a C-compatible interface to the Microsoft XPlatLib MSI authentication library, allowing the Rust code to obtain MSI tokens for authentication with Azure services. + +## Integration + +The Rust FFI bindings in `../ffi.rs` reference these native files when the `msi_auth` feature is enabled. The build process (via `build.rs`) handles compilation and linking of these native components when MSI authentication support is available. diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp new file mode 100644 index 000000000..a4886afb0 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +#include "IMSIToken.h" +#include "StringUtils.h" +#include "XPlatErrors.h" + +extern "C" { + +// Simple wrapper for the existing C function +XPLATRESULT rust_get_msi_access_token( + const char* resource, + const char* managed_id_identifier, + const char* managed_id_value, + bool is_ant_mds, + char** token +) { + return GetMSIAccessToken(resource, managed_id_identifier, managed_id_value, is_ant_mds, token); +} + +// Create MSI Token Source +void* rust_create_imsi_token_source() { + return CreateIMSITokenSource(); +} + +// Initialize MSI Token Source +XPLATRESULT rust_imsi_token_source_initialize( + void* token_source, + const char* resource, + const char* managed_id_identifier, + const char* managed_id_value, + bool fallback_to_default, + bool is_ant_mds +) { + if (!token_source) return XPLAT_INITIALIZATION_FAILED; + + auto* source = static_cast(token_source); + xplat_string_t x_resource = XPlatUtils::string_to_string_t(std::string(resource ? resource : "")); + xplat_string_t x_managed_id_identifier = XPlatUtils::string_to_string_t(std::string(managed_id_identifier ? managed_id_identifier : "")); + xplat_string_t x_managed_id_value = XPlatUtils::string_to_string_t(std::string(managed_id_value ? managed_id_value : "")); + + return source->Initialize(x_resource, x_managed_id_identifier, x_managed_id_value, fallback_to_default, is_ant_mds); +} + +// Get Access Token +XPLATRESULT rust_imsi_token_source_get_access_token( + void* token_source, + bool force_refresh, + char** access_token +) { + if (!token_source || !access_token) return XPLAT_FAIL; + + auto* source = static_cast(token_source); + xplat_string_t token; + + XPLATRESULT result = source->GetAccessToken(token, force_refresh); + if (SUCCEEDED(result)) { + std::string token_str = XPlatUtils::string_t_to_string(token); + *access_token = new char[token_str.length() + 1]; + strcpy(*access_token, token_str.c_str()); + } else { + *access_token = nullptr; + } + + return result; +} + +// Get Expires On Seconds +XPLATRESULT rust_imsi_token_source_get_expires_on_seconds( + void* token_source, + long int* expires_on_seconds +) { + if (!token_source || !expires_on_seconds) return XPLAT_FAIL; + + auto* source = static_cast(token_source); + return source->GetExpiresOnSeconds(*expires_on_seconds); +} + +// Set IMDS Host Address +XPLATRESULT rust_imsi_token_source_set_imds_host_address( + void* token_source, + const char* host_address, + int endpoint_type +) { + if (!token_source || !host_address) return XPLAT_FAIL; + + auto* source = static_cast(token_source); + xplat_string_t x_host_address = XPlatUtils::string_to_string_t(std::string(host_address)); + ImdsEndpointType x_endpoint_type = static_cast(endpoint_type); + + return source->SetImdsHostAddress(x_host_address, x_endpoint_type); +} + +// Get IMDS Host Address +XPLATRESULT rust_imsi_token_source_get_imds_host_address( + void* token_source, + char** host_address +) { + if (!token_source || !host_address) return XPLAT_FAIL; + + auto* source = static_cast(token_source); + xplat_string_t address = source->GetImdsHostAddress(); + std::string address_str = XPlatUtils::string_t_to_string(address); + + *host_address = new char[address_str.length() + 1]; + strcpy(*host_address, address_str.c_str()); + + return XPLAT_NO_ERROR; +} + +// Stop Token Source +void rust_imsi_token_source_stop(void* token_source) { + if (token_source) { + auto* source = static_cast(token_source); + source->Stop(); + } +} + +// Destroy Token Source +void rust_destroy_imsi_token_source(void* token_source) { + if (token_source) { + delete static_cast(token_source); + } +} + +// Free string allocated by the library +void rust_free_string(char* str) { + if (str) { + delete[] str; + } +} + +} // extern "C" diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/wrapper.h b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/wrapper.h new file mode 100644 index 000000000..33fa549de --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/wrapper.h @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +#ifndef RUST_MSI_TOKEN_WRAPPER_H +#define RUST_MSI_TOKEN_WRAPPER_H + +#ifdef __cplusplus +extern "C" { +#endif + +// Platform-specific types +#ifdef _WIN32 +typedef long XPLATRESULT; +#else +typedef int XPLATRESULT; +#endif + +// Error codes (from XPlatErrors.h) +extern const XPLATRESULT XPLAT_NO_ERROR; +extern const XPLATRESULT XPLAT_FAIL; +extern const XPLATRESULT XPLAT_INITIALIZED; +extern const XPLATRESULT XPLAT_INITIALIZATION_FAILED; +extern const XPLATRESULT XPLAT_AZURE_MSI_FAILED; +extern const XPLATRESULT XPLAT_ARC_MSI_FAILED; +extern const XPLATRESULT XPLAT_ANTMDS_MSI_FAILED; +extern const XPLATRESULT XPLAT_IMDS_ENDPOINT_ERROR; + +// Endpoint types +typedef enum { + Custom_Endpoint = 0, + ARC_Endpoint = 1, + Azure_Endpoint = 2, + AntMds_Endpoint = 3 +} ImdsEndpointType; + +// Simple wrapper for the existing C function +XPLATRESULT rust_get_msi_access_token( + const char* resource, + const char* managed_id_identifier, + const char* managed_id_value, + bool is_ant_mds, + char** token +); + +// Create MSI Token Source +void* rust_create_imsi_token_source(void); + +// Initialize MSI Token Source +XPLATRESULT rust_imsi_token_source_initialize( + void* token_source, + const char* resource, + const char* managed_id_identifier, + const char* managed_id_value, + bool fallback_to_default, + bool is_ant_mds +); + +// Get Access Token +XPLATRESULT rust_imsi_token_source_get_access_token( + void* token_source, + bool force_refresh, + char** access_token +); + +// Get Expires On Seconds +XPLATRESULT rust_imsi_token_source_get_expires_on_seconds( + void* token_source, + long int* expires_on_seconds +); + +// Set IMDS Host Address +XPLATRESULT rust_imsi_token_source_set_imds_host_address( + void* token_source, + const char* host_address, + int endpoint_type +); + +// Get IMDS Host Address +XPLATRESULT rust_imsi_token_source_get_imds_host_address( + void* token_source, + char** host_address +); + +// Stop Token Source +void rust_imsi_token_source_stop(void* token_source); + +// Destroy Token Source +void rust_destroy_imsi_token_source(void* token_source); + +// Free string allocated by the library +void rust_free_string(char* str); + +#ifdef __cplusplus +} +#endif + +#endif // RUST_MSI_TOKEN_WRAPPER_H diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs new file mode 100644 index 000000000..ddf38b040 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs @@ -0,0 +1,116 @@ +//! High-level safe wrapper around the MSI token source functionality + +#[cfg(feature = "msi_auth")] +use std::os::raw::{c_char, c_long}; +#[cfg(feature = "msi_auth")] +use std::ptr; + +#[cfg(feature = "msi_auth")] +use crate::msi::error::{MsiError, MsiResult}; +#[cfg(feature = "msi_auth")] +use crate::msi::ffi; +#[cfg(feature = "msi_auth")] +use crate::msi::types::{string_utils, ManagedIdentity}; + +/// Convenience function to get an MSI access token with simple parameters +#[cfg(all(feature = "msi_auth", msi_native_available))] +pub fn get_msi_access_token( + resource: &str, + managed_identity: Option<&ManagedIdentity>, + is_ant_mds: bool, +) -> MsiResult { + let id_type = managed_identity.map(|id| id.identifier_type()).unwrap_or(""); + let id_value = managed_identity.map(|id| id.identifier_value()).unwrap_or(""); + + let (_resource_ptr, _resource_cstring) = string_utils::string_to_c_ptr(resource)?; + let (_id_type_ptr, _id_type_cstring) = string_utils::optional_string_to_c_ptr( + if id_type.is_empty() { None } else { Some(id_type) } + )?; + let (_id_value_ptr, _id_value_cstring) = string_utils::optional_string_to_c_ptr( + if id_value.is_empty() { None } else { Some(id_value) } + )?; + + let mut token_ptr: *mut c_char = ptr::null_mut(); + + let result = unsafe { + ffi::rust_get_msi_access_token( + _resource_ptr, + _id_type_ptr, + _id_value_ptr, + is_ant_mds, + &mut token_ptr, + ) + }; + + MsiError::check_result(result)?; + + if token_ptr.is_null() { + return Err(MsiError::NullPointer); + } + + let token = unsafe { + let result = string_utils::c_string_to_rust_string(token_ptr); + ffi::rust_free_string(token_ptr); + result + }?; + + Ok(token) +} + +/// Stub implementation when MSI feature is enabled but native library is not available +#[cfg(all(feature = "msi_auth", not(msi_native_available)))] +pub fn get_msi_access_token( + _resource: &str, + _managed_identity: Option<&ManagedIdentity>, + _is_ant_mds: bool, +) -> MsiResult { + Err(MsiError::AuthenticationFailed( + "MSI native library is not available. Set MSINATIVE_LIB_PATH and ensure dependencies are installed.".to_string() + )) +} + +/// Stub implementation when MSI authentication is not enabled +#[cfg(not(feature = "msi_auth"))] +pub fn get_msi_access_token( + _resource: &str, + _managed_identity: Option<&crate::config_service::client::MsiIdentityType>, + _is_ant_mds: bool, +) -> Result { + Err("MSI authentication support is not enabled. Enable the 'msi_auth' feature to use MSI authentication.".into()) +} + +#[cfg(test)] +#[cfg(feature = "msi_auth")] +mod tests { + use super::*; + use crate::msi::types::ManagedIdentity; + + #[test] + fn test_managed_identity_parameters() { + // Test parameter generation for different identity types + let client_id = ManagedIdentity::ClientId("test-client-id".to_string()); + assert_eq!(client_id.identifier_type(), "client_id"); + assert_eq!(client_id.identifier_value(), "test-client-id"); + + let object_id = ManagedIdentity::ObjectId("test-object-id".to_string()); + assert_eq!(object_id.identifier_type(), "object_id"); + assert_eq!(object_id.identifier_value(), "test-object-id"); + + let resource_id = ManagedIdentity::ResourceId("/subscriptions/test".to_string()); + assert_eq!(resource_id.identifier_type(), "mi_res_id"); + assert_eq!(resource_id.identifier_value(), "/subscriptions/test"); + } + + #[test] + fn test_parameter_validation() { + // Test that empty/None parameters are handled correctly + let id_type = None::<&ManagedIdentity>.map(|id| id.identifier_type()).unwrap_or(""); + let id_value = None::<&ManagedIdentity>.map(|id| id.identifier_value()).unwrap_or(""); + + assert_eq!(id_type, ""); + assert_eq!(id_value, ""); + } + + // Note: We can't test the actual get_msi_access_token function without the C++ library + // being available in the test environment, but we can test the parameter handling logic +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs new file mode 100644 index 000000000..be9d0e283 --- /dev/null +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs @@ -0,0 +1,272 @@ +//! Type definitions for MSI authentication + +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +#[cfg(feature = "msi_auth")] +use crate::msi::error::{MsiError, MsiResult}; + +/// Managed Identity configuration +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ManagedIdentity { + /// Use Object ID to identify the managed identity + ObjectId(String), + /// Use Client ID to identify the managed identity + ClientId(String), + /// Use Resource ID to identify the managed identity + ResourceId(String), +} + +impl ManagedIdentity { + /// Get the identifier type string for the C++ API + pub fn identifier_type(&self) -> &'static str { + match self { + ManagedIdentity::ObjectId(_) => "object_id", + ManagedIdentity::ClientId(_) => "client_id", + ManagedIdentity::ResourceId(_) => "mi_res_id", + } + } + + /// Get the identifier value + pub fn identifier_value(&self) -> &str { + match self { + ManagedIdentity::ObjectId(value) => value, + ManagedIdentity::ClientId(value) => value, + ManagedIdentity::ResourceId(value) => value, + } + } +} + +/// Configuration for MSI token requests +#[cfg(feature = "msi_auth")] +#[derive(Debug, Clone)] +pub struct MsiConfig { + /// The resource for which to request a token (e.g., "https://monitor.core.windows.net/") + pub resource: String, + /// Optional managed identity configuration + pub managed_identity: Option, + /// Whether to fallback to default identity if the specified identity fails + pub fallback_to_default: bool, + /// Whether this is an AntMds request + pub is_ant_mds: bool, +} + +#[cfg(feature = "msi_auth")] +impl MsiConfig { + /// Create a new configuration for the specified resource + pub fn new(resource: impl Into) -> Self { + Self { + resource: resource.into(), + managed_identity: None, + fallback_to_default: false, + is_ant_mds: false, + } + } + + /// Set the managed identity + pub fn with_managed_identity(mut self, identity: ManagedIdentity) -> Self { + self.managed_identity = Some(identity); + self + } + + /// Enable fallback to default identity + pub fn with_fallback_to_default(mut self, fallback: bool) -> Self { + self.fallback_to_default = fallback; + self + } + + /// Set whether this is an AntMds request + pub fn with_ant_mds(mut self, is_ant_mds: bool) -> Self { + self.is_ant_mds = is_ant_mds; + self + } + + /// Get the identifier type string, or empty string if no managed identity is set + pub fn identifier_type(&self) -> &str { + self.managed_identity + .as_ref() + .map(|id| id.identifier_type()) + .unwrap_or("") + } + + /// Get the identifier value, or empty string if no managed identity is set + pub fn identifier_value(&self) -> &str { + self.managed_identity + .as_ref() + .map(|id| id.identifier_value()) + .unwrap_or("") + } +} + +/// Utility functions for string conversion between Rust and C +#[cfg(feature = "msi_auth")] +pub(crate) mod string_utils { + use super::*; + + /// Convert a Rust string to a C string pointer + /// Returns None if the string contains null bytes + pub fn rust_string_to_c_string(s: &str) -> MsiResult { + CString::new(s).map_err(|_| MsiError::StringConversionFailed) + } + + /// Convert a C string pointer to a Rust String + /// Returns an error if the pointer is null or the string is not valid UTF-8 + pub unsafe fn c_string_to_rust_string(ptr: *const c_char) -> MsiResult { + if ptr.is_null() { + return Err(MsiError::NullPointer); + } + + let c_str = CStr::from_ptr(ptr); + c_str + .to_str() + .map(|s| s.to_string()) + .map_err(|_| MsiError::StringConversionFailed) + } + + /// Helper to get a C string pointer from an optional Rust string + /// Returns a null pointer if the input is None or empty + pub fn optional_string_to_c_ptr(s: Option<&str>) -> MsiResult<(*const c_char, Option)> { + match s { + Some(s) if !s.is_empty() => { + let c_string = rust_string_to_c_string(s)?; + let ptr = c_string.as_ptr(); + Ok((ptr, Some(c_string))) + } + _ => Ok((ptr::null(), None)), + } + } + + /// Helper to get a C string pointer from a Rust string + pub fn string_to_c_ptr(s: &str) -> MsiResult<(*const c_char, CString)> { + let c_string = rust_string_to_c_string(s)?; + let ptr = c_string.as_ptr(); + Ok((ptr, c_string)) + } +} + +/// Token information returned by MSI requests +#[cfg(feature = "msi_auth")] +#[derive(Debug, Clone)] +pub struct TokenInfo { + /// The access token + pub access_token: String, + /// Expiration time in seconds since Unix epoch + pub expires_on: i64, +} + +#[cfg(feature = "msi_auth")] +impl TokenInfo { + /// Create a new TokenInfo + pub fn new(access_token: String, expires_on: i64) -> Self { + Self { + access_token, + expires_on, + } + } + + /// Check if the token is expired (with a 5-minute buffer) + pub fn is_expired(&self) -> bool { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + + // Add 5-minute buffer to avoid using tokens that are about to expire + self.expires_on <= (now + 300) + } + + /// Get the number of seconds until expiration + pub fn seconds_until_expiration(&self) -> i64 { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64; + + self.expires_on - now + } +} + +#[cfg(test)] +#[cfg(feature = "msi_auth")] +mod tests { + use super::*; + + #[test] + fn test_managed_identity() { + let object_id = ManagedIdentity::ObjectId("12345".to_string()); + assert_eq!(object_id.identifier_type(), "object_id"); + assert_eq!(object_id.identifier_value(), "12345"); + + let client_id = ManagedIdentity::ClientId("67890".to_string()); + assert_eq!(client_id.identifier_type(), "client_id"); + assert_eq!(client_id.identifier_value(), "67890"); + + let resource_id = ManagedIdentity::ResourceId("/subscriptions/...".to_string()); + assert_eq!(resource_id.identifier_type(), "mi_res_id"); + assert_eq!(resource_id.identifier_value(), "/subscriptions/..."); + } + + #[test] + fn test_msi_config() { + let config = MsiConfig::new("https://monitor.core.windows.net/"); + assert_eq!(config.resource, "https://monitor.core.windows.net/"); + assert!(config.managed_identity.is_none()); + assert!(!config.fallback_to_default); + assert!(!config.is_ant_mds); + + let config = config + .with_managed_identity(ManagedIdentity::ClientId("test".to_string())) + .with_fallback_to_default(true) + .with_ant_mds(true); + + assert!(config.managed_identity.is_some()); + assert_eq!(config.identifier_type(), "client_id"); + assert_eq!(config.identifier_value(), "test"); + assert!(config.fallback_to_default); + assert!(config.is_ant_mds); + } + + #[test] + fn test_token_info() { + let future_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64 + 3600; // 1 hour from now + + let token = TokenInfo::new("test_token".to_string(), future_time); + assert!(!token.is_expired()); + assert!(token.seconds_until_expiration() > 0); + + let past_time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() as i64 - 3600; // 1 hour ago + + let expired_token = TokenInfo::new("expired_token".to_string(), past_time); + assert!(expired_token.is_expired()); + assert!(expired_token.seconds_until_expiration() < 0); + } + + #[test] + fn test_string_utils() { + use string_utils::*; + + // Test valid string conversion + let c_string = rust_string_to_c_string("test").unwrap(); + assert_eq!(c_string.to_str().unwrap(), "test"); + + // Test string with null byte (should fail) + assert!(rust_string_to_c_string("test\0test").is_err()); + + // Test optional string conversion + let (ptr, _cstring) = optional_string_to_c_ptr(Some("test")).unwrap(); + assert!(!ptr.is_null()); + + let (ptr, _cstring) = optional_string_to_c_ptr(None).unwrap(); + assert!(ptr.is_null()); + + let (ptr, _cstring) = optional_string_to_c_ptr(Some("")).unwrap(); + assert!(ptr.is_null()); + } +} From fb3e1271bc0e32558ae905e771ca003bcbc4c064 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Thu, 24 Jul 2025 05:55:09 +0000 Subject: [PATCH 2/4] fix --- opentelemetry-exporter-geneva/geneva-uploader/build.rs | 7 +++++++ .../geneva-uploader/src/msi/ffi.rs | 4 ++-- .../geneva-uploader/src/msi/mod.rs | 7 +------ .../geneva-uploader/src/msi/types.rs | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/build.rs b/opentelemetry-exporter-geneva/geneva-uploader/build.rs index 8f096e51a..66f00de56 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/build.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/build.rs @@ -1,6 +1,7 @@ //! Build script for Geneva Uploader with optional MSI authentication support fn main() { + println!("cargo:warning=---> Running build script for Geneva Uploader"); // Always output cargo rerun instructions for environment changes println!("cargo:rerun-if-env-changed=MSINATIVE_LIB_PATH"); println!("cargo:rerun-if-changed=build.rs"); @@ -11,6 +12,7 @@ fn main() { // Only run MSI build logic if the msi_auth feature is enabled #[cfg(feature = "msi_auth")] { + println!("cargo:warning=---> Checking for MSI native library support"); let msi_available = check_msi_library(); if msi_available { println!("cargo:rustc-cfg=msi_native_available"); @@ -29,6 +31,7 @@ fn check_msi_library() -> bool { // Check if MSINATIVE_LIB_PATH is provided match env::var("MSINATIVE_LIB_PATH") { Ok(msinative_lib_path) => { + println!("cargo:warning=---> MSINATIVE_LIB_PATH is set to: {}", msinative_lib_path); println!("cargo:rerun-if-changed={}", msinative_lib_path); // Check if the path points to a valid static library file @@ -59,9 +62,12 @@ fn check_msi_library() -> bool { eprintln!("MSI authentication will be disabled."); return false; } + + println!("cargo:warning=---> Valid static library found: {}", msinative_lib_path); // Extract library directory and name for linking if let (Some(lib_dir), Some(lib_name)) = (lib_path.parent(), lib_path.file_stem().and_then(|n| n.to_str())) { + println!("cargo:warning=---> Library directory: {}", lib_dir.display()); // Add library search path println!("cargo:rustc-link-search=native={}", lib_dir.display()); @@ -71,6 +77,7 @@ fn check_msi_library() -> bool { } else { lib_name }; + println!("cargo:warning=---> Linking against library: {}", link_name); println!("cargo:rustc-link-lib=static={}", link_name); // Add platform-specific system libraries that MSI typically depends on diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs index c755e646c..761f73c8b 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs @@ -4,7 +4,7 @@ use std::os::raw::{c_char, c_int, c_long, c_void}; // Include the C++ wrapper header for MSI authentication #[cfg(feature = "msi_auth")] -extern "C" { +unsafe extern "C" { #[link_name = "src/msi/native/wrapper.h"] fn __include_wrapper_header(); } @@ -31,7 +31,7 @@ pub enum ImdsEndpointType { } #[cfg(feature = "msi_auth")] -extern "C" { +unsafe extern "C" { /// Error constants (these will be linked from the C++ library) pub static XPLAT_NO_ERROR: XPLATRESULT; pub static XPLAT_FAIL: XPLATRESULT; diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs index a47d18406..b006e0701 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs @@ -24,10 +24,5 @@ pub use types::ManagedIdentity; pub mod resources { /// Azure Monitor endpoint for public Azure cloud (used for Geneva authentication) pub const AZURE_MONITOR_PUBLIC: &str = "https://monitor.core.windows.net/"; - - /// Azure Monitor endpoint for US Government cloud - pub const AZURE_MONITOR_USGOV: &str = "https://monitor.core.usgovcloudapi.net/"; - - /// Azure Monitor endpoint for China cloud - pub const AZURE_MONITOR_CHINA: &str = "https://monitor.core.chinacloudapi.cn/"; + // Add more endpoints as needed } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs index be9d0e283..1dd7ad8a2 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/types.rs @@ -117,7 +117,7 @@ pub(crate) mod string_utils { return Err(MsiError::NullPointer); } - let c_str = CStr::from_ptr(ptr); + let c_str = unsafe { CStr::from_ptr(ptr) }; c_str .to_str() .map(|s| s.to_string()) From a7ae27267a0e4366c3a863eb73fb2ec73cdda41d Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 25 Jul 2025 21:56:50 +0000 Subject: [PATCH 3/4] fix --- .../geneva-uploader/Cargo.toml | 3 +- .../geneva-uploader/build.rs | 25 + .../geneva-uploader/src/msi/ffi.rs | 182 +++++-- .../geneva-uploader/src/msi/mod.rs | 455 ++++++++++++++++++ .../geneva-uploader/src/msi/native/bridge.cpp | 69 ++- .../geneva-uploader/src/msi/token_source.rs | 7 +- 6 files changed, 669 insertions(+), 72 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml b/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml index 44412f2c4..e20b16420 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml +++ b/opentelemetry-exporter-geneva/geneva-uploader/Cargo.toml @@ -40,7 +40,8 @@ lz4_flex = { version = "0.11" } criterion = {version = "0.6"} rand = {version = "0.9"} -# No build dependencies needed for the simplified MSI integration +[build-dependencies] +cc = "1.0" [lints] workspace = true diff --git a/opentelemetry-exporter-geneva/geneva-uploader/build.rs b/opentelemetry-exporter-geneva/geneva-uploader/build.rs index 66f00de56..cad7d9d40 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/build.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/build.rs @@ -68,6 +68,24 @@ fn check_msi_library() -> bool { // Extract library directory and name for linking if let (Some(lib_dir), Some(lib_name)) = (lib_path.parent(), lib_path.file_stem().and_then(|n| n.to_str())) { println!("cargo:warning=---> Library directory: {}", lib_dir.display()); + + // Determine XPlatLib include directory + let xplatlib_inc = env::var("XPLATLIB_INC_PATH") + .unwrap_or_else(|_| "/home/labhas/strato/Compute-Runtime-Tux/external/GenevaMonAgent-Shared-CrossPlat/src/XPlatLib/inc".to_string()); + + // Compile the C++ bridge file + let bridge_path = "src/msi/native/bridge.cpp"; + println!("cargo:warning=---> Compiling C++ bridge: {}", bridge_path); + println!("cargo:rerun-if-changed={}", bridge_path); + + cc::Build::new() + .cpp(true) + .file(bridge_path) + .include(&xplatlib_inc) + .flag("-std=c++17") + .flag("-fPIC") + .compile("msi_bridge"); + // Add library search path println!("cargo:rustc-link-search=native={}", lib_dir.display()); @@ -136,6 +154,13 @@ fn add_platform_libraries() { println!("cargo:rustc-link-lib=dl"); println!("cargo:rustc-link-lib=ssl"); println!("cargo:rustc-link-lib=crypto"); + // Additional libraries required by XPlatLib (cpprestsdk dependencies) + println!("cargo:rustc-link-lib=cpprest"); + println!("cargo:rustc-link-lib=boost_system"); + println!("cargo:rustc-link-lib=boost_thread"); + println!("cargo:rustc-link-lib=boost_atomic"); + println!("cargo:rustc-link-lib=boost_chrono"); + println!("cargo:rustc-link-lib=boost_regex"); } "macos" => { println!("cargo:rustc-link-lib=c++"); diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs index 761f73c8b..e4e6a91e8 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/ffi.rs @@ -16,7 +16,7 @@ pub type XPLATRESULT = c_long; #[cfg(not(target_os = "windows"))] pub type XPLATRESULT = c_int; -/// IMDS Endpoint Types +/// IMDS Endpoint Types (matching XPlatLib's ImdsEndpointType) #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImdsEndpointType { @@ -26,80 +26,203 @@ pub enum ImdsEndpointType { ArcEndpoint = 1, /// Standard Azure IMDS endpoint AzureEndpoint = 2, - /// AntMds endpoint for specific environments - AntMdsEndpoint = 3, } -#[cfg(feature = "msi_auth")] +// When native MSI library is available, use external functions from XPlatLib +#[cfg(all(feature = "msi_auth", msi_native_available))] unsafe extern "C" { - /// Error constants (these will be linked from the C++ library) + /// Error constants from XPlatErrors.h pub static XPLAT_NO_ERROR: XPLATRESULT; pub static XPLAT_FAIL: XPLATRESULT; pub static XPLAT_INITIALIZED: XPLATRESULT; pub static XPLAT_INITIALIZATION_FAILED: XPLATRESULT; pub static XPLAT_AZURE_MSI_FAILED: XPLATRESULT; pub static XPLAT_ARC_MSI_FAILED: XPLATRESULT; - pub static XPLAT_ANTMDS_MSI_FAILED: XPLATRESULT; pub static XPLAT_IMDS_ENDPOINT_ERROR: XPLATRESULT; - /// Simple wrapper for getting MSI access token - pub fn rust_get_msi_access_token( + /// Direct function from IMSIToken.h - GetMSIAccessToken + pub fn GetMSIAccessToken( resource: *const c_char, managed_id_identifier: *const c_char, managed_id_value: *const c_char, - is_ant_mds: bool, token: *mut *mut c_char, ) -> XPLATRESULT; - /// Create MSI Token Source - pub fn rust_create_imsi_token_source() -> *mut c_void; + /// Factory function from IMSIToken.h - CreateIMSITokenSource + pub fn CreateIMSITokenSource() -> *mut c_void; + + /// Factory function from IMSIToken.h - CreateIMSITokenInfo + pub fn CreateIMSITokenInfo() -> *mut c_void; +} - /// Initialize MSI Token Source - pub fn rust_imsi_token_source_initialize( +// C++ class method wrappers that will be implemented in bridge.cpp +#[cfg(all(feature = "msi_auth", msi_native_available))] +unsafe extern "C" { + /// Initialize MSI Token Source (wrapper for IMSITokenSource::Initialize) + pub fn imsi_token_source_initialize( token_source: *mut c_void, resource: *const c_char, managed_id_identifier: *const c_char, managed_id_value: *const c_char, fallback_to_default: bool, - is_ant_mds: bool, ) -> XPLATRESULT; - /// Get Access Token - pub fn rust_imsi_token_source_get_access_token( + /// Get Access Token (wrapper for IMSITokenSource::GetAccessToken) + pub fn imsi_token_source_get_access_token( token_source: *mut c_void, - force_refresh: bool, access_token: *mut *mut c_char, + force_refresh: bool, ) -> XPLATRESULT; - /// Get Expires On Seconds - pub fn rust_imsi_token_source_get_expires_on_seconds( + /// Get Expires On Seconds (wrapper for IMSITokenSource::GetExpiresOnSeconds) + pub fn imsi_token_source_get_expires_on_seconds( token_source: *mut c_void, expires_on_seconds: *mut c_long, ) -> XPLATRESULT; - /// Set IMDS Host Address - pub fn rust_imsi_token_source_set_imds_host_address( + /// Set IMDS Host Address (wrapper for IMSITokenSource::SetImdsHostAddress) + pub fn imsi_token_source_set_imds_host_address( token_source: *mut c_void, host_address: *const c_char, endpoint_type: c_int, ) -> XPLATRESULT; - /// Get IMDS Host Address - pub fn rust_imsi_token_source_get_imds_host_address( + /// Get IMDS Host Address (wrapper for IMSITokenSource::GetImdsHostAddress) + pub fn imsi_token_source_get_imds_host_address( token_source: *mut c_void, host_address: *mut *mut c_char, ) -> XPLATRESULT; - /// Stop Token Source - pub fn rust_imsi_token_source_stop(token_source: *mut c_void); + /// Stop Token Source (wrapper for IMSITokenSource::Stop) + pub fn imsi_token_source_stop(token_source: *mut c_void); - /// Destroy Token Source - pub fn rust_destroy_imsi_token_source(token_source: *mut c_void); + /// Destroy Token Source (wrapper for delete operator) + pub fn imsi_token_source_destroy(token_source: *mut c_void); - /// Free string allocated by the library - pub fn rust_free_string(str: *mut c_char); + /// Free string allocated by XPlatLib (using delete[]) + pub fn xplat_free_string(str: *mut c_char); } +// When MSI feature is enabled but native library is not available, provide stub implementations +#[cfg(all(feature = "msi_auth", not(msi_native_available)))] +mod stub_implementations { + use super::*; + use std::ptr; + + // Error constants (stub implementations) + pub static XPLAT_NO_ERROR: XPLATRESULT = 0; + pub static XPLAT_FAIL: XPLATRESULT = -1; + pub static XPLAT_INITIALIZED: XPLATRESULT = 1; + pub static XPLAT_INITIALIZATION_FAILED: XPLATRESULT = -2; + pub static XPLAT_AZURE_MSI_FAILED: XPLATRESULT = -3; + pub static XPLAT_ARC_MSI_FAILED: XPLATRESULT = -4; + pub static XPLAT_ANTMDS_MSI_FAILED: XPLATRESULT = -5; + pub static XPLAT_IMDS_ENDPOINT_ERROR: XPLATRESULT = -6; + + /// Stub implementation for getting MSI access token + #[no_mangle] + pub unsafe extern "C" fn rust_get_msi_access_token( + _resource: *const c_char, + _managed_id_identifier: *const c_char, + _managed_id_value: *const c_char, + _is_ant_mds: bool, + token: *mut *mut c_char, + ) -> XPLATRESULT { + if !token.is_null() { + *token = ptr::null_mut(); + } + XPLAT_AZURE_MSI_FAILED + } + + /// Stub implementation for creating MSI Token Source + #[no_mangle] + pub unsafe extern "C" fn rust_create_imsi_token_source() -> *mut c_void { + ptr::null_mut() + } + + /// Stub implementation for initializing MSI Token Source + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_initialize( + _token_source: *mut c_void, + _resource: *const c_char, + _managed_id_identifier: *const c_char, + _managed_id_value: *const c_char, + _fallback_to_default: bool, + _is_ant_mds: bool, + ) -> XPLATRESULT { + XPLAT_INITIALIZATION_FAILED + } + + /// Stub implementation for getting access token + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_get_access_token( + _token_source: *mut c_void, + _force_refresh: bool, + access_token: *mut *mut c_char, + ) -> XPLATRESULT { + if !access_token.is_null() { + *access_token = ptr::null_mut(); + } + XPLAT_AZURE_MSI_FAILED + } + + /// Stub implementation for getting expires on seconds + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_get_expires_on_seconds( + _token_source: *mut c_void, + expires_on_seconds: *mut c_long, + ) -> XPLATRESULT { + if !expires_on_seconds.is_null() { + *expires_on_seconds = 0; + } + XPLAT_FAIL + } + + /// Stub implementation for setting IMDS host address + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_set_imds_host_address( + _token_source: *mut c_void, + _host_address: *const c_char, + _endpoint_type: c_int, + ) -> XPLATRESULT { + XPLAT_FAIL + } + + /// Stub implementation for getting IMDS host address + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_get_imds_host_address( + _token_source: *mut c_void, + host_address: *mut *mut c_char, + ) -> XPLATRESULT { + if !host_address.is_null() { + *host_address = ptr::null_mut(); + } + XPLAT_FAIL + } + + /// Stub implementation for stopping token source + #[no_mangle] + pub unsafe extern "C" fn rust_imsi_token_source_stop(_token_source: *mut c_void) { + // No-op for stub + } + + /// Stub implementation for destroying token source + #[no_mangle] + pub unsafe extern "C" fn rust_destroy_imsi_token_source(_token_source: *mut c_void) { + // No-op for stub + } + + /// Stub implementation for freeing string + #[no_mangle] + pub unsafe extern "C" fn rust_free_string(_str: *mut c_char) { + // No-op for stub (since we never allocate strings) + } +} + +// Re-export stub constants when native library is not available +#[cfg(all(feature = "msi_auth", not(msi_native_available)))] +pub use stub_implementations::*; + #[cfg(test)] mod tests { use super::*; @@ -110,7 +233,6 @@ mod tests { assert_eq!(ImdsEndpointType::CustomEndpoint as c_int, 0); assert_eq!(ImdsEndpointType::ArcEndpoint as c_int, 1); assert_eq!(ImdsEndpointType::AzureEndpoint as c_int, 2); - assert_eq!(ImdsEndpointType::AntMdsEndpoint as c_int, 3); } #[test] diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs index b006e0701..39798966e 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/mod.rs @@ -26,3 +26,458 @@ pub mod resources { pub const AZURE_MONITOR_PUBLIC: &str = "https://monitor.core.windows.net/"; // Add more endpoints as needed } + +#[cfg(test)] +#[cfg(feature = "msi_auth")] +mod tests { + use super::*; + + /// Test MSI authentication against real Azure endpoints + /// + /// This test is ignored by default and requires environment variables to be set: + /// - TEST_MSI_OBJECT_ID: Object ID of the managed identity + /// - TEST_MSI_CLIENT_ID: Client ID of the managed identity (optional, alternative to object ID) + /// - TEST_MSI_RESOURCE_ID: Resource ID of the managed identity (optional, alternative to object/client ID) + /// + /// Run with: cargo test --features msi_auth test_real_msi_authentication -- --ignored + /// + /// Note: This test only works when running on Azure infrastructure where the managed identity is assigned + #[tokio::test] + #[ignore = "Requires real Azure environment and managed identity setup"] + async fn test_real_msi_authentication() { + use std::env; + + // Try to get identity configuration from environment variables + let identity = if let Ok(object_id) = env::var("TEST_MSI_OBJECT_ID") { + Some(ManagedIdentity::ObjectId(object_id)) + } else if let Ok(client_id) = env::var("TEST_MSI_CLIENT_ID") { + Some(ManagedIdentity::ClientId(client_id)) + } else if let Ok(resource_id) = env::var("TEST_MSI_RESOURCE_ID") { + Some(ManagedIdentity::ResourceId(resource_id)) + } else { + // Test with system-assigned identity if no user-assigned identity is specified + None + }; + + println!("Testing MSI authentication with identity: {:?}", identity); + + // Test getting MSI token for Azure Monitor + let result = get_msi_access_token( + resources::AZURE_MONITOR_PUBLIC, + identity.as_ref(), + false, // not AntMds + ); + + match result { + Ok(token) => { + println!("✅ Successfully obtained MSI token"); + println!("Token prefix: {}...", &token[..std::cmp::min(20, token.len())]); + + // Validate that we got a JWT token (should have 3 parts separated by dots) + let parts: Vec<&str> = token.split('.').collect(); + assert_eq!(parts.len(), 3, "Token should be a valid JWT with 3 parts"); + + // Validate token is not empty + assert!(!token.is_empty(), "Token should not be empty"); + assert!(token.len() > 100, "Token should be substantial length"); + } + Err(e) => { + panic!("❌ MSI authentication failed: {}", e); + } + } + } + + /// Test MSI authentication with explicit fallback behavior + /// + /// This test demonstrates how fallback from user-assigned to system-assigned identity works. + /// Requires environment variables: + /// - TEST_MSI_OBJECT_ID: Object ID of a user-assigned managed identity + /// + /// Run with: cargo test --features msi_auth test_msi_fallback_behavior -- --ignored + #[tokio::test] + #[ignore = "Requires real Azure environment and managed identity setup"] + async fn test_msi_fallback_behavior() { + use std::env; + + let object_id = env::var("TEST_MSI_OBJECT_ID") + .expect("TEST_MSI_OBJECT_ID environment variable must be set for this test"); + + let user_assigned_identity = ManagedIdentity::ObjectId(object_id); + + println!("Testing user-assigned identity: {:?}", user_assigned_identity); + + // Test with user-assigned identity + let result = get_msi_access_token( + resources::AZURE_MONITOR_PUBLIC, + Some(&user_assigned_identity), + false, + ); + + match result { + Ok(token) => { + println!("✅ User-assigned identity authentication successful"); + println!("Token prefix: {}...", &token[..std::cmp::min(20, token.len())]); + } + Err(e) => { + println!("⚠️ User-assigned identity failed: {}", e); + + // Try fallback to system-assigned identity + println!("Testing fallback to system-assigned identity..."); + let fallback_result = get_msi_access_token( + resources::AZURE_MONITOR_PUBLIC, + None, // system-assigned + false, + ); + + match fallback_result { + Ok(token) => { + println!("✅ Fallback to system-assigned identity successful"); + println!("Token prefix: {}...", &token[..std::cmp::min(20, token.len())]); + } + Err(fallback_e) => { + panic!("❌ Both user-assigned and system-assigned identity failed. User-assigned error: {}, System-assigned error: {}", e, fallback_e); + } + } + } + } + } + + /// Test MSI token validation and parsing + /// + /// This test validates that MSI tokens have the expected JWT structure + /// and contain the necessary claims. + /// + /// Run with: cargo test --features msi_auth test_msi_token_validation -- --ignored + #[tokio::test] + #[ignore = "Requires real Azure environment and managed identity setup"] + async fn test_msi_token_validation() { + use std::env; + + // Use any available identity for this test + let identity = if let Ok(object_id) = env::var("TEST_MSI_OBJECT_ID") { + Some(ManagedIdentity::ObjectId(object_id)) + } else { + None // system-assigned + }; + + let token = get_msi_access_token( + resources::AZURE_MONITOR_PUBLIC, + identity.as_ref(), + false, + ).expect("Failed to get MSI token"); + + // Validate JWT structure + let parts: Vec<&str> = token.split('.').collect(); + assert_eq!(parts.len(), 3, "Token should have 3 parts (header.payload.signature)"); + + // Validate each part is base64-encoded and not empty + assert!(!parts[0].is_empty(), "JWT header should not be empty"); + assert!(!parts[1].is_empty(), "JWT payload should not be empty"); + assert!(!parts[2].is_empty(), "JWT signature should not be empty"); + + // Decode and validate payload contains expected claims + use base64::{engine::general_purpose, Engine as _}; + + let payload = parts[1]; + // Add padding if necessary + let payload_padded = match payload.len() % 4 { + 0 => payload.to_string(), + 2 => format!("{payload}=="), + 3 => format!("{payload}="), + _ => payload.to_string(), + }; + + let decoded = general_purpose::URL_SAFE_NO_PAD + .decode(&payload_padded) + .expect("Failed to decode JWT payload"); + + let payload_str = String::from_utf8(decoded) + .expect("JWT payload should be valid UTF-8"); + + let payload_json: serde_json::Value = serde_json::from_str(&payload_str) + .expect("JWT payload should be valid JSON"); + + // Validate required claims + assert!(payload_json.get("aud").is_some(), "Token should have 'aud' (audience) claim"); + assert!(payload_json.get("iss").is_some(), "Token should have 'iss' (issuer) claim"); + assert!(payload_json.get("exp").is_some(), "Token should have 'exp' (expiration) claim"); + + // Validate audience matches Azure Monitor + let audience = payload_json["aud"].as_str() + .expect("Audience claim should be a string"); + assert!( + audience.contains("monitor.core.windows.net") || audience.contains("monitor.azure.com"), + "Audience should be for Azure Monitor service, got: {}", audience + ); + + println!("✅ Token validation successful"); + println!("Audience: {}", audience); + println!("Issuer: {}", payload_json["iss"].as_str().unwrap_or("N/A")); + } + + /// Test MSI authentication against real Geneva Config Service (GCS) + /// + /// This test validates end-to-end MSI authentication with Geneva Config Service + /// to retrieve ingestion gateway information, similar to certificate-based tests. + /// + /// Required environment variables: + /// - GENEVA_ENDPOINT: Geneva Config Service endpoint URL + /// - GENEVA_ENVIRONMENT: Environment name (e.g., "production", "dev") + /// - GENEVA_ACCOUNT: Geneva account name + /// - GENEVA_NAMESPACE: Service namespace + /// - GENEVA_REGION: Azure region (e.g., "westus2") + /// - GENEVA_CONFIG_MAJOR_VERSION: Configuration major version (e.g., "1") + /// - TEST_MSI_OBJECT_ID: Object ID of managed identity (optional) + /// - TEST_MSI_CLIENT_ID: Client ID of managed identity (optional, alternative to object ID) + /// - TEST_MSI_RESOURCE_ID: Resource ID of managed identity (optional, alternative to object/client ID) + /// + /// Run with: cargo test --features msi_auth test_gcs_msi_authentication -- --ignored + /// + /// Note: Must run on Azure infrastructure where the managed identity is assigned + #[tokio::test] + #[ignore = "Requires real Azure environment and Geneva Config Service access"] + async fn test_gcs_msi_authentication() { + use std::env; + use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig, MsiIdentityType}; + + // Read Geneva configuration from environment variables + let endpoint = env::var("GENEVA_ENDPOINT") + .expect("GENEVA_ENDPOINT environment variable must be set"); + let environment = env::var("GENEVA_ENVIRONMENT") + .expect("GENEVA_ENVIRONMENT environment variable must be set"); + let account = env::var("GENEVA_ACCOUNT") + .expect("GENEVA_ACCOUNT environment variable must be set"); + let namespace = env::var("GENEVA_NAMESPACE") + .expect("GENEVA_NAMESPACE environment variable must be set"); + let region = env::var("GENEVA_REGION") + .expect("GENEVA_REGION environment variable must be set"); + let config_major_version = env::var("GENEVA_CONFIG_MAJOR_VERSION") + .expect("GENEVA_CONFIG_MAJOR_VERSION environment variable must be set") + .parse::() + .expect("GENEVA_CONFIG_MAJOR_VERSION must be a valid unsigned integer"); + + // Determine which identity to use based on environment variables + let auth_method = if let Ok(object_id) = env::var("TEST_MSI_OBJECT_ID") { + println!("Using user-assigned managed identity with Object ID: {}", object_id); + AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ObjectId(object_id)), + fallback_to_default: true, + } + } else if let Ok(client_id) = env::var("TEST_MSI_CLIENT_ID") { + println!("Using user-assigned managed identity with Client ID: {}", client_id); + AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ClientId(client_id)), + fallback_to_default: true, + } + } else if let Ok(resource_id) = env::var("TEST_MSI_RESOURCE_ID") { + println!("Using user-assigned managed identity with Resource ID: {}", resource_id); + AuthMethod::ManagedIdentity { + identity: Some(MsiIdentityType::ResourceId(resource_id)), + fallback_to_default: true, + } + } else { + println!("Using system-assigned managed identity (no user-assigned identity specified)"); + AuthMethod::ManagedIdentity { + identity: None, + fallback_to_default: false, + } + }; + + let config = GenevaConfigClientConfig { + endpoint, + environment, + account, + namespace, + region, + config_major_version, + auth_method, + }; + + println!("Connecting to Geneva Config Service with MSI authentication..."); + let client = GenevaConfigClient::new(config) + .expect("Failed to create Geneva Config client with MSI authentication"); + + println!("Fetching ingestion info from GCS..."); + let (ingestion_info, moniker_info, token_endpoint) = client + .get_ingestion_info() + .await + .expect("Failed to get ingestion info from GCS using MSI authentication"); + + // Validate the response contains expected fields + assert!( + !ingestion_info.endpoint.is_empty(), + "Ingestion endpoint should not be empty" + ); + assert!( + !ingestion_info.auth_token.is_empty(), + "Auth token should not be empty" + ); + assert!( + !moniker_info.name.is_empty(), + "Moniker name should not be empty" + ); + assert!( + !moniker_info.account_group.is_empty(), + "Moniker account group should not be empty" + ); + assert!( + !token_endpoint.is_empty(), + "Token endpoint should not be empty" + ); + + // Validate that we got a proper JWT token + let token_parts: Vec<&str> = ingestion_info.auth_token.split('.').collect(); + assert_eq!( + token_parts.len(), + 3, + "Auth token should be a valid JWT with 3 parts" + ); + + // Validate moniker is a diagnostic moniker (should contain "diag") + assert!( + moniker_info.name.contains("diag"), + "Moniker name should contain 'diag', got: {}", + moniker_info.name + ); + + println!("✅ Successfully authenticated with Geneva Config Service using MSI"); + println!("Ingestion endpoint: {}", ingestion_info.endpoint); + println!("Token endpoint: {}", token_endpoint); + println!("Auth token length: {}", ingestion_info.auth_token.len()); + println!("Auth token expiry: {}", ingestion_info.auth_token_expiry_time); + println!("Moniker name: {}", moniker_info.name); + println!("Moniker account group: {}", moniker_info.account_group); + } + + /// Test MSI vs Certificate authentication comparison + /// + /// This test compares the results of MSI authentication vs certificate authentication + /// to ensure they provide equivalent access to Geneva Config Service. + /// + /// Required environment variables (same as previous tests): + /// - All Geneva Config variables (GENEVA_ENDPOINT, etc.) + /// - Certificate variables: GENEVA_CERT_PATH, GENEVA_CERT_PASSWORD + /// - MSI identity variables: TEST_MSI_OBJECT_ID (or alternatives) + /// + /// Run with: cargo test --features msi_auth test_msi_vs_certificate_authentication -- --ignored + #[tokio::test] + #[ignore = "Requires real Azure environment with both certificate and MSI access"] + async fn test_msi_vs_certificate_authentication() { + use std::env; + use std::path::PathBuf; + use crate::config_service::client::{AuthMethod, GenevaConfigClient, GenevaConfigClientConfig, MsiIdentityType}; + + // Read common Geneva configuration + let endpoint = env::var("GENEVA_ENDPOINT") + .expect("GENEVA_ENDPOINT environment variable must be set"); + let environment = env::var("GENEVA_ENVIRONMENT") + .expect("GENEVA_ENVIRONMENT environment variable must be set"); + let account = env::var("GENEVA_ACCOUNT") + .expect("GENEVA_ACCOUNT environment variable must be set"); + let namespace = env::var("GENEVA_NAMESPACE") + .expect("GENEVA_NAMESPACE environment variable must be set"); + let region = env::var("GENEVA_REGION") + .expect("GENEVA_REGION environment variable must be set"); + let config_major_version = env::var("GENEVA_CONFIG_MAJOR_VERSION") + .expect("GENEVA_CONFIG_MAJOR_VERSION environment variable must be set") + .parse::() + .expect("GENEVA_CONFIG_MAJOR_VERSION must be a valid unsigned integer"); + + // Test 1: Certificate authentication + println!("🔑 Testing certificate authentication..."); + let cert_path = env::var("GENEVA_CERT_PATH") + .expect("GENEVA_CERT_PATH environment variable must be set for comparison test"); + let cert_password = env::var("GENEVA_CERT_PASSWORD") + .expect("GENEVA_CERT_PASSWORD environment variable must be set for comparison test"); + + let cert_config = GenevaConfigClientConfig { + endpoint: endpoint.clone(), + environment: environment.clone(), + account: account.clone(), + namespace: namespace.clone(), + region: region.clone(), + config_major_version, + auth_method: AuthMethod::Certificate { + path: PathBuf::from(cert_path), + password: cert_password, + }, + }; + + let cert_client = GenevaConfigClient::new(cert_config) + .expect("Failed to create certificate-based client"); + + let (cert_ingestion_info, cert_moniker_info, cert_token_endpoint) = cert_client + .get_ingestion_info() + .await + .expect("Failed to get ingestion info using certificate authentication"); + + // Test 2: MSI authentication + println!("🔐 Testing MSI authentication..."); + let msi_identity = if let Ok(object_id) = env::var("TEST_MSI_OBJECT_ID") { + Some(MsiIdentityType::ObjectId(object_id)) + } else if let Ok(client_id) = env::var("TEST_MSI_CLIENT_ID") { + Some(MsiIdentityType::ClientId(client_id)) + } else if let Ok(resource_id) = env::var("TEST_MSI_RESOURCE_ID") { + Some(MsiIdentityType::ResourceId(resource_id)) + } else { + None // system-assigned + }; + + let msi_config = GenevaConfigClientConfig { + endpoint, + environment, + account, + namespace, + region, + config_major_version, + auth_method: AuthMethod::ManagedIdentity { + identity: msi_identity, + fallback_to_default: true, + }, + }; + + let msi_client = GenevaConfigClient::new(msi_config) + .expect("Failed to create MSI-based client"); + + let (msi_ingestion_info, msi_moniker_info, msi_token_endpoint) = msi_client + .get_ingestion_info() + .await + .expect("Failed to get ingestion info using MSI authentication"); + + // Test 3: Compare results + println!("🔍 Comparing authentication results..."); + + // Both should provide access to the same Geneva account/namespace + assert_eq!( + cert_moniker_info.name, + msi_moniker_info.name, + "Certificate and MSI should access the same moniker" + ); + assert_eq!( + cert_moniker_info.account_group, + msi_moniker_info.account_group, + "Certificate and MSI should access the same account group" + ); + + // Token endpoints should be the same (from JWT parsing) + assert_eq!( + cert_token_endpoint, + msi_token_endpoint, + "Certificate and MSI should provide tokens for the same endpoint" + ); + + // Both tokens should be valid JWTs + let cert_token_parts: Vec<&str> = cert_ingestion_info.auth_token.split('.').collect(); + let msi_token_parts: Vec<&str> = msi_ingestion_info.auth_token.split('.').collect(); + assert_eq!(cert_token_parts.len(), 3, "Certificate token should be valid JWT"); + assert_eq!(msi_token_parts.len(), 3, "MSI token should be valid JWT"); + + println!("✅ Both authentication methods provide equivalent access"); + println!("Certificate ingestion endpoint: {}", cert_ingestion_info.endpoint); + println!("MSI ingestion endpoint: {}", msi_ingestion_info.endpoint); + println!("Shared moniker: {}", cert_moniker_info.name); + println!("Shared token endpoint: {}", cert_token_endpoint); + println!("Certificate token length: {}", cert_ingestion_info.auth_token.len()); + println!("MSI token length: {}", msi_ingestion_info.auth_token.len()); + } +} diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp index a4886afb0..412e422bc 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/native/bridge.cpp @@ -1,36 +1,23 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +#include #include "IMSIToken.h" #include "StringUtils.h" #include "XPlatErrors.h" +#include "ImdsEndpointFetcher.h" extern "C" { -// Simple wrapper for the existing C function -XPLATRESULT rust_get_msi_access_token( - const char* resource, - const char* managed_id_identifier, - const char* managed_id_value, - bool is_ant_mds, - char** token -) { - return GetMSIAccessToken(resource, managed_id_identifier, managed_id_value, is_ant_mds, token); -} +// C++ wrapper functions for Rust FFI to XPlatLib integration -// Create MSI Token Source -void* rust_create_imsi_token_source() { - return CreateIMSITokenSource(); -} - -// Initialize MSI Token Source -XPLATRESULT rust_imsi_token_source_initialize( +// Initialize MSI Token Source (wrapper for IMSITokenSource::Initialize) +XPLATRESULT imsi_token_source_initialize( void* token_source, const char* resource, const char* managed_id_identifier, const char* managed_id_value, - bool fallback_to_default, - bool is_ant_mds + bool fallback_to_default ) { if (!token_source) return XPLAT_INITIALIZATION_FAILED; @@ -39,14 +26,14 @@ XPLATRESULT rust_imsi_token_source_initialize( xplat_string_t x_managed_id_identifier = XPlatUtils::string_to_string_t(std::string(managed_id_identifier ? managed_id_identifier : "")); xplat_string_t x_managed_id_value = XPlatUtils::string_to_string_t(std::string(managed_id_value ? managed_id_value : "")); - return source->Initialize(x_resource, x_managed_id_identifier, x_managed_id_value, fallback_to_default, is_ant_mds); + return source->Initialize(x_resource, x_managed_id_identifier, x_managed_id_value, fallback_to_default); } -// Get Access Token -XPLATRESULT rust_imsi_token_source_get_access_token( +// Get Access Token (wrapper for IMSITokenSource::GetAccessToken) +XPLATRESULT imsi_token_source_get_access_token( void* token_source, - bool force_refresh, - char** access_token + char** access_token, + bool force_refresh ) { if (!token_source || !access_token) return XPLAT_FAIL; @@ -65,8 +52,8 @@ XPLATRESULT rust_imsi_token_source_get_access_token( return result; } -// Get Expires On Seconds -XPLATRESULT rust_imsi_token_source_get_expires_on_seconds( +// Get Expires On Seconds (wrapper for IMSITokenSource::GetExpiresOnSeconds) +XPLATRESULT imsi_token_source_get_expires_on_seconds( void* token_source, long int* expires_on_seconds ) { @@ -76,8 +63,8 @@ XPLATRESULT rust_imsi_token_source_get_expires_on_seconds( return source->GetExpiresOnSeconds(*expires_on_seconds); } -// Set IMDS Host Address -XPLATRESULT rust_imsi_token_source_set_imds_host_address( +// Set IMDS Host Address (wrapper for IMSITokenSource::SetImdsHostAddress) +XPLATRESULT imsi_token_source_set_imds_host_address( void* token_source, const char* host_address, int endpoint_type @@ -86,13 +73,21 @@ XPLATRESULT rust_imsi_token_source_set_imds_host_address( auto* source = static_cast(token_source); xplat_string_t x_host_address = XPlatUtils::string_to_string_t(std::string(host_address)); - ImdsEndpointType x_endpoint_type = static_cast(endpoint_type); + + // Map from our enum values to XPlatLib enum values + ImdsEndpointType x_endpoint_type; + switch (endpoint_type) { + case 0: x_endpoint_type = ImdsEndpointType::Custom_Endpoint; break; + case 1: x_endpoint_type = ImdsEndpointType::ARC_Endpoint; break; + case 2: x_endpoint_type = ImdsEndpointType::Azure_Endpoint; break; + default: return XPLAT_FAIL; + } return source->SetImdsHostAddress(x_host_address, x_endpoint_type); } -// Get IMDS Host Address -XPLATRESULT rust_imsi_token_source_get_imds_host_address( +// Get IMDS Host Address (wrapper for IMSITokenSource::GetImdsHostAddress) +XPLATRESULT imsi_token_source_get_imds_host_address( void* token_source, char** host_address ) { @@ -108,23 +103,23 @@ XPLATRESULT rust_imsi_token_source_get_imds_host_address( return XPLAT_NO_ERROR; } -// Stop Token Source -void rust_imsi_token_source_stop(void* token_source) { +// Stop Token Source (wrapper for IMSITokenSource::Stop) +void imsi_token_source_stop(void* token_source) { if (token_source) { auto* source = static_cast(token_source); source->Stop(); } } -// Destroy Token Source -void rust_destroy_imsi_token_source(void* token_source) { +// Destroy Token Source (wrapper for delete operator) +void imsi_token_source_destroy(void* token_source) { if (token_source) { delete static_cast(token_source); } } -// Free string allocated by the library -void rust_free_string(char* str) { +// Free string allocated by XPlatLib (using delete[]) +void xplat_free_string(char* str) { if (str) { delete[] str; } diff --git a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs index ddf38b040..fe44b8eb7 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/src/msi/token_source.rs @@ -17,7 +17,7 @@ use crate::msi::types::{string_utils, ManagedIdentity}; pub fn get_msi_access_token( resource: &str, managed_identity: Option<&ManagedIdentity>, - is_ant_mds: bool, + _is_ant_mds: bool, ) -> MsiResult { let id_type = managed_identity.map(|id| id.identifier_type()).unwrap_or(""); let id_value = managed_identity.map(|id| id.identifier_value()).unwrap_or(""); @@ -33,11 +33,10 @@ pub fn get_msi_access_token( let mut token_ptr: *mut c_char = ptr::null_mut(); let result = unsafe { - ffi::rust_get_msi_access_token( + ffi::GetMSIAccessToken( _resource_ptr, _id_type_ptr, _id_value_ptr, - is_ant_mds, &mut token_ptr, ) }; @@ -50,7 +49,7 @@ pub fn get_msi_access_token( let token = unsafe { let result = string_utils::c_string_to_rust_string(token_ptr); - ffi::rust_free_string(token_ptr); + ffi::xplat_free_string(token_ptr); result }?; From 1e423741a6db10f12e73c5a9a26568ea883b07c6 Mon Sep 17 00:00:00 2001 From: Lalit Kumar Bhasin Date: Fri, 25 Jul 2025 22:29:19 +0000 Subject: [PATCH 4/4] fox --- opentelemetry-exporter-geneva/geneva-uploader/build.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/opentelemetry-exporter-geneva/geneva-uploader/build.rs b/opentelemetry-exporter-geneva/geneva-uploader/build.rs index cad7d9d40..4a801b140 100644 --- a/opentelemetry-exporter-geneva/geneva-uploader/build.rs +++ b/opentelemetry-exporter-geneva/geneva-uploader/build.rs @@ -154,13 +154,7 @@ fn add_platform_libraries() { println!("cargo:rustc-link-lib=dl"); println!("cargo:rustc-link-lib=ssl"); println!("cargo:rustc-link-lib=crypto"); - // Additional libraries required by XPlatLib (cpprestsdk dependencies) - println!("cargo:rustc-link-lib=cpprest"); - println!("cargo:rustc-link-lib=boost_system"); - println!("cargo:rustc-link-lib=boost_thread"); - println!("cargo:rustc-link-lib=boost_atomic"); - println!("cargo:rustc-link-lib=boost_chrono"); - println!("cargo:rustc-link-lib=boost_regex"); + // Only link system libraries - XPlatLib is static and contains its dependencies } "macos" => { println!("cargo:rustc-link-lib=c++");