use crate::error::{Error, Result};
use crate::ffi::loader::NativeLibrary;
use crate::ffi::protocol::*;
use secrecy::{ExposeSecret, SecretString};
use std::sync::Arc;
use zeroize::Zeroize;
pub(crate) struct SdkClient {
client_id: u64,
library: Arc<NativeLibrary>,
}
impl SdkClient {
pub fn init(
library: Arc<NativeLibrary>,
token: &SecretString,
integration_name: &str,
integration_version: &str,
) -> Result<Self> {
let params = InitClientParams {
service_account_token: SecretString::from(token.expose_secret().to_string()),
programming_language: "Rust".to_string(),
sdk_version: "0030201".to_string(),
integration_name: integration_name.to_string(),
integration_version: integration_version.to_string(),
request_library_name: "reqwest".to_string(),
request_library_version: "0.11".to_string(),
os: std::env::consts::OS.to_string(),
os_version: "0.0.0".to_string(),
architecture: std::env::consts::ARCH.to_string(),
};
let mut config_json = serde_json::to_string(¶ms)?;
let result = library.init_client(&config_json);
config_json.zeroize();
let client_id_str = result?;
let client_id = client_id_str.parse::<u64>().map_err(|e| Error::SdkError {
message: format!("invalid client_id '{client_id_str}': {e}"),
})?;
Ok(Self { client_id, library })
}
pub fn resolve_secret(&self, reference: &str) -> Result<SecretString> {
let params = ResolveSecretParams {
secret_reference: reference.to_string(),
};
let invocation = SdkInvocation::new(self.client_id, methods::SECRETS_RESOLVE, params);
let request_json = invocation.to_json()?;
let mut response_json = self.library.invoke_sync(&request_json)?;
let parse_result: std::result::Result<String, _> = serde_json::from_str(&response_json);
response_json.zeroize();
let mut secret = parse_result.map_err(|e| Error::SdkError {
message: format!("failed to parse secret response: {e}"),
})?;
let result = SecretString::from(secret.clone());
secret.zeroize();
Ok(result)
}
pub fn resolve_secrets_batch(&self, references: &[&str]) -> Result<Vec<SecretString>> {
if references.is_empty() {
return Ok(Vec::new());
}
let mut secrets = Vec::with_capacity(references.len());
for reference in references {
secrets.push(self.resolve_secret(reference)?);
}
Ok(secrets)
}
}
impl Drop for SdkClient {
fn drop(&mut self) {
#[cfg(feature = "tracing")]
tracing::debug!(client_id = self.client_id, "releasing SDK client");
self.library.release_client(&self.client_id.to_string());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ffi::loader::load_library;
fn get_test_token() -> Option<String> {
std::env::var("OP_SERVICE_ACCOUNT_TOKEN").ok()
}
fn get_test_secret_ref() -> Option<String> {
std::env::var("TEST_SECRET_REF").ok()
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_sdk_client_init_success() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
let client = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings",
"0.1.0",
)
.expect("should init client");
assert!(client.client_id > 0, "client_id should be positive");
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_sdk_client_init_with_invalid_token() {
let library = load_library().expect("should load library");
let result = SdkClient::init(
library,
&SecretString::from("invalid-token-12345".to_string()),
"test-bindings",
"0.1.0",
);
assert!(result.is_err(), "should fail with invalid token");
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN and TEST_SECRET_REF"]
fn test_sdk_client_resolve_secret() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let secret_ref = get_test_secret_ref().expect("TEST_SECRET_REF required");
let library = load_library().expect("should load library");
let client = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings",
"0.1.0",
)
.expect("should init client");
let secret = client
.resolve_secret(&secret_ref)
.expect("should resolve secret");
assert!(
!secret.expose_secret().is_empty(),
"secret should not be empty"
);
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_sdk_client_resolve_invalid_reference() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
let client = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings",
"0.1.0",
)
.expect("should init client");
let result = client.resolve_secret("op://nonexistent/vault/field");
assert!(result.is_err(), "should fail with invalid reference");
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN and TEST_SECRET_REF"]
fn test_sdk_client_resolve_secrets_batch() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let secret_ref = get_test_secret_ref().expect("TEST_SECRET_REF required");
let library = load_library().expect("should load library");
let client = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings",
"0.1.0",
)
.expect("should init client");
let secrets = client
.resolve_secrets_batch(&[&secret_ref, &secret_ref])
.expect("should resolve batch");
assert_eq!(secrets.len(), 2);
assert_eq!(secrets[0].expose_secret(), secrets[1].expose_secret());
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_sdk_client_resolve_secrets_batch_empty() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
let client = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings",
"0.1.0",
)
.expect("should init client");
let secrets = client
.resolve_secrets_batch(&[])
.expect("should handle empty batch");
assert!(secrets.is_empty());
}
#[test]
#[ignore = "requires OP_SERVICE_ACCOUNT_TOKEN"]
fn test_sdk_client_drop_releases_client() {
let token = get_test_token().expect("OP_SERVICE_ACCOUNT_TOKEN required");
let library = load_library().expect("should load library");
{
let _client = SdkClient::init(
library.clone(),
&SecretString::from(token.clone()),
"test-bindings",
"0.1.0",
)
.expect("should init client");
}
let _client2 = SdkClient::init(
library,
&SecretString::from(token),
"test-bindings-2",
"0.1.0",
)
.expect("should init another client after first was released");
}
}