Skip to content
Draft
67 changes: 55 additions & 12 deletions .github/workflows/publish.crates.rust.sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,38 @@ on:
workflow_dispatch:

jobs:
check-version:
name: Check version not already published
permissions:
contents: read
runs-on: ubuntu-latest
timeout-minutes: 5

defaults:
run:
working-directory: ./sdk/rust

steps:
- name: Get the source code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Verify version does not exist on crates.io
run: |
VERSION=$(grep -Po '^version\s*=\s*"\K[^"]*' Cargo.toml)
echo "Checking crates.io for keeper-secrets-manager-core v${VERSION}..."
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
"https://crates.io/api/v1/crates/keeper_secrets_manager_core/${VERSION}")
if [ "$STATUS" = "200" ]; then
echo "❌ Version ${VERSION} already exists on crates.io — bump the version before publishing"
exit 1
fi
echo "✅ Version ${VERSION} is new — proceeding with publish"

test-rust-sdk:
name: Test Rust SDK (${{ matrix.rust }})
needs: check-version
permissions:
contents: read
runs-on: ubuntu-latest
Expand All @@ -22,10 +52,12 @@ jobs:

steps:
- name: Get the source code
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
Expand Down Expand Up @@ -53,8 +85,13 @@ jobs:
cargo test --all-features --test integration_tests
cargo test --all-features --test notation_tests
cargo test --all-features --test payload_test
cargo test --all-features --test proxy_test
cargo test --all-features --test totp_test
cargo test --all-features --test update_secret_tests
cargo test --all-features --test caching_transmission_key_tests
cargo test --all-features --test download_file_by_title_tests
cargo test --all-features --test duplicate_uid_notation_test
cargo test --all-features --test empty_config_test

- name: Run caching tests (serial execution required)
run: cargo test --all-features --test caching_tests -- --test-threads=1
Expand All @@ -76,7 +113,9 @@ jobs:

steps:
- name: Get the source code
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Detect Rust SDK version
id: detect-version
Expand All @@ -92,7 +131,7 @@ jobs:
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
with:
toolchain: stable

Expand All @@ -113,7 +152,7 @@ jobs:
jq '.metadata.component.name' rust-sdk-sbom.json

- name: Publish SBOM to Manifest Cyber
uses: manifest-cyber/manifest-github-action@main
uses: manifest-cyber/manifest-github-action@9aa4e84c80e6d232c3f49506a17f0ff1151e6896 # main
with:
apiKey: ${{ secrets.MANIFEST_TOKEN }}
bomFilePath: ./sdk/rust/rust-sdk-sbom.json
Expand All @@ -122,7 +161,7 @@ jobs:
asset-labels: application,sbom-generated,rust,cargo,secrets-manager

- name: Archive SBOM
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: sbom-rust-sdk-${{ steps.detect-version.outputs.version }}
path: ./sdk/rust/rust-sdk-sbom.json
Expand All @@ -145,15 +184,17 @@ jobs:

steps:
- name: Get the source code
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
with:
toolchain: stable

- name: Authenticate with crates.io via OIDC
uses: rust-lang/crates-io-auth-action@v1
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1
id: auth

- name: Dry-run publish (validates package and authentication)
Expand All @@ -180,15 +221,17 @@ jobs:

steps:
- name: Get the source code
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
with:
toolchain: stable

- name: Authenticate with crates.io via OIDC
uses: rust-lang/crates-io-auth-action@v1
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1
id: auth

- name: Publish to crates.io
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/test.rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ jobs:

steps:
- name: Get the source code
uses: actions/checkout@v3
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
persist-credentials: false

- name: Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
Expand Down Expand Up @@ -67,6 +69,10 @@ jobs:
cargo test --all-features --test proxy_test
cargo test --all-features --test totp_test
cargo test --all-features --test update_secret_tests
cargo test --all-features --test caching_transmission_key_tests
cargo test --all-features --test download_file_by_title_tests
cargo test --all-features --test duplicate_uid_notation_test
cargo test --all-features --test empty_config_test

- name: Run caching tests (serial execution required)
run: cargo test --all-features --test caching_tests -- --test-threads=1
Expand Down
2 changes: 1 addition & 1 deletion sdk/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion sdk/rust/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "keeper-secrets-manager-core"
version = "17.1.0"
version = "17.2.0"
authors = ["Keeper Security <sm@keepersecurity.com>"]
edition = "2021"
rust-version = "1.87"
Expand Down
50 changes: 37 additions & 13 deletions sdk/rust/src/core/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,16 @@ pub struct SecretsManager {
pub cache: KSMCache,
pub proxy_url: Option<String>,
custom_post_function: Option<CustomPostFunction>,
/// Pre-built HTTP client shared across all operations (API calls + file downloads).
/// Built once during init to avoid constructing reqwest::blocking::Client inside
/// tokio::spawn_blocking, which fails due to nested runtime conflicts.
/// See: https://github.com/seanmonstar/reqwest/issues/1017
http_client: Option<reqwest::blocking::Client>,
}

