Skip to content

Commit

Permalink
Add AzurePipelinesCredential
Browse files Browse the repository at this point in the history
Resolves Azure#2030
  • Loading branch information
heaths committed Mar 8, 2025
1 parent 3aae498 commit 4a0a54e
Show file tree
Hide file tree
Showing 20 changed files with 630 additions and 102 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
"azure-pipelines.1ESPipelineTemplatesSchemaFile": true,
"cSpell.enabled": true,
"editor.formatOnSave": true,
"markdownlint.config": {
"MD024": false
},
"rust-analyzer.cargo.features": "all",
"rust-analyzer.check.command": "clippy",
"yaml.format.printWidth": 240,
"[powershell]": {
"editor.defaultFormatter": "ms-vscode.powershell",
},
}
}
1 change: 1 addition & 0 deletions 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/core/azure_core_test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
uuid.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
tokio = { workspace = true, features = ["signal"] }
tokio = { workspace = true, features = ["rt", "signal"] }
31 changes: 0 additions & 31 deletions sdk/core/azure_core_test/src/credential.rs

This file was deleted.

70 changes: 70 additions & 0 deletions sdk/core/azure_core_test/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//! Credentials for live and recorded tests.
use azure_core::{
credentials::{AccessToken, Secret, TokenCredential},
date::OffsetDateTime,
error::ErrorKind,
};
use azure_identity::{AzurePipelinesCredential, DefaultAzureCredential, TokenCredentialOptions};
use std::{env, sync::Arc, time::Duration};

/// A mock [`TokenCredential`] useful for testing.
#[derive(Clone, Debug, Default)]
pub struct MockCredential;

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl TokenCredential for MockCredential {
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
let token: Secret = format!("TEST TOKEN {}", scopes.join(" ")).into();
let expires_on = OffsetDateTime::now_utc().saturating_add(
Duration::from_secs(60 * 5).try_into().map_err(|err| {
azure_core::Error::full(ErrorKind::Other, err, "failed to compute expiration")
})?,
);
Ok(AccessToken { token, expires_on })
}

async fn clear_cache(&self) -> azure_core::Result<()> {
Ok(())
}
}

/// Gets a `TokenCredential` appropriate for the current environment.
///
/// When running in Azure Pipelines, this will return an [`AzurePipelinesCredential`];
/// otherwise, it will return a [`DefaultAzureCredential`].
pub fn from_env(
options: Option<TokenCredentialOptions>,
) -> azure_core::Result<Arc<dyn TokenCredential>> {
// cspell:ignore accesstoken azuresubscription
let tenant_id = env::var("AZURESUBSCRIPTION_TENANT_ID").ok();
let client_id = env::var("AZURESUBSCRIPTION_CLIENT_ID").ok();
let connection_id = env::var("AZURESUBSCRIPTION_SERVICE_CONNECTION_ID").ok();
let access_token = env::var("SYSTEM_ACCESSTOKEN").ok();

if let (Some(tenant_id), Some(client_id), Some(connection_id), Some(access_token)) =
(tenant_id, client_id, connection_id, access_token)
{
if !tenant_id.is_empty()
&& !client_id.is_empty()
&& !connection_id.is_empty()
&& !access_token.is_empty()
{
return Ok(AzurePipelinesCredential::new(
tenant_id,
client_id,
&connection_id,
access_token,
options.map(Into::into),
)? as Arc<dyn TokenCredential>);
}
}

Ok(
DefaultAzureCredential::with_options(options.unwrap_or_default())?
as Arc<dyn TokenCredential>,
)
}
75 changes: 75 additions & 0 deletions sdk/core/azure_core_test/src/http/clients.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

use async_trait::async_trait;
use azure_core::{HttpClient, Request, Response, Result};
use futures::lock::Mutex;
use std::{fmt, future::Future, pin::Pin};

pub struct MockHttpClient<C>(Mutex<C>);

impl<C> MockHttpClient<C>
where
C: FnMut(&Request) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>>
+ Send
+ Sync,
{
pub fn new(client: C) -> Self {
Self(Mutex::new(client))
}
}

impl<C> fmt::Debug for MockHttpClient<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(stringify!("MockHttpClient"))
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> HttpClient for MockHttpClient<C>
where
C: FnMut(&Request) -> Pin<Box<dyn Future<Output = Result<Response>> + Send + Sync + '_>>
+ Send
+ Sync,
{
async fn execute_request(&self, req: &Request) -> Result<Response> {
let mut client = self.0.lock().await;
(client)(req).await
}
}

