Skip to content

Commit 5376989

Browse files
authored
Update UserAgentPolicy to better align with guidelines (#2743)
Adds documentation to our repo's root README - which is also now syntactically verified when testing `azure_core_test`, and removes the `UserAgentOptions::disabled` in favor of, like most languages, recommending a user policy to remove the `User-Agent` header.
1 parent 258986c commit 5376989

File tree

11 files changed

+322
-18
lines changed

11 files changed

+322
-18
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,52 @@ Building each crate should be as straight forward as `cargo build`, but check ea
3939
- Have a question, or find a bug? File an issue via [GitHub Issues](https://github.com/Azure/azure-sdk-for-rust/issues/new/choose).
4040
- Check [previous questions](https://stackoverflow.com/questions/tagged/azure+rust) or ask new ones on StackOverflow using the `azure` and `rust` tags.
4141

42+
## Data Collection
43+
44+
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described below. You can learn more about data collection and use in the help documentation and Microsoft’s [privacy statement](https://go.microsoft.com/fwlink/?LinkID=824704). For more information on the data collected by the Azure SDK, please visit the [Telemetry Guidelines](https://azure.github.io/azure-sdk/general_azurecore.html#telemetry-policy) page.
45+
46+
### Telemetry Configuration
47+
48+
A `User-Agent` header is sent in requests by default with a value similar to:
49+
50+
> azsdk-rust-security_keyvault_secrets/0.4.0 (1.86.0; linux; aarch64)
51+
52+
You can assign an optional application ID for your own telemetry by setting `UserPolicyOptions::application_id`. This will appear at the beginning of the `User-Agent` header.
53+
54+
To disable sending the `User-Agent` header entirely, you can write a `Policy` that will remove it:
55+
56+
```rust no_run
57+
use async_trait::async_trait;
58+
use azure_core::http::{
59+
policies::{Policy, PolicyResult},
60+
Context, Request,
61+
};
62+
use std::sync::Arc;
63+
64+
// Define a policy that will remove the User-Agent header.
65+
#[derive(Debug)]
66+
struct RemoveUserAgent;
67+
68+
#[async_trait]
69+
impl Policy for RemoveUserAgent {
70+
async fn send(
71+
&self,
72+
ctx: &Context,
73+
request: &mut Request,
74+
next: &[Arc<dyn Policy>],
75+
) -> PolicyResult {
76+
let headers = request.headers_mut();
77+
78+
// Note: HTTP headers are case-insensitive but client-added headers are normalized to lowercase.
79+
headers.remove("user-agent");
80+
81+
next[0].send(ctx, request, &next[1..]).await
82+
}
83+
}
84+
```
85+
86+
For a complete example, see our [`azure_core` example](https://github.com/Azure/azure-sdk-for-rust/blob/main/sdk/core/azure_core/examples/core_remove_user_agent.rs).
87+
4288
### Reporting security issues and security bugs
4389

4490
Security issues and bugs should be reported privately, via email, to the Microsoft Security Response Center (MSRC) <secure@microsoft.com>. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Further information, including the MSRC PGP key, can be found in the [Security TechCenter](https://www.microsoft.com/msrc/faqs-report-an-issue).

sdk/core/azure_core/CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
### Features Added
66

77
- Added `get_async_runtime()` and `set_async_runtime()` to allow customers to replace the asynchronous runtime used by the Azure SDK.
8-
- Added `UserAgentOptions::enabled` to allow disabling sending the `User-Agent` header.
98

109
### Breaking Changes
1110

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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::{
6+
credentials::TokenCredential,
7+
http::{
8+
headers::Headers,
9+
policies::{Policy, PolicyResult},
10+
Context, HttpClient, Method, RawResponse, Request, StatusCode, TransportOptions,
11+
},
12+
};
13+
use azure_core_test::{credentials::MockCredential, http::MockHttpClient};
14+
use azure_security_keyvault_secrets::{SecretClient, SecretClientOptions};
15+
use futures::FutureExt;
16+
use std::sync::Arc;
17+
18+
// Define a policy that will remove the User-Agent header.
19+
#[derive(Debug)]
20+
struct RemoveUserAgent;
21+
22+
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
23+
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
24+
impl Policy for RemoveUserAgent {
25+
async fn send(
26+
&self,
27+
ctx: &Context,
28+
request: &mut Request,
29+
next: &[Arc<dyn Policy>],
30+
) -> PolicyResult {
31+
let headers = request.headers_mut();
32+
33+
// Note: HTTP headers are case-insensitive but client-added headers are normalized to lowercase.
34+
headers.remove("user-agent");
35+
36+
next[0].send(ctx, request, &next[1..]).await
37+
}
38+
}
39+
40+
async fn test_remove_user_agent() -> Result<(), Box<dyn std::error::Error>> {
41+
// Policies are created in an Arc to be generally shared.
42+
let remove_user_agent = Arc::new(RemoveUserAgent);
43+
44+
// Construct client options with your policy that runs after the built-in per-call UserAgentPolicy.
45+
let mut options = SecretClientOptions::default();
46+
options
47+
.client_options
48+
.per_call_policies
49+
.push(remove_user_agent);
50+
51+
// Ignore: this is only set up for testing.
52+
// You normally would create credentials from `azure_identity` and
53+
// use the default transport in production.
54+
let (credential, transport) = setup()?;
55+
options.client_options.transport = Some(TransportOptions::new(transport));
56+
57+
// Construct the client with these options and a shared credential.
58+
let client = SecretClient::new(
59+
"https://my-vault.vault.azure.net",
60+
credential.clone(),
61+
Some(options),
62+
)?;
63+
64+
// We'll fetch a secret and let the mock client assert the User-Agent header was removed.
65+
let secret = client
66+
.get_secret("my-secret", "", None)
67+
.await?
68+
.into_body()
69+
.await?;
70+
assert_eq!(secret.value.as_deref(), Some("secret-value"));
71+
72+
Ok(())
73+
}
74+
75+
// ----- BEGIN TEST SETUP -----
76+
#[tokio::test]
77+
async fn test_core_remove_user_agent() -> Result<(), Box<dyn std::error::Error>> {
78+
test_remove_user_agent().await
79+
}
80+
81+
#[tokio::main]
82+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
83+
test_remove_user_agent().await
84+
}
85+
86+
#[allow(clippy::type_complexity)]
87+
fn setup() -> Result<(Arc<dyn TokenCredential>, Arc<dyn HttpClient>), Box<dyn std::error::Error>> {
88+
let client = MockHttpClient::new(|request| {
89+
async move {
90+
assert!(request.url().path().starts_with("/secrets/my-secret"));
91+
assert_eq!(*request.method(), Method::Get);
92+
assert!(
93+
!request
94+
.headers()
95+
.iter()
96+
.any(|(name, _)| name.as_str().eq_ignore_ascii_case("user-agent")),
97+
"user-agent header should be absent"
98+
);
99+
Ok(RawResponse::from_bytes(
100+
StatusCode::Ok,
101+
Headers::new(),
102+
r#"{"value":"secret-value"}"#,
103+
))
104+
}
105+
.boxed()
106+
});
107+
108+
Ok((MockCredential::new()?, Arc::new(client)))
109+
}
110+
// ----- END TEST SETUP -----

sdk/core/azure_core/src/http/options/user_agent.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#[derive(Clone, Debug, Default)]
66
pub struct UserAgentOptions {
77
/// Set the application ID in the `User-Agent` header that can be telemetered.
8+
///
9+
/// # Panics
10+
///
11+
/// Panics if [`UserAgentOptions::application_id`] is greater than 24 characters.
12+
/// See [guidelines](https://azure.github.io/azure-sdk/general_azurecore.html#azurecore-http-telemetry-appid-length) for details.
813
pub application_id: Option<String>,
9-
10-
/// Disable to prevent sending the `User-Agent` header in requests.
11-
pub disabled: bool,
1214
}

sdk/core/azure_core/src/http/pipeline.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,8 @@ impl Pipeline {
5252
push_unique(&mut per_call_policies, ClientRequestIdPolicy::default());
5353

5454
let (user_agent, options) = options.deconstruct();
55-
if !user_agent.disabled {
56-
let telemetry_policy = UserAgentPolicy::new(crate_name, crate_version, &user_agent);
57-
push_unique(&mut per_call_policies, telemetry_policy);
58-
}
55+
let telemetry_policy = UserAgentPolicy::new(crate_name, crate_version, &user_agent);
56+
push_unique(&mut per_call_policies, telemetry_policy);
5957

6058
Self(http::Pipeline::new(
6159
options,
@@ -263,17 +261,24 @@ mod tests {
263261
}
264262

265263
#[tokio::test]
266-
async fn pipeline_with_user_agent_disabled() {
264+
async fn pipeline_with_custom_application_id() {
267265
// Arrange
266+
const CUSTOM_APPLICATION_ID: &str = "my-custom-app/2.1.0";
268267
let ctx = Context::new();
269268

270269
let transport = TransportOptions::new(Arc::new(MockHttpClient::new(|req| {
271270
async {
272271
// Assert
273-
let user_agent = req.headers().get_optional_str(&headers::USER_AGENT);
272+
let user_agent = req
273+
.headers()
274+
.get_optional_str(&headers::USER_AGENT)
275+
.expect("User-Agent header should be present");
276+
// The user agent should contain the custom application_id followed by the standard Azure SDK format
277+
// Expected format: my-custom-app/2.1.0 azsdk-rust-test-crate/1.0.0 (<rustc_version>; <OS>; <ARCH>)
274278
assert!(
275-
user_agent.is_none(),
276-
"User-Agent header should not be present when disabled"
279+
user_agent.starts_with("my-custom-app/2.1.0 azsdk-rust-test-crate/1.0.0 "),
280+
"User-Agent header should start with custom application_id and expected prefix, got: {}",
281+
user_agent
277282
);
278283

279284
Ok(RawResponse::from_bytes(
@@ -284,13 +289,14 @@ mod tests {
284289
}
285290
.boxed()
286291
})));
287-
let user_agent = UserAgentOptions {
288-
disabled: true,
289-
..Default::default()
292+
293+
let user_agent_options = UserAgentOptions {
294+
application_id: Some(CUSTOM_APPLICATION_ID.to_string()),
290295
};
296+
291297
let options = ClientOptions {
292298
transport: Some(transport),
293-
user_agent: Some(user_agent),
299+
user_agent: Some(user_agent_options),
294300
..Default::default()
295301
};
296302

sdk/core/azure_core/src/http/policies/user_agent.rs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ use std::sync::Arc;
1010
use typespec_client_core::http::policies::{Policy, PolicyResult};
1111
use typespec_client_core::http::{Context, Request};
1212

13-
/// Sets the User-Agent header with useful information in a typical format for Azure SDKs.
13+
/// Sets the `User-Agent` header with useful information in a typical format for Azure SDKs.
1414
#[derive(Clone, Debug)]
1515
pub struct UserAgentPolicy {
1616
header: String,
1717
}
1818

1919
impl<'a> UserAgentPolicy {
20+
/// Create a new `UserAgentPolicy`.
21+
///
22+
/// # Panics
23+
///
24+
/// Panics if [`UserAgentOptions::application_id`] is greater than 24 characters.
25+
/// See [guidelines](https://azure.github.io/azure-sdk/general_azurecore.html#azurecore-http-telemetry-appid-length) for details.
2026
pub fn new(
2127
crate_name: Option<&'a str>,
2228
crate_version: Option<&'a str>,
@@ -46,8 +52,15 @@ impl<'a> UserAgentPolicy {
4652
crate_name = name;
4753
}
4854

55+
const MAX_APPLICATION_ID_LEN: usize = 24;
4956
let header = match &options.application_id {
5057
Some(application_id) => {
58+
if application_id.len() > MAX_APPLICATION_ID_LEN {
59+
panic!(
60+
"application_id must be shorter than {} characters",
61+
MAX_APPLICATION_ID_LEN + 1
62+
);
63+
}
5164
format!("{application_id} azsdk-rust-{crate_name}/{crate_version} {platform_info}")
5265
}
5366
None => format!("azsdk-rust-{crate_name}/{crate_version} {platform_info}"),
@@ -94,7 +107,6 @@ mod tests {
94107
fn with_application_id() {
95108
let options = UserAgentOptions {
96109
application_id: Some("my_app".to_string()),
97-
..Default::default()
98110
};
99111
let policy = UserAgentPolicy::new_with_rustc_version(
100112
Some("test"),
@@ -118,4 +130,37 @@ mod tests {
118130
format!("azsdk-rust-unknown/unknown (unknown; {OS}; {ARCH})")
119131
);
120132
}
133+
134+
#[test]
135+
#[should_panic(expected = "application_id must be shorter than 25 characters")]
136+
fn panics_when_application_id_too_long() {
137+
let options = UserAgentOptions {
138+
application_id: Some(
139+
"this_application_id_is_way_too_long_and_exceeds_limit".to_string(),
140+
), // 53 characters
141+
};
142+
let _policy = UserAgentPolicy::new_with_rustc_version(
143+
Some("test"),
144+
Some("1.2.3"),
145+
Some("4.5.6"),
146+
&options,
147+
);
148+
}
149+
150+
#[test]
151+
fn works_with_application_id_at_limit() {
152+
let options = UserAgentOptions {
153+
application_id: Some("exactly_24_characters!".to_string()), // Exactly 24 characters
154+
};
155+
let policy = UserAgentPolicy::new_with_rustc_version(
156+
Some("test"),
157+
Some("1.2.3"),
158+
Some("4.5.6"),
159+
&options,
160+
);
161+
assert_eq!(
162+
policy.header,
163+
format!("exactly_24_characters! azsdk-rust-test/1.2.3 (4.5.6; {OS}; {ARCH})")
164+
);
165+
}
121166
}

sdk/core/azure_core_test/src/credentials.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ use std::{env, sync::Arc};
1313
#[derive(Clone, Debug, Default)]
1414
pub struct MockCredential;
1515

16+
impl MockCredential {
17+
/// Create a new `MockCredential`.
18+
pub fn new() -> azure_core::Result<Arc<Self>> {
19+
Ok(Arc::new(MockCredential {}))
20+
}
21+
}
22+
1623
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
1724
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
1825
impl TokenCredential for MockCredential {

sdk/core/azure_core_test/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ pub mod http;
88
pub mod proxy;
99
pub mod recorded;
1010
mod recording;
11+
#[cfg(doctest)]
12+
mod root_readme;
1113
pub mod stream;
1214

1315
use azure_core::Error;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#![doc = include_str!("../../../../README.md")]
2+
3+
// Make sure the repository's root README.md has no syntactical errors.

0 commit comments

Comments
 (0)