impl Clone for SecretsManager {
fn clone(&self) -> Self {
SecretsManager {
// Clone each field of the struct
token: self.token.clone(),
hostname: self.hostname.clone(),
verify_ssl_certs: self.verify_ssl_certs,
Expand All @@ -209,6 +213,7 @@ impl Clone for SecretsManager {
cache: self.cache.clone(),
proxy_url: self.proxy_url.clone(),
custom_post_function: self.custom_post_function,
http_client: self.http_client.clone(),
}
}
}
Expand Down Expand Up @@ -251,10 +256,11 @@ impl SecretsManager {
hostname: String::new(),
verify_ssl_certs: false,
config: KvStoreType::None,
log_level: Level::Info, // Default to Info if not provided
cache: KSMCache::None, // Default is no cache
log_level: Level::Info,
cache: KSMCache::None,
proxy_url: client_options.proxy_url.clone(),
custom_post_function: client_options.custom_post_function,
http_client: None, // built after SSL/proxy config is resolved
};

let mut config = client_options.config;
Expand Down Expand Up @@ -389,10 +395,24 @@ impl SecretsManager {
}
secrets_manager.config = config.clone();

match secrets_manager._init() {
Ok(secrets_manager) => Ok(secrets_manager),
Err(e) => Err(e),
let mut sm = secrets_manager._init()?;

// Build a shared HTTP client once, outside any async runtime.
// Reused for API calls and file downloads. Avoids constructing
// reqwest::blocking::Client inside tokio::spawn_blocking which fails
// due to nested runtime conflicts (reqwest#1017).
let mut client_builder =
reqwest::blocking::Client::builder().danger_accept_invalid_certs(sm.verify_ssl_certs);
if let Some(proxy_url) = &sm.proxy_url {
if let Ok(proxy) = SecretsManager::build_proxy(proxy_url) {
client_builder = client_builder.proxy(proxy);
}
}
sm.http_client = Some(client_builder.build().map_err(|e| {
KSMRError::SecretManagerCreationError(format!("Failed to build HTTP client: {}", e))
})?);

Ok(sm)
}

fn _init(&mut self) -> Result<Self, KSMRError> {
Expand Down Expand Up @@ -1194,10 +1214,12 @@ impl SecretsManager {
let record_result =
Record::new_from_json(record_hashmap_parsed, &_secret_key, None);
if let Ok(mut unwrapped_record) = record_result {
if let Some(proxy) = &self.proxy_url {
for file in &mut unwrapped_record.files {
for file in &mut unwrapped_record.files {
if let Some(proxy) = &self.proxy_url {
file.proxy_url = Some(proxy.clone());
}
file.skip_ssl_verify = self.verify_ssl_certs;
file.http_client = self.http_client.clone();
}
records_count += 1;
records.push(unwrapped_record);
Expand All @@ -1219,11 +1241,13 @@ impl SecretsManager {
if let Some(unwrapped_folder) = folder_result {
shared_folders_count += 1;
let mut folder_records = unwrapped_folder.records()?;
if let Some(proxy) = &self.proxy_url {
for record in &mut folder_records {
for file in &mut record.files {
for record in &mut folder_records {
for file in &mut record.files {
if let Some(proxy) = &self.proxy_url {
file.proxy_url = Some(proxy.clone());
}
file.skip_ssl_verify = self.verify_ssl_certs;
file.http_client = self.http_client.clone();
}
}
records_count += folder_records.len();
Expand Down Expand Up @@ -1291,7 +1315,7 @@ impl SecretsManager {
Ok(secrets_manager_response)
}

fn fetch_and_decrypt_folders(mut self) -> Result<Vec<KeeperFolder>, KSMRError> {
fn fetch_and_decrypt_folders(&mut self) -> Result<Vec<KeeperFolder>, KSMRError> {
let payload = self
.clone()
.prepare_get_payload(self.config.clone(), None)?;
Expand Down Expand Up @@ -1462,7 +1486,7 @@ impl SecretsManager {
///
/// * `HTTPError` - If the API request fails
/// * `CryptoError` - If folder decryption fails
pub fn get_folders(self) -> Result<Vec<KeeperFolder>, KSMRError> {
pub fn get_folders(&mut self) -> Result<Vec<KeeperFolder>, KSMRError> {
let folders = self.fetch_and_decrypt_folders()?;
Ok(folders)
}
Expand Down
Loading
Loading