diff --git a/sdk/security_keyvault/examples/update_secret.rs b/sdk/security_keyvault/examples/update_secret.rs index 9091286465..5820c819ce 100644 --- a/sdk/security_keyvault/examples/update_secret.rs +++ b/sdk/security_keyvault/examples/update_secret.rs @@ -1,5 +1,5 @@ use azure_identity::token_credentials::{ClientSecretCredential, TokenCredentialOptions}; -use azure_security_keyvault::{KeyClient, RecoveryLevel}; +use azure_security_keyvault::KeyClient; use chrono::prelude::*; use chrono::Duration; use std::env; @@ -31,7 +31,7 @@ async fn main() -> Result<(), Box> { // Update secret recovery level to `Purgeable`. client - .update_secret_recovery_level(&secret_name, &secret_version, RecoveryLevel::Purgeable) + .update_secret_recovery_level(&secret_name, &secret_version, "Purgeable".into()) .await?; // Update secret to expire in two weeks. diff --git a/sdk/security_keyvault/src/certificate.rs b/sdk/security_keyvault/src/certificate.rs new file mode 100644 index 0000000000..5d4ca67833 --- /dev/null +++ b/sdk/security_keyvault/src/certificate.rs @@ -0,0 +1,683 @@ +use crate::client::API_VERSION_PARAM; +use crate::CertificateClient; +use crate::Error; + +use azure_core::TokenCredential; +use chrono::serde::{ts_seconds, ts_seconds_option}; +use chrono::{DateTime, Utc}; +use getset::Getters; +use reqwest::Url; +use serde::Deserialize; +use serde_json::{Map, Value}; + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultCertificateBaseIdentifierAttributedRaw { + enabled: bool, + #[serde(default)] + #[serde(with = "ts_seconds_option")] + exp: Option>, + #[serde(default)] + #[serde(with = "ts_seconds_option")] + nbf: Option>, + #[serde(with = "ts_seconds")] + created: DateTime, + #[serde(with = "ts_seconds")] + updated: DateTime, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultCertificateBaseIdentifierRaw { + id: String, + x5t: String, + attributes: KeyVaultCertificateBaseIdentifierAttributedRaw, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificatesResponse { + value: Vec, + #[serde(rename = "nextLink")] + next_link: Option, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponse { + kid: String, + sid: String, + x5t: String, + cer: String, + id: String, + attributes: KeyVaultGetCertificateResponseAttributes, + policy: KeyVaultGetCertificateResponsePolicy, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponseAttributes { + enabled: bool, + #[serde(default)] + #[serde(with = "ts_seconds_option")] + exp: Option>, + #[serde(default)] + #[serde(with = "ts_seconds_option")] + nbf: Option>, + #[serde(with = "ts_seconds")] + created: DateTime, + #[serde(with = "ts_seconds")] + updated: DateTime, + #[serde(rename = "recoveryLevel")] + recovery_level: String, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicy { + id: String, + key_props: KeyVaultGetCertificateResponsePolicyKeyProperties, + secret_props: KeyVaultGetCertificateResponsePolicySecretProperties, + x509_props: KeyVaultGetCertificateResponsePolicyX509Properties, + issuer: KeyVaultGetCertificateResponsePolicyIssuer, + attributes: KeyVaultGetCertificateResponsePolicyAttributes, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicyKeyProperties { + exportable: bool, + kty: String, + key_size: u64, + reuse_key: bool, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicySecretProperties { + #[serde(rename = "contentType")] + content_type: String, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicyX509Properties { + subject: String, + validity_months: u64, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicyIssuer { + name: String, +} +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultGetCertificateResponsePolicyAttributes { + enabled: bool, + #[serde(with = "ts_seconds")] + created: DateTime, + #[serde(with = "ts_seconds")] + updated: DateTime, +} + +#[derive(Deserialize, Debug)] +pub(crate) struct KeyVaultCertificateBackupResponseRaw { + value: String, +} + +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct KeyVaultCertificate { + key_id: String, + secret_id: String, + x5t: String, + cer: String, + content_type: String, + properties: CertificateProperties, +} + +#[derive(Debug, Getters)] +#[getset(get = "pub")] +pub struct CertificateProperties { + id: String, + name: String, + version: String, + not_before: Option>, + expires_on: Option>, + created_on: DateTime, + updated_on: DateTime, + enabled: bool, +} + +pub struct CertificateBackupResult { + pub backup: Vec, +} + +impl<'a, T: TokenCredential> CertificateClient<'a, T> { + /// Gets a certificate from the Key Vault. + /// Note that the latest version is fetched. For a specific version, use `get_certificate_with_version`. + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// let certificate = client.get_certificate(&"CERTIFICATE_NAME").await.unwrap(); + /// dbg!(&certificate); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn get_certificate(&mut self, name: &'a str) -> Result { + Ok(self.get_certificate_with_version(name, "").await?) + } + + /// Gets a certificate from the Key Vault with a specific version. + /// If you need the latest version, use `get_certificate`. + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// let certificate = client.get_certificate_with_version(&"CERTIFICATE_NAME", &"CERTIFICATE_VERSION").await.unwrap(); + /// dbg!(&certificate); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn get_certificate_with_version( + &mut self, + name: &'a str, + version: &'a str, + ) -> Result { + let mut uri = self.vault_url.clone(); + uri.set_path(&format!("certificates/{}/{}", name, version)); + uri.set_query(Some(API_VERSION_PARAM)); + + let response_body = self.get_authed(uri.to_string()).await?; + let response = serde_json::from_str::(&response_body) + .map_err(|error| Error::BackupCertificateParseError { + error, + certificate_name: name.to_string(), + response_body, + })?; + Ok(KeyVaultCertificate { + key_id: response.kid, + secret_id: response.sid, + x5t: response.x5t, + cer: response.cer, + content_type: response.policy.secret_props.content_type, + properties: CertificateProperties { + id: response.id, + name: name.to_string(), + version: version.to_string(), + enabled: response.attributes.enabled, + not_before: response.attributes.nbf, + expires_on: response.attributes.exp, + created_on: response.attributes.created, + updated_on: response.attributes.updated, + }, + }) + } + + /// Lists all the certificates in the Key Vault. + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// let certificates = client.list_properties_of_certificates().await.unwrap(); + /// dbg!(&certificates); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn list_properties_of_certificates( + &mut self, + ) -> Result, Error> { + let mut certificates = Vec::::new(); + + let mut uri = self.vault_url.clone(); + uri.set_path("certificates"); + uri.set_query(Some(API_VERSION_PARAM)); + + loop { + let resp_body = self.get_authed(uri.to_string()).await?; + let response = + serde_json::from_str::(&resp_body).unwrap(); + + certificates.extend( + response + .value + .into_iter() + .map(|s| CertificateProperties { + id: s.id.to_owned(), + name: s.id.split('/').collect::>()[4].to_string(), + version: s.id.split('/').collect::>()[5].to_string(), + enabled: s.attributes.enabled, + created_on: s.attributes.created, + updated_on: s.attributes.updated, + not_before: s.attributes.nbf, + expires_on: s.attributes.exp, + }) + .collect::>(), + ); + + match response.next_link { + None => break, + Some(u) => uri = Url::parse(&u).unwrap(), + } + } + + Ok(certificates) + } + + /// Gets all the versions for a certificate in the Key Vault. + // + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// let certificate_versions = client.list_properties_of_certificate_versions(&"CERTIFICATE_NAME").await.unwrap(); + /// dbg!(&certificate_versions); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn list_properties_of_certificate_versions( + &mut self, + name: &'a str, + ) -> Result, Error> { + let mut versions = Vec::::new(); + + let mut uri = self.vault_url.clone(); + uri.set_path(&format!("certificates/{}/versions", name)); + uri.set_query(Some(API_VERSION_PARAM)); + + loop { + let resp_body = self.get_authed(uri.to_string()).await?; + + let response = + serde_json::from_str::(&resp_body).unwrap(); + + versions.extend( + response + .value + .into_iter() + .map(|s| CertificateProperties { + id: s.id.to_owned(), + name: name.to_string(), + version: s.id.split('/').collect::>()[5].to_string(), + enabled: s.attributes.enabled, + created_on: s.attributes.created, + updated_on: s.attributes.updated, + not_before: s.attributes.nbf, + expires_on: s.attributes.exp, + }) + .collect::>(), + ); + match response.next_link { + None => break, + Some(u) => uri = Url::parse(&u).unwrap(), + } + } + + // Return the certificate versions sorted by the time modified in descending order. + versions.sort_by(|a, b| { + if a.updated_on > b.updated_on { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + } + }); + Ok(versions) + } + + pub async fn update_certificate_attributes( + &mut self, + properties: CertificateProperties, + ) -> Result<(), Error> { + let mut uri = self.vault_url.clone(); + uri.set_path(&format!( + "certificates/{}/{}", + properties.name, properties.version + )); + uri.set_query(Some(API_VERSION_PARAM)); + + let mut request_body = Map::new(); + request_body.insert( + "attributes".to_string(), + serde_json::json!({ + "enabled": properties.enabled, + "nbf": properties.not_before, + "exp": properties.expires_on, + }), + ); + + self.patch_authed(uri.to_string(), Value::Object(request_body).to_string()) + .await?; + + Ok(()) + } + + async fn _update_certificate_policy( + &mut self, + name: &'a str, + policy: Map, + ) -> Result<(), Error> { + let mut uri = self.vault_url.clone(); + uri.set_path(&format!("certificates/{}/policy", name)); + uri.set_query(Some(API_VERSION_PARAM)); + + self.patch_authed(uri.to_string(), Value::Object(policy).to_string()) + .await?; + + Ok(()) + } + + /// Restores a backed up certificate and all its versions. + /// This operation requires the certificates/restore permission. + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// client.restore_certificate(b"KUF6dXJlS2V5VmF1bHRTZWNyZXRCYWNrdXBWMS5taW").await.unwrap(); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn restore_certificate(&mut self, backup: &[u8]) -> Result<(), Error> { + let mut uri = self.vault_url.clone(); + uri.set_path("certificates/restore"); + uri.set_query(Some(API_VERSION_PARAM)); + + let mut request_body = Map::new(); + request_body.insert("value".to_owned(), Value::String(base64::encode(backup))); + + self.post_authed( + uri.to_string(), + Some(Value::Object(request_body).to_string()), + ) + .await?; + + Ok(()) + } + + /// Restores a backed up certificate and all its versions. + /// This operation requires the certificates/restore permission. + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// client.backup_certificate(&"CERTIFICATE_NAME").await.unwrap(); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn backup_certificate( + &mut self, + name: &'a str, + ) -> Result { + let mut uri = self.vault_url.clone(); + uri.set_path(&format!("certificates/{}/backup", name)); + uri.set_query(Some(API_VERSION_PARAM)); + + let response_body = self.post_authed(uri.to_string(), None).await?; + let backup_blob = serde_json::from_str::( + &response_body, + ) + .map_err(|error| Error::BackupCertificateParseError { + error, + certificate_name: name.to_string(), + response_body, + })?; + + Ok(CertificateBackupResult { + backup: base64::decode(backup_blob.value)?, + }) + } + + /// Deletes a certificate in the Key Vault. + /// + /// # Arguments + /// + /// * `name` - Name of the certificate + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// use tokio::runtime::Runtime; + /// + /// async fn example() { + /// let creds = DefaultAzureCredential::default(); + /// let mut client = CertificateClient::new( + /// &"KEYVAULT_URL", + /// &creds, + /// ).unwrap(); + /// client.delete_certificate(&"CERTIFICATE_NAME").await.unwrap(); + /// } + /// + /// Runtime::new().unwrap().block_on(example()); + /// ``` + pub async fn delete_certificate(&mut self, _name: &'a str) -> Result<(), Error> { + // let mut uri = self.vault_url.clone(); + // uri.set_path(&format!("certificates/{}", certificate_name)); + // uri.set_query(Some(API_VERSION_PARAM)); + + // self.delete_authed(uri.to_string()).await?; + + // Ok(()) + + todo!("See issue #174 at: https://github.com/Azure/azure-sdk-for-rust/issues/174.") + } +} + +#[cfg(test)] +#[allow(unused_must_use)] +mod tests { + use super::*; + + use chrono::{Duration, Utc}; + use mockito::{mock, Matcher}; + use serde_json::json; + + use crate::client::*; + use crate::mock_cert_client; + use crate::tests::MockCredential; + + fn diff(first: DateTime, second: DateTime) -> Duration { + if first > second { + first - second + } else { + second - first + } + } + + #[tokio::test] + async fn get_certificate() { + let time_created = Utc::now() - Duration::days(7); + let time_updated = Utc::now(); + let _m = mock("GET", "/certificates/test-certificate/") + .match_query(Matcher::UrlEncoded("api-version".into(), API_VERSION.into())) + .with_header("content-type", "application/json") + .with_body( + json!({ + "id": "https://test-keyvault.vault.azure.net/certificates/test-certificate/002ade539442463aba45c0efb42e3e84", + "x5t": "fLi3U52HunIVNXubkEnf8tP6Wbo", + "kid": "https://test-keyvault.vault.azure.net/keys/test-certificate/002ade539442463aba45c0efb42e3e84", + "sid": "https://test-keyvault.vault.azure.net/secrets/test-certificate/002ade539442463aba45c0efb42e3e84", + "cer": "MIICODCCAeagAwIBAgIQqHmpBAv+CY9IJFoUhlbziTAJBgUrDgMCHQUAMBYxFDASBgNVBAMTC1Jvb3QgQWdlbmN5MB4XDTE1MDQyOTIxNTM0MVoXDTM5MTIzMTIzNTk1OVowFzEVMBMGA1UEAxMMS2V5VmF1bHRUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5bVAT73zr4+N4WVv2+SvTunAw08ksS4BrJW/nNliz3S9XuzMBMXvmYzU5HJ8TtEgluBiZZYd5qsMJD+OXHSNbsLdmMhni0jYX09h3XlC2VJw2sGKeYF+xEaavXm337aZZaZyjrFBrrUl51UePaN+kVFXNlBb3N3TYpqa7KokXenJQuR+i9Gv9a77c0UsSsDSryxppYhKK7HvTZCpKrhVtulF5iPMswWe9np3uggfMamyIsK/0L7X9w9B2qN7993RR0A00nOk4H6CnkuwO77dSsD0KJsk6FyAoZBzRXDZh9+d9R76zCL506NcQy/jl0lCiQYwsUX73PG5pxOh02OwKwIDAQABo0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxMLUm9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwCQYFKw4DAh0FAANBAGqIjo2geVagzuzaZOe1ClGKhZeiCKfWAxklaGN+qlGUbVS4IN4V1lot3VKnzabasmkEHeNxPwLn1qvSD0cX9CE=", + "attributes": { + "enabled": true, + "created": time_created.timestamp(), + "updated": time_updated.timestamp(), + "recoveryLevel": "Recoverable+Purgeable" + }, + "policy": { + "id": "https://test-keyvault.vault.azure.net/certificates/selfSignedCert01/policy", + "key_props": { + "exportable": true, + "kty": "RSA", + "key_size": 2048, + "reuse_key": false + }, + "secret_props": { + "contentType": "application/x-pkcs12" + }, + "x509_props": { + "subject": "CN=KeyVaultTest", + "ekus": [], + "key_usage": [], + "validity_months": 297 + }, + "issuer": { + "name": "Unknown" + }, + "attributes": { + "enabled": true, + "created": 1493938289, + "updated": 1493938291 + } + } + }) + .to_string(), + ) + .with_status(200) + .create(); + + let creds = MockCredential; + dbg!(mockito::server_url()); + let mut client = mock_cert_client!(&"test-keyvault", &creds,); + + let certificate: KeyVaultCertificate = + client.get_certificate(&"test-certificate").await.unwrap(); + + assert_eq!( + "https://test-keyvault.vault.azure.net/keys/test-certificate/002ade539442463aba45c0efb42e3e84", + certificate.key_id() + ); + assert_eq!(true, *certificate.properties.enabled()); + assert!(diff(time_created, *certificate.properties.created_on()) < Duration::seconds(1)); + assert!(diff(time_updated, *certificate.properties.updated_on()) < Duration::seconds(1)); + } + + #[tokio::test] + async fn get_certificate_versions() { + let time_created_1 = Utc::now() - Duration::days(7); + let time_updated_1 = Utc::now(); + let time_created_2 = Utc::now() - Duration::days(9); + let time_updated_2 = Utc::now() - Duration::days(2); + + let _m1 = mock("GET", "/certificates/test-certificate/versions") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("api-version".into(), API_VERSION.into()), + ])) + .with_header("content-type", "application/json") + .with_body( + json!({ + "value": [{ + "id": "https://test-keyvault.vault.azure.net/certificates/test-certificate/VERSION_1", + "x5t": "fLi3U52HunIVNXubkEnf8tP6Wbo", + "attributes": { + "enabled": true, + "created": time_created_1.timestamp(), + "updated": time_updated_1.timestamp(), + } + }], + "nextLink": format!("{}/certificates/text-certificate/versions?api-version={}&maxresults=1&$skiptoken=SKIP_TOKEN_MOCK", mockito::server_url(), API_VERSION) + }) + .to_string(), + ) + .with_status(200) + .create(); + + let _m2 = mock("GET", "/certificates/text-certificate/versions") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("api-version".into(), API_VERSION.into()), + Matcher::UrlEncoded("maxresults".into(), "1".into()), + Matcher::UrlEncoded("$skiptoken".into(), "SKIP_TOKEN_MOCK".into()), + ])) + .with_header("content-type", "application/json") + .with_body( + json!({ + "value": [{ + "id": "https://test-keyvault.vault.azure.net/certificates/test-certificate/VERSION_2", + "x5t": "fLi3U52HunIVNXubkEnf8tP6Wbo", + "attributes": { + "enabled": true, + "created": time_created_2.timestamp(), + "updated": time_updated_2.timestamp(), + } + }], + "nextLink": null + }) + .to_string(), + ) + .with_status(200) + .create(); + + let creds = MockCredential; + let mut client = mock_cert_client!(&"test-keyvault", &creds,); + + let certificate_versions = client + .list_properties_of_certificate_versions(&"test-certificate") + .await + .unwrap(); + + let certificate_1 = &certificate_versions[0]; + assert_eq!( + "https://test-keyvault.vault.azure.net/certificates/test-certificate/VERSION_1", + certificate_1.id() + ); + assert!(diff(time_created_1, *certificate_1.created_on()) < Duration::seconds(1)); + assert!(diff(time_updated_1, *certificate_1.updated_on()) < Duration::seconds(1)); + + let certificate_2 = &certificate_versions[1]; + assert_eq!( + "https://test-keyvault.vault.azure.net/certificates/test-certificate/VERSION_2", + certificate_2.id() + ); + assert!(diff(time_created_2, *certificate_2.created_on()) < Duration::seconds(1)); + assert!(diff(time_updated_2, *certificate_2.updated_on()) < Duration::seconds(1)); + } +} diff --git a/sdk/security_keyvault/src/client.rs b/sdk/security_keyvault/src/client.rs index 0745b5fc0c..3218028da9 100644 --- a/sdk/security_keyvault/src/client.rs +++ b/sdk/security_keyvault/src/client.rs @@ -164,6 +164,164 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { } } +/// Client for Key Vault operations - getting a certificate, listing certificates, etc. +/// +/// # Example +/// +/// ```no_run +/// use azure_security_keyvault::CertificateClient; +/// use azure_identity::token_credentials::DefaultAzureCredential; +/// let creds = DefaultAzureCredential::default(); +/// let client = CertificateClient::new(&"https://test-key-vault.vault.azure.net", &creds).unwrap(); +/// ``` +#[derive(Debug)] +pub struct CertificateClient<'a, T> { + pub(crate) vault_url: Url, + pub(crate) endpoint: String, + pub(crate) token_credential: &'a T, + pub(crate) token: Option, +} + +impl<'a, T: TokenCredential> CertificateClient<'a, T> { + /// Creates a new `CertificateClient`. + /// + /// # Example + /// + /// ```no_run + /// use azure_security_keyvault::CertificateClient; + /// use azure_identity::token_credentials::DefaultAzureCredential; + /// let creds = DefaultAzureCredential::default(); + /// let client = CertificateClient::new("test-key-vault.vault.azure.net", &creds).unwrap(); + /// ``` + pub fn new(vault_url: &str, token_credential: &'a T) -> Result { + let vault_url = Url::parse(vault_url)?; + let endpoint = extract_endpoint(&vault_url)?; + let client = CertificateClient { + vault_url, + endpoint, + token_credential, + token: None, + }; + Ok(client) + } + + pub(crate) async fn refresh_token(&mut self) -> Result<(), Error> { + if matches!(&self.token, Some(token) if token.expires_on > chrono::Utc::now()) { + // Token is valid, return it. + return Ok(()); + } + + let token = self + .token_credential + .get_token(&self.endpoint) + .await + .map_err(|_| Error::Authorization)?; + self.token = Some(token); + Ok(()) + } + + pub(crate) async fn get_authed(&mut self, uri: String) -> Result { + self.refresh_token().await?; + + let resp = reqwest::Client::new() + .get(&uri) + .bearer_auth(self.token.as_ref().unwrap().token.secret()) + .send() + .await + .unwrap(); + let body = resp.text().await.unwrap(); + Ok(body) + } + + pub(crate) async fn _put_authed(&mut self, uri: String, body: String) -> Result { + self.refresh_token().await?; + + let resp = reqwest::Client::new() + .put(&uri) + .bearer_auth(self.token.as_ref().unwrap().token.secret()) + .header("Content-Type", "application/json") + .body(body) + .send() + .await + .unwrap(); + let body = resp.text().await?; + Ok(body) + } + + pub(crate) async fn post_authed( + &mut self, + uri: String, + json_body: Option, + ) -> Result { + self.refresh_token().await?; + + let mut req = reqwest::Client::new() + .post(&uri) + .bearer_auth(self.token.as_ref().unwrap().token.secret()); + + if let Some(body) = json_body { + req = req.header("Content-Type", "application/json").body(body); + } else { + req = req.header("Content-Length", 0); + } + + let resp = req.send().await?; + + let body = resp.text().await?; + + let body_serialized = serde_json::from_str::(&body).unwrap(); + + if let Some(err) = body_serialized.get("error") { + let msg = err.get("message").ok_or(Error::UnparsableError)?; + Err(Error::General(msg.to_string())) + } else { + Ok(body) + } + } + + pub(crate) async fn patch_authed( + &mut self, + uri: String, + body: String, + ) -> Result { + self.refresh_token().await?; + + let resp = reqwest::Client::new() + .patch(&uri) + .bearer_auth(self.token.as_ref().unwrap().token.secret()) + .header("Content-Type", "application/json") + .body(body) + .send() + .await + .unwrap(); + + let body = resp.text().await.unwrap(); + + let body_serialized = serde_json::from_str::(&body).unwrap(); + + if let Some(err) = body_serialized.get("error") { + let msg = err.get("message").ok_or(Error::UnparsableError)?; + Err(Error::General(msg.to_string())) + } else { + Ok(body) + } + } + + pub(crate) async fn _delete_authed(&mut self, uri: String) -> Result { + self.refresh_token().await?; + + let resp = reqwest::Client::new() + .delete(&uri) + .bearer_auth(self.token.as_ref().unwrap().token.secret()) + .header("Content-Type", "application/json") + .send() + .await + .unwrap(); + let body = resp.text().await.unwrap(); + Ok(body) + } +} + /// Helper to get vault endpoint with a scheme and a trailing slash /// ex. `https://vault.azure.net/` where the full client url is `https://myvault.vault.azure.net` fn extract_endpoint(url: &Url) -> Result { diff --git a/sdk/security_keyvault/src/key.rs b/sdk/security_keyvault/src/key.rs index 9c2fda1b06..08f437403c 100644 --- a/sdk/security_keyvault/src/key.rs +++ b/sdk/security_keyvault/src/key.rs @@ -522,7 +522,7 @@ mod tests { use serde_json::json; use crate::client::API_VERSION; - use crate::mock_client; + use crate::mock_key_client; use crate::tests::MockCredential; fn diff(first: DateTime, second: DateTime) -> Duration { @@ -574,7 +574,7 @@ mod tests { .create(); let creds = MockCredential; - let mut client = mock_client!(&"test-keyvault", &creds,); + let mut client = mock_key_client!(&"test-keyvault", &creds,); let key = client .get_key("test-key", Some("78deebed173b48e48f55abf87ed4cf71")) @@ -626,7 +626,7 @@ mod tests { .create(); let creds = MockCredential; - let mut client = mock_client!(&"test-keyvault", &creds,); + let mut client = mock_key_client!(&"test-keyvault", &creds,); let res = client .sign( @@ -667,7 +667,7 @@ mod tests { .create(); let creds = MockCredential; - let mut client = mock_client!(&"test-keyvault", &creds,); + let mut client = mock_key_client!(&"test-keyvault", &creds,); let decrypt_parameters = DecryptParameters { ciphertext: base64::decode("dvDmrSBpjRjtYg").unwrap(), diff --git a/sdk/security_keyvault/src/lib.rs b/sdk/security_keyvault/src/lib.rs index 9df2c6f328..d8840a7da4 100644 --- a/sdk/security_keyvault/src/lib.rs +++ b/sdk/security_keyvault/src/lib.rs @@ -1,10 +1,9 @@ -//! Azure Key Vault crate for the unofficial Microsoft Azure SDK for Rust. This crate is part of a collection of crates: for more information please refer to [https://github.com/azure/azure-sdk-for-rust](). +pub mod certificate; mod client; pub mod key; pub mod secret; -pub use client::KeyClient; -pub use secret::RecoveryLevel; +pub use client::{CertificateClient, KeyClient}; #[non_exhaustive] #[derive(thiserror::Error, Debug)] @@ -24,6 +23,9 @@ pub enum Error { #[error("Key Vault Error: {0}")] General(String), + #[error("Base64 Decode Error: {0}")] + Base64(#[from] base64::DecodeError), + #[error("Failed to parse response from Key Vault: {0}")] SerdeParse(#[from] serde_json::Error), @@ -39,6 +41,15 @@ pub enum Error { secret_name: String, response_body: String, }, + #[error("Failed to parse response from Key Vault when backing up certificate {}, response body: {}, error: {}", certificate_name, response_body, error)] + BackupCertificateParseError { + error: serde_json::Error, + certificate_name: String, + response_body: String, + }, + + #[error("Maximum Query results is 25, given {0}.")] + MaxQueryTooHigh(usize), #[error("Encryption algorithm mismatch")] EncryptionAlgorithmMismatch, @@ -51,9 +62,20 @@ mod tests { use oauth2::AccessToken; #[macro_export] - macro_rules! mock_client { + macro_rules! mock_key_client { + ($keyvault_name:expr, $creds:expr, ) => {{ + crate::client::KeyClient { + vault_url: url::Url::parse(&mockito::server_url()).unwrap(), + endpoint: "".to_string(), + token_credential: $creds, + token: None, + } + }}; + } + #[macro_export] + macro_rules! mock_cert_client { ($keyvault_name:expr, $creds:expr, ) => {{ - KeyClient { + crate::client::CertificateClient { vault_url: url::Url::parse(&mockito::server_url()).unwrap(), endpoint: "".to_string(), token_credential: $creds, diff --git a/sdk/security_keyvault/src/secret.rs b/sdk/security_keyvault/src/secret.rs index b931c2b0ef..c144eda681 100644 --- a/sdk/security_keyvault/src/secret.rs +++ b/sdk/security_keyvault/src/secret.rs @@ -10,36 +10,12 @@ use getset::Getters; use reqwest::Url; use serde::Deserialize; use serde_json::{Map, Value}; -use std::fmt; const DEFAULT_MAX_RESULTS: usize = 25; const API_VERSION_MAX_RESULTS_PARAM: &str = formatcp!("{}&maxresults={}", API_VERSION_PARAM, DEFAULT_MAX_RESULTS); -/// Reflects the deletion recovery level currently in effect for keys in the current Key Vault. -/// If it contains 'Purgeable' the key can be permanently deleted by a privileged user; -/// otherwise, only the system can purge the key, at the end of the retention interval. -pub enum RecoveryLevel { - Purgeable, - Recoverable, - RecoverableAndProtectedSubscription, - RecoverableAndPurgeable, -} - -impl fmt::Display for RecoveryLevel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - RecoveryLevel::Purgeable => write!(f, "Purgeable"), - RecoveryLevel::Recoverable => write!(f, "Recoverable"), - RecoveryLevel::RecoverableAndProtectedSubscription => { - write!(f, "Recoverable+ProtectedSubscription") - } - RecoveryLevel::RecoverableAndPurgeable => write!(f, "Recoverable+Purgeable"), - } - } -} - #[derive(Deserialize, Debug)] pub(crate) struct KeyVaultSecretBaseIdentifierAttributedRaw { enabled: bool, @@ -403,7 +379,7 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { /// # Example /// /// ```no_run - /// use azure_security_keyvault::{KeyClient, RecoveryLevel}; + /// use azure_security_keyvault::KeyClient; /// use azure_identity::token_credentials::DefaultAzureCredential; /// use tokio::runtime::Runtime; /// @@ -413,7 +389,7 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { /// &"KEYVAULT_URL", /// &creds, /// ).unwrap(); - /// client.update_secret_recovery_level(&"SECRET_NAME", &"", RecoveryLevel::Purgeable).await.unwrap(); + /// client.update_secret_recovery_level(&"SECRET_NAME", &"", "Purgeable".into()).await.unwrap(); /// } /// /// Runtime::new().unwrap().block_on(example()); @@ -422,13 +398,10 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { &mut self, secret_name: &str, secret_version: &str, - recovery_level: RecoveryLevel, + recovery_level: String, ) -> Result<(), Error> { let mut attributes = Map::new(); - attributes.insert( - "enabled".to_owned(), - Value::String(recovery_level.to_string()), - ); + attributes.insert("enabled".to_owned(), Value::String(recovery_level)); self.update_secret(secret_name, secret_version, attributes) .await?; @@ -447,7 +420,7 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { /// # Example /// /// ```no_run - /// use azure_security_keyvault::{KeyClient, RecoveryLevel}; + /// use azure_security_keyvault::KeyClient; /// use azure_identity::token_credentials::DefaultAzureCredential; /// use tokio::runtime::Runtime; /// use chrono::{Utc, Duration}; @@ -589,7 +562,7 @@ impl<'a, T: TokenCredential> KeyClient<'a, T> { /// # Example /// /// ```no_run - /// use azure_security_keyvault::{KeyClient, RecoveryLevel}; + /// use azure_security_keyvault::KeyClient; /// use azure_identity::token_credentials::DefaultAzureCredential; /// use tokio::runtime::Runtime; /// @@ -625,7 +598,7 @@ mod tests { use serde_json::json; use crate::client::API_VERSION; - use crate::mock_client; + use crate::mock_key_client; use crate::tests::MockCredential; fn diff(first: DateTime, second: DateTime) -> Duration { @@ -661,7 +634,7 @@ mod tests { let creds = MockCredential; dbg!(mockito::server_url()); - let mut client = mock_client!(&"test-keyvault", &creds,); + let mut client = mock_key_client!(&"test-keyvault", &creds,); let secret: KeyVaultSecret = client.get_secret("test-secret").await.unwrap(); @@ -730,7 +703,7 @@ mod tests { .create(); let creds = MockCredential; - let mut client = mock_client!(&"test-keyvault", &creds,); + let mut client = mock_key_client!(&"test-keyvault", &creds,); let secret_versions = client.get_secret_versions("test-secret").await.unwrap(); diff --git a/services/mgmt/sqlvirtualmachine/src/package_2017_03_01_preview/models.rs b/services/mgmt/sqlvirtualmachine/src/package_2017_03_01_preview/models.rs index 1de90f2324..a936b7a915 100644 --- a/services/mgmt/sqlvirtualmachine/src/package_2017_03_01_preview/models.rs +++ b/services/mgmt/sqlvirtualmachine/src/package_2017_03_01_preview/models.rs @@ -353,7 +353,7 @@ pub struct KeyVaultCredentialSettings { #[serde(rename = "credentialName", default, skip_serializing_if = "Option::is_none")] pub credential_name: Option, #[serde(rename = "azureKeyVaultUrl", default, skip_serializing_if = "Option::is_none")] - pub azure_key_vault_url: Option, + pub azure_security_keyvault_url: Option, #[serde(rename = "servicePrincipalName", default, skip_serializing_if = "Option::is_none")] pub service_principal_name: Option, #[serde(rename = "servicePrincipalSecret", default, skip_serializing_if = "Option::is_none")]