diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cf465d3..c66d806 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -39,7 +39,7 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - name: Run tests + - name: Run unit tests run: cargo test --bins build: runs-on: ubuntu-latest diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1fe19d7..2eeba49 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,20 +35,67 @@ jobs: template: bicep/main.bicep deploymentName: ${{ env.DEPLOYMENT_NAME }} - test: - name: Tests + tests-no-access: + name: Tests with No Access needs: bicep runs-on: ubuntu-latest environment: test - env: - AZURE_CLIENT_ID_NO_ACCESS: ${{ secrets.AZURE_CLIENT_ID_NO_ACCESS }} - AZURE_CLIENT_ID_GET: ${{ secrets.AZURE_CLIENT_ID_GET }} - AZURE_CLIENT_ID_LIST: ${{ secrets.AZURE_CLIENT_ID_LIST }} - AZURE_CLIENT_ID_GET_LIST: ${{ secrets.AZURE_CLIENT_ID_GET_LIST }} - AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID_NO_ACCESS }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Run all tests - run: cargo test --all + run: cargo test no_access + tests-get: + name: Tests with Get + needs: bicep + runs-on: ubuntu-latest + environment: test + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID_GET }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Run all tests + run: cargo test only_get + tests-list: + name: Tests with List + needs: bicep + runs-on: ubuntu-latest + environment: test + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID_LIST }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Run all tests + run: cargo test only_list + tests-get-list: + name: Tests with Get and List + needs: bicep + runs-on: ubuntu-latest + environment: test + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID_GET_LIST }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Run all tests + run: cargo test get_and_list_access diff --git a/bicep/modules/kv.bicep b/bicep/modules/kv.bicep index a0bd707..de4d025 100644 --- a/bicep/modules/kv.bicep +++ b/bicep/modules/kv.bicep @@ -8,7 +8,7 @@ var accessPolicies = [for identity in identities: { tenantId: tenant().tenantId objectId: identity.id permissions: { - secrets: contains(identity.name, 'get') && contains(identity.name, 'list') ? ['Get', 'List'] : contains(identity.name, 'get') ? ['Get'] : ['List'] + secrets: contains(identity.name, 'get') && contains(identity.name, 'list') ? ['Get', 'List'] : contains(identity.name, 'get') ? ['Get'] : contains(identity.name, 'list') ? ['List'] : [] } }] diff --git a/src/main.rs b/src/main.rs index 2b11a37..59fe640 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,16 @@ use anyhow::Result; +use azure_core::error::HttpError; use azure_identity::DefaultAzureCredential; use azure_security_keyvault::prelude::KeyVaultGetSecretsResponse; use azure_security_keyvault::KeyvaultClient; use clap::Parser; use futures::stream::StreamExt; -use paris::{info, log}; use paris::{error, Logger}; use std::fs::File; use std::io::Write; use std::sync::Arc; use tokio::sync::mpsc; use tokio::sync::Semaphore; -use azure_core::error::HttpError; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] @@ -32,21 +31,20 @@ struct Opts { async fn check_vault_dns(vault_name: &str) -> Result<()> { let vault_host = format!("{}.vault.azure.net", vault_name); - let lookup_result = { - tokio::net::lookup_host((vault_host.as_str(), 443)).await - }; + let lookup_result = { tokio::net::lookup_host((vault_host.as_str(), 443)).await }; match lookup_result { Ok(_) => Ok(()), Err(err) => { error!("DNS lookup failed for Key Vault: {}", vault_name); - info!("Please check that the Key Vault exists or that you have no connectivity issues."); + error!( + "Please check that the Key Vault exists or that you have no connectivity issues." + ); Err(err.into()) } } } - async fn fetch_secrets_from_key_vault( client: &KeyvaultClient, filter: Option<&str>, @@ -58,15 +56,24 @@ async fn fetch_secrets_from_key_vault( let page = match page { Ok(p) => p, Err(err) => { - log!("\n"); - error!("Failed to fetch secrets page: {}", err); + error!("\n"); + error!("Failed to fetch secrets."); let specific_error = err.downcast_ref::(); if let Some(specific_error) = specific_error { - // Check the contents of the specific error - if specific_error.error_message().unwrap().to_string().contains("does not have secrets list permission on key vault") { - info!("Make sure you have List permissions on the Key Vault."); - } else if specific_error.error_message().unwrap().to_string().contains("is not authorized and caller is not a trusted service") { - info!("Make sure you're on the Key Vaults Firewall allowlist."); + if specific_error + .error_message() + .unwrap() + .to_string() + .contains("does not have secrets list permission on key vault") + { + error!("Make sure you have List permissions on the Key Vault."); + } else if specific_error + .error_message() + .unwrap() + .to_string() + .contains("is not authorized and caller is not a trusted service") + { + error!("Make sure you're on the Key Vaults Firewall allowlist."); } } return Err(err.into()); @@ -79,7 +86,6 @@ async fn fetch_secrets_from_key_vault( Ok(secret_values) } - async fn fetch_secrets_from_page( client: &azure_security_keyvault::SecretClient, page: &KeyVaultGetSecretsResponse, @@ -136,9 +142,8 @@ async fn fetch_and_send_secret( let _ = tx.send((secret_id.clone(), bundle.value.clone())).await; (secret_id, bundle.value) } - Err(err) => { - error!("Error fetching secret: {}", err); - info!("Make sure you have Get permissions on the Key Vault."); + Err(_err) => { + error!("Error fetching secret. Make sure you have Get permissions on the Key Vault."); (secret_id, String::new()) } } diff --git a/tests/e2e.rs b/tests/e2e.rs index 0d45949..0b2332f 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,76 +1,47 @@ use assert_cmd::prelude::*; use assert_fs::prelude::*; use assert_fs::TempDir; -use std::process::Command; use predicates::prelude::*; use serial_test::serial; -use std::env; +use std::process::Command; static BINARY: &str = "keyweave"; static KEYVAULT: &str = "bvdbkeyweavetweukvt1"; static FIREWALL_KEYVAULT: &str = "bvdbkeyweavetweukvt2"; static NON_EXISTENT_KEYVAULT: &str = "bvdbkeyweavetweukvt3"; -fn azure_cli_login(client_id: String, tenant_id: String, subscription_id: String) -> Result<(), std::io::Error> { - println!("Executing 'az login' with client ID: {}", client_id); - let login_output = Command::new("az") - .arg("login") - .arg("--identity") - .arg("--username") - .arg(&client_id) - .arg("--tenant") - .arg(&tenant_id) - .output()?; - println!("Login output: {:?}", login_output); - println!("Executing 'az account set' with subscription ID: {}", subscription_id); - let account_output = Command::new("az") - .arg("account") - .arg("set") - .arg("--subscription") - .arg(&subscription_id) - .output()?; - println!("Account output: {:?}", account_output); - Ok(()) -} - -/// Test with no access policies - expected to fail. #[tokio::test] #[serial] async fn test_no_access_policies() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_NO_ACCESS").expect("Failed to get AZURE_CLIENT_ID_NO_ACCESS"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(KEYVAULT) - .arg("--output").arg(output_path.path()); - cmd.assert().failure().stderr(predicate::str::contains("Make sure you have List permissions on the Key Vault.")); + cmd.arg("--vault-name") + .arg(KEYVAULT) + .arg("--output") + .arg(output_path.path()); + cmd.assert().failure().stderr(predicate::str::contains( + "Make sure you have List permissions on the Key Vault.", + )); temp_dir.close().unwrap(); } -/// Test with only Get access policy - expected to fail. #[tokio::test] #[serial] async fn test_only_get_access_policy() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_GET").expect("Failed to get AZURE_CLIENT_ID_GET"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(KEYVAULT) - .arg("--output").arg(output_path.path()); - cmd.assert().failure().stderr(predicate::str::contains("Make sure you have List permissions on the Key Vault.")); + cmd.arg("--vault-name") + .arg(KEYVAULT) + .arg("--output") + .arg(output_path.path()); + cmd.assert().failure().stderr(predicate::str::contains( + "Make sure you have List permissions on the Key Vault.", + )); temp_dir.close().unwrap(); } @@ -79,19 +50,17 @@ async fn test_only_get_access_policy() { #[tokio::test] #[serial] async fn test_only_list_access_policy() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_LIST").expect("Failed to get AZURE_CLIENT_ID_LIST"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(KEYVAULT) - .arg("--output").arg(output_path.path()); - cmd.assert().success().stderr(predicate::str::contains("Make sure you have Get permissions on the Key Vault.")); + cmd.arg("--vault-name") + .arg(KEYVAULT) + .arg("--output") + .arg(output_path.path()); + cmd.assert().success().stderr(predicate::str::contains( + "Make sure you have Get permissions on the Key Vault.", + )); temp_dir.close().unwrap(); } @@ -100,23 +69,21 @@ async fn test_only_list_access_policy() { #[tokio::test] #[serial] async fn test_get_and_list_access_policies() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_GET_LIST").expect("Failed to get AZURE_CLIENT_ID_GET_LIST"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(KEYVAULT) - .arg("--output").arg(output_path.path()); + cmd.arg("--vault-name") + .arg(KEYVAULT) + .arg("--output") + .arg(output_path.path()); cmd.assert().success(); output_path.assert(predicate::path::is_file()); output_path.assert(predicate::str::contains("testSecret=testSecretValue")); - output_path.assert(predicate::str::contains("filterTestSecret=filterTestSecretValue")); + output_path.assert(predicate::str::contains( + "filterTestSecret=filterTestSecretValue", + )); temp_dir.close().unwrap(); } @@ -125,23 +92,22 @@ async fn test_get_and_list_access_policies() { #[tokio::test] #[serial] async fn test_get_and_list_access_policies_filter() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_GET_LIST").expect("Failed to get AZURE_CLIENT_ID_GET_LIST"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(KEYVAULT) - .arg("--output").arg(output_path.path()) - .arg("--filter").arg("filter"); + cmd.arg("--vault-name") + .arg(KEYVAULT) + .arg("--output") + .arg(output_path.path()) + .arg("--filter") + .arg("filter"); cmd.assert().success(); output_path.assert(predicate::path::is_file()); - output_path.assert(predicate::str::contains("filterTestSecret=filterTestSecretValue")); + output_path.assert(predicate::str::contains( + "filterTestSecret=filterTestSecretValue", + )); temp_dir.close().unwrap(); } @@ -150,19 +116,17 @@ async fn test_get_and_list_access_policies_filter() { #[tokio::test] #[serial] async fn test_get_and_list_access_policies_firewall() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_GET_LIST").expect("Failed to get AZURE_CLIENT_ID_GET_LIST"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(FIREWALL_KEYVAULT) - .arg("--output").arg(output_path.path()); - cmd.assert().failure().stderr(predicate::str::contains("Make sure you're on the Key Vaults Firewall allowlist.")); + cmd.arg("--vault-name") + .arg(FIREWALL_KEYVAULT) + .arg("--output") + .arg(output_path.path()); + cmd.assert().failure().stderr(predicate::str::contains( + "Make sure you're on the Key Vaults Firewall allowlist.", + )); temp_dir.close().unwrap(); } @@ -171,20 +135,17 @@ async fn test_get_and_list_access_policies_firewall() { #[tokio::test] #[serial] async fn test_get_and_list_access_policies_non_existent() { - azure_cli_login( - env::var("AZURE_CLIENT_ID_GET_LIST").expect("Failed to get AZURE_CLIENT_ID_GET_LIST"), - env::var("AZURE_TENANT_ID").expect("Failed to get AZURE_TENANT_ID"), - env::var("AZURE_SUBSCRIPTION_ID").expect("Failed to get AZURE_SUBSCRIPTION_ID"), - ).expect("Failed to log in to Azure CLI"); - let temp_dir = TempDir::new().unwrap(); let output_path = temp_dir.child(".env"); let mut cmd = Command::cargo_bin(BINARY).unwrap(); - cmd.arg("--vault-name").arg(NON_EXISTENT_KEYVAULT) - .arg("--output").arg(output_path.path()); - cmd.assert().failure().stderr(predicate::str::contains("Please check that the Key Vault exists or that you have no connectivity issues.")); + cmd.arg("--vault-name") + .arg(NON_EXISTENT_KEYVAULT) + .arg("--output") + .arg(output_path.path()); + cmd.assert().failure().stderr(predicate::str::contains( + "Please check that the Key Vault exists or that you have no connectivity issues.", + )); temp_dir.close().unwrap(); } -