Skip to content

Commit 207d8df

Browse files
authored
Add AzurePipelinesCredential (Azure#2306)
* Add AzurePipelinesCredential Resolves Azure#2030 * Clean up declaration a little and add docs, example * Fix README samples after refactoring * Fix lint * Fix clippy issues with wasm32
1 parent 6e73f3d commit 207d8df

File tree

21 files changed

+682
-104
lines changed

21 files changed

+682
-104
lines changed

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
"azure-pipelines.1ESPipelineTemplatesSchemaFile": true,
33
"cSpell.enabled": true,
44
"editor.formatOnSave": true,
5+
"markdownlint.config": {
6+
"MD024": false
7+
},
58
"rust-analyzer.cargo.features": "all",
69
"rust-analyzer.check.command": "clippy",
710
"yaml.format.printWidth": 240,
811
"[powershell]": {
912
"editor.defaultFormatter": "ms-vscode.powershell",
1013
},
11-
}
14+
}

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/core/azure_core_test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ zip.workspace = true
5353
# Crate used in README.md example.
5454
azure_security_keyvault_secrets = { path = "../../keyvault/azure_security_keyvault_secrets" }
5555
clap.workspace = true
56+
tokio = { workspace = true, features = ["macros", "rt"] }
5657
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
5758
uuid.workspace = true
5859

sdk/core/azure_core_test/src/credential.rs

