Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AzurePipelinesCredential #2306

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@analogrelay any other good ideas here? The DX this forces (see test below; we have to pin the future) is unwieldy but I couldn't figure out a better way without taking advantage of Rust 1.85 async captures.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cleaned it up a little using a BoxedFuture, though the boxed() "extension method" probably does more heavy lifting here. Still open to other suggestions, but this seems to be a fairly common solution I found - more than what inspired me originally.

{
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
Loading