#[tokio::test]
async fn test_mock_http_client() {
use azure_core::{
headers::{HeaderName, Headers},
Method, StatusCode,
};
use std::sync::{Arc, Mutex};

const COUNT_HEADER: HeaderName = HeaderName::from_static("x-count");

let count = Arc::new(Mutex::new(0));
let mock_client = Arc::new(MockHttpClient::new(|req| {
let count = count.clone();
Box::pin(async move {
assert_eq!(req.url().host_str(), Some("localhost"));

if req.headers().get_optional_str(&COUNT_HEADER).is_some() {
let mut count = count.lock().unwrap();
*count += 1;
}

Ok(Response::from_bytes(StatusCode::Ok, Headers::new(), vec![]))
})
})) as Arc<dyn HttpClient>;

let req = Request::new("https://localhost".parse().unwrap(), Method::Get);
mock_client.execute_request(&req).await.unwrap();

let mut req = Request::new("https://localhost".parse().unwrap(), Method::Get);
req.insert_header(COUNT_HEADER, "true");
mock_client.execute_request(&req).await.unwrap();

assert_eq!(*count.lock().unwrap(), 1);
}
7 changes: 7 additions & 0 deletions sdk/core/azure_core_test/src/http/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

//! HTTP testing utilities.
mod clients;

pub use clients::*;
4 changes: 2 additions & 2 deletions sdk/core/azure_core_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@

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

mod credential;
pub mod credentials;
pub mod http;
pub mod proxy;
pub mod recorded;
mod recording;

use azure_core::Error;
pub use azure_core::{error::ErrorKind, test::TestMode};
pub use credential::*;
pub use proxy::{matchers::*, sanitizers::*};
pub use recording::*;
use std::path::{Path, PathBuf};
Expand Down
6 changes: 3 additions & 3 deletions sdk/core/azure_core_test/src/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// cspell:ignore csprng seedable tpbwhbkhckmk
use crate::{
credentials::{self, MockCredential},
proxy::{
client::{
Client, ClientAddSanitizerOptions, ClientRemoveSanitizersOptions,
Expand All @@ -14,7 +15,7 @@ use crate::{
policy::RecordingPolicy,
Proxy, RecordingId,
},
Matcher, MockCredential, Sanitizer,
Matcher, Sanitizer,
};
use azure_core::{
base64,
Expand All @@ -24,7 +25,6 @@ use azure_core::{
test::TestMode,
ClientOptions, Header,
};
use azure_identity::DefaultAzureCredential;
use rand::{
distributions::{Alphanumeric, DistString, Distribution, Standard},
Rng, SeedableRng,
Expand Down Expand Up @@ -83,7 +83,7 @@ impl Recording {
pub fn credential(&self) -> Arc<dyn TokenCredential> {
match self.test_mode {
TestMode::Playback => Arc::new(MockCredential) as Arc<dyn TokenCredential>,
_ => DefaultAzureCredential::new().map_or_else(
_ => credentials::from_env(None).map_or_else(
|err| panic!("failed to create DefaultAzureCredential: {err}"),
|cred| cred as Arc<dyn TokenCredential>,
),
Expand Down
15 changes: 15 additions & 0 deletions sdk/identity/azure_identity/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Release History

## 0.32.0 (Unreleased)

### Features Added

- Added `AzurePipelinesCredential`.

### Breaking Changes

- `ClientAssertionCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.
- `WorkloadIdentityCredential` constructors moved some parameters to an `Option<ClientAssertionCredentialOptions>` parameter.

### Bugs Fixed

### Other Changes

## 0.22.0 (2025-02-18)

### Features Added
Expand Down
19 changes: 10 additions & 9 deletions sdk/identity/azure_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ categories = ["api-bindings"]
edition.workspace = true

[dependencies]
azure_core.workspace = true
async-lock.workspace = true
oauth2.workspace = true
url.workspace = true
async-trait.workspace = true
azure_core.workspace = true
futures.workspace = true
oauth2.workspace = true
openssl = { workspace = true, optional = true }
pin-project.workspace = true
serde.workspace = true
time.workspace = true
tracing.workspace = true
async-trait.workspace = true
openssl = { workspace = true, optional = true }
pin-project.workspace = true
typespec_client_core = { workspace = true, features = ["derive"] }
url.workspace = true

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
async-process.workspace = true
Expand All @@ -33,13 +33,14 @@ async-process.workspace = true
tz-rs = { workspace = true, optional = true }

[dev-dependencies]
azure_core_test.workspace = true
azure_security_keyvault_secrets = { path = "../../keyvault/azure_security_keyvault_secrets" }
clap.workspace = true
reqwest.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
serde_test.workspace = true
serial_test.workspace = true
clap.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true

[features]
default = ["reqwest", "old_azure_cli"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ impl SpecificAzureCredential {
)
})?,
azure_credential_kinds::WORKLOAD_IDENTITY => {
WorkloadIdentityCredential::from_env(options)
WorkloadIdentityCredential::from_env(Some(options.into()))
.map(SpecificAzureCredentialKind::WorkloadIdentity)
.with_context(ErrorKind::Credential, || {
format!(
Expand Down
Loading

0 comments on commit 4a0a54e

Please sign in to comment.