Lines changed: 0 additions & 31 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
//! Credentials for live and recorded tests.
5+
use azure_core::{
6+
credentials::{AccessToken, Secret, TokenCredential},
7+
date::OffsetDateTime,
8+
error::ErrorKind,
9+
};
10+
use azure_identity::{AzurePipelinesCredential, DefaultAzureCredential, TokenCredentialOptions};
11+
use std::{env, sync::Arc, time::Duration};
12+
13+
/// A mock [`TokenCredential`] useful for testing.
14+
#[derive(Clone, Debug, Default)]
15+
pub struct MockCredential;
16+
17+
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
18+
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
19+
impl TokenCredential for MockCredential {
20+
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
21+
let token: Secret = format!("TEST TOKEN {}", scopes.join(" ")).into();
22+
let expires_on = OffsetDateTime::now_utc().saturating_add(
23+
Duration::from_secs(60 * 5).try_into().map_err(|err| {
24+
azure_core::Error::full(ErrorKind::Other, err, "failed to compute expiration")
25+
})?,
26+
);
27+
Ok(AccessToken { token, expires_on })
28+
}
29+
30+
async fn clear_cache(&self) -> azure_core::Result<()> {
31+
Ok(())
32+
}
33+
}
34+
35+
/// Gets a `TokenCredential` appropriate for the current environment.
36+
///
37+
/// When running in Azure Pipelines, this will return an [`AzurePipelinesCredential`];
38+
/// otherwise, it will return a [`DefaultAzureCredential`].
39+
pub fn from_env(
40+
options: Option<TokenCredentialOptions>,
41+
) -> azure_core::Result<Arc<dyn TokenCredential>> {
42+
// cspell:ignore accesstoken azuresubscription
43+
let tenant_id = env::var("AZURESUBSCRIPTION_TENANT_ID").ok();
44+
let client_id = env::var("AZURESUBSCRIPTION_CLIENT_ID").ok();
45+
let connection_id = env::var("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID").ok();
46+
let access_token = env::var("SYSTEM_ACCESSTOKEN").ok();
47+
48+
if let (Some(tenant_id), Some(client_id), Some(connection_id), Some(access_token)) =
49+
(tenant_id, client_id, connection_id, access_token)
50+
{
51+
if !tenant_id.is_empty()
52+
&& !client_id.is_empty()
53+
&& !connection_id.is_empty()
54+
&& !access_token.is_empty()
55+
{
56+
return Ok(AzurePipelinesCredential::new(
57+
tenant_id,
58+
client_id,
59+
&connection_id,
60+
access_token,
61+
options.map(Into::into),
62+
)? as Arc<dyn TokenCredential>);
63+
}
64+
}
65+
66+
Ok(
67+
DefaultAzureCredential::with_options(options.unwrap_or_default())?
68+
as Arc<dyn TokenCredential>,
69+
)
70+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
use async_trait::async_trait;
5+
use azure_core::{HttpClient, Request, Response, Result};
6+
use futures::{future::BoxFuture, lock::Mutex};
7+
use std::fmt;
8+
9+
/// An [`HttpClient`] from which you can assert [`Request`]s and return mock [`Response`]s.
10+
///
11+
/// # Examples
12+
///
13+
/// ```
14+
/// use azure_core::{
15+
/// Bytes, ClientOptions,
16+
/// headers::Headers,
17+
/// Response, StatusCode, TransportOptions,
18+
/// };
19+
/// use azure_core_test::http::MockHttpClient;
20+
/// use azure_identity::DefaultAzureCredential;
21+
/// use azure_security_keyvault_secrets::{SecretClient, SecretClientOptions};
22+
/// use futures::FutureExt as _;
23+
/// use std::sync::Arc;
24+
///
25+
/// # #[tokio::main]
26+
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
27+
/// let mock_client = Arc::new(MockHttpClient::new(|req| async {
28+
/// assert_eq!(req.url().host_str(), Some("my-vault.vault.azure.net"));
29+
/// Ok(Response::from_bytes(
30+
/// StatusCode::Ok,
31+
/// Headers::new(),
32+
/// Bytes::from_static(br#"{"value":"secret"}"#),
33+
/// ))
34+
/// }.boxed()));
35+
/// let credential = DefaultAzureCredential::new()?;
36+
/// let options = SecretClientOptions {
37+
/// client_options: ClientOptions {
38+
/// transport: Some(TransportOptions::new(mock_client.clone())),
39+
/// ..Default::default()
40+
/// },
41+
/// ..Default::default()
42+
/// };
43+
/// let client = SecretClient::new(
44+
/// "https://my-vault.vault.azure.net",
45+
/// credential.clone(),
46+
/// Some(options),
47+
/// );
48+
/// # Ok(())
49+
/// # }
50+
/// ```
51+
pub struct MockHttpClient<C>(Mutex<C>);
52+
53+
impl<C> MockHttpClient<C>
54+
where
55+
C: FnMut(&Request) -> BoxFuture<'_, Result<Response>> + Send + Sync,
56+
{
57+
/// Creates a new `MockHttpClient` using a capture.
58+
///
59+
/// The capture takes a `&Request` and returns a `BoxedFuture<Output = azure_core::Result<Response>>`.
60+
/// See the example on [`MockHttpClient`].
61+
pub fn new(client: C) -> Self {
62+
Self(Mutex::new(client))
63+
}
64+
}
65+
66+
impl<C> fmt::Debug for MockHttpClient<C> {
67+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68+
f.write_str(stringify!("MockHttpClient"))
69+
}
70+
}
71+
72+
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
73+
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
74+
impl<C> HttpClient for MockHttpClient<C>
75+
where
76+
C: FnMut(&Request) -> BoxFuture<'_, Result<Response>> + Send + Sync,
77+
{
78+
async fn execute_request(&self, req: &Request) -> Result<Response> {
79+
let mut client = self.0.lock().await;
80+
(client)(req).await
81+
}
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
use futures::FutureExt as _;
88+
89+
#[tokio::test]
90+
async fn mock_http_client() {
91+
use azure_core::{
92+
headers::{HeaderName, Headers},
93+
Method, StatusCode,
94+
};
95+
use std::sync::{Arc, Mutex};
96+
97+
const COUNT_HEADER: HeaderName = HeaderName::from_static("x-count");
98+
99+
let count = Arc::new(Mutex::new(0));
100+
let mock_client = Arc::new(MockHttpClient::new(|req| {
101+
let count = count.clone();
102+
async move {
103+
assert_eq!(req.url().host_str(), Some("localhost"));
104+
105+
if req.headers().get_optional_str(&COUNT_HEADER).is_some() {
106+
let mut count = count.lock().unwrap();
107+
*count += 1;
108+
}
109+
110+
Ok(Response::from_bytes(StatusCode::Ok, Headers::new(), vec![]))
111+
}
112+
.boxed()
113+
})) as Arc<dyn HttpClient>;
114+
115+
let req = Request::new("https://localhost".parse().unwrap(), Method::Get);
116+
mock_client.execute_request(&req).await.unwrap();
117+
118+
let mut req = Request::new("https://localhost".parse().unwrap(), Method::Get);
119+
req.insert_header(COUNT_HEADER, "true");
120+
mock_client.execute_request(&req).await.unwrap();
121+
122+
assert_eq!(*count.lock().unwrap(), 1);
123+
}
124+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
//! HTTP testing utilities.
5+
mod clients;
6+
7+
pub use clients::*;

sdk/core/azure_core_test/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33

44
#![doc = include_str!("../README.md")]
55

6-
mod credential;
6+
pub mod credentials;
7+
pub mod http;
78
pub mod proxy;
89
pub mod recorded;
910
mod recording;
1011

1112
use azure_core::Error;
1213
pub use azure_core::{error::ErrorKind, test::TestMode};
13-
pub use credential::*;
1414
pub use proxy::{matchers::*, sanitizers::*};
1515
pub use recording::*;
1616
use std::path::{Path, PathBuf};

sdk/core/azure_core_test/src/recording.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
// cspell:ignore csprng seedable tpbwhbkhckmk
77
use crate::{
8+
credentials::{self, MockCredential},
89
proxy::{
910
client::{
1011
Client, ClientAddSanitizerOptions, ClientRemoveSanitizersOptions,
@@ -14,7 +15,7 @@ use crate::{
1415
policy::RecordingPolicy,
1516
Proxy, RecordingId,
1617
},
17-
Matcher, MockCredential, Sanitizer,
18+
Matcher, Sanitizer,
1819
};
1920
use azure_core::{
2021
base64,
@@ -24,7 +25,6 @@ use azure_core::{
2425
test::TestMode,
2526
ClientOptions, Header,
2627
};
27-
use azure_identity::DefaultAzureCredential;
2828
use rand::{
2929
distributions::{Alphanumeric, DistString, Distribution, Standard},
3030
Rng, SeedableRng,
@@ -83,7 +83,7 @@ impl Recording {
8383
pub fn credential(&self) -> Arc<dyn TokenCredential> {
8484
match self.test_mode {
8585
TestMode::Playback => Arc::new(MockCredential) as Arc<dyn TokenCredential>,
86-
_ => DefaultAzureCredential::new().map_or_else(
86+
_ => credentials::from_env(None).map_or_else(
8787
|err| panic!("failed to create DefaultAzureCredential: {err}"),
8888
|cred| cred as Arc<dyn TokenCredential>,
8989
),

sdk/identity/azure_identity/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Release History
22

3+
## 0.32.0 (Unreleased)
4+
5+
### Features Added
6+
7+
- Added `AzurePipelinesCredential`.
8+
9+
### Breaking Changes
10+
11+
- `ClientAssertionCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
12+
- `WorkloadIdentityCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
13+
14+
### Bugs Fixed
15+
16+
### Other Changes
17+
318
## 0.22.0 (2025-02-18)
419

520
### Features Added

0 commit comments

Comments
 (0)