@@ -49,11 +49,130 @@ impl KeyValueAzureCosmosRuntimeConfigOptions {
4949pub enum KeyValueAzureCosmosAuthOptions {
5050 /// Runtime Config values indicates the account and key have been specified directly
5151 RuntimeConfigValues ( KeyValueAzureCosmosRuntimeConfigOptions ) ,
52- /// Uses DeveloperToolsCredential when the runtime config omits `key`.
52+ /// An Azure AD token credential, used when the runtime config omits `key`.
5353 ///
54- /// Athenticated via developer tools only: Azure CLI (`az login`),
55- /// then Azure Developer CLI (`azd auth login`).
54+ /// The specific credential is chosen by the operator via the `auth_type`
55+ /// runtime-config field (defaulting to developer tools for local
56+ /// development). There is deliberately no fallback *between* credential
57+ /// types: `azure_identity` 1.0 removed `EnvironmentCredential` /
58+ /// `DefaultAzureCredential` because silently trying a different identity
59+ /// after one fails is a security footgun (Azure/azure-sdk-for-rust#2283).
60+ /// This mirrors their recommended "specific credential" pattern.
61+ AadCredential ( AzureCredentialKind ) ,
62+ }
63+
64+ /// The specific Azure AD credential to use when authenticating to Cosmos
65+ /// without an account key.
66+ ///
67+ /// Each variant maps to exactly one `azure_identity` credential; the operator
68+ /// names the one matching their deployment via the `auth_type` runtime-config
69+ /// field. Modeled on `azure_identity`'s `specific_credential.rs` example.
70+ #[ derive( Clone , Debug , Default , PartialEq , Eq ) ]
71+ pub enum AzureCredentialKind {
72+ /// Developer tools: Azure CLI (`az login`), then Azure Developer CLI
73+ /// (`azd auth login`). Intended for local development; the default when
74+ /// `auth_type` is omitted.
75+ #[ default]
5676 DeveloperTools ,
77+ /// Managed identity (Azure VM, App Service, or AKS with managed identity).
78+ ///
79+ /// `client_id` optionally selects a *user-assigned* managed identity by its
80+ /// client ID; when `None`, the SDK uses the *system-assigned* identity.
81+ ManagedIdentity { client_id : Option < String > } ,
82+ /// Workload identity (AKS federated token).
83+ WorkloadIdentity ,
84+ /// Service principal authenticated with a client secret. Reads
85+ /// `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, and `AZURE_CLIENT_SECRET` from the
86+ /// environment — the same variables the legacy SDK's `EnvironmentCredential`
87+ /// used, so existing deployments keep working without config changes.
88+ ServicePrincipal ,
89+ }
90+
91+ impl AzureCredentialKind {
92+ /// Parses the `auth_type` runtime-config value into a credential kind.
93+ ///
94+ /// `None` (the field omitted) defaults to [`AzureCredentialKind::DeveloperTools`],
95+ /// intended for local development. An unrecognized value is an error rather
96+ /// than a silent fallback.
97+ ///
98+ /// `client_id` selects a user-assigned managed identity by its client ID. It
99+ /// only applies to `managed_identity`; with any other `auth_type` it is
100+ /// ignored.
101+ pub fn from_auth_type ( auth_type : Option < & str > , client_id : Option < String > ) -> Result < Self > {
102+ // Case-insensitive, but the value must be one of the canonical
103+ // snake_case names: a non-canonical form (e.g. a space or hyphen
104+ // separator) is rejected rather than silently normalized, so the
105+ // accepted set matches exactly what the docs and the error below list.
106+ // `client_id` is consumed only by the `managed_identity` arm; for any
107+ // other auth type it is simply dropped.
108+ match auth_type. map ( |s| s. to_lowercase ( ) ) . as_deref ( ) {
109+ None => Ok ( Self :: default ( ) ) ,
110+ Some ( "developer_tools" ) => Ok ( Self :: DeveloperTools ) ,
111+ Some ( "managed_identity" ) => Ok ( Self :: ManagedIdentity { client_id } ) ,
112+ Some ( "workload_identity" ) => Ok ( Self :: WorkloadIdentity ) ,
113+ Some ( "service_principal" ) => Ok ( Self :: ServicePrincipal ) ,
114+ Some ( other) => anyhow:: bail!(
115+ "unknown Azure Cosmos `auth_type` {other:?}; expected one of \
116+ \" managed_identity\" , \" workload_identity\" , \" service_principal\" , \
117+ or \" developer_tools\" (or set `key` for account-key auth)"
118+ ) ,
119+ }
120+ }
121+
122+ /// Constructs the corresponding `azure_identity` token credential.
123+ ///
124+ /// This runs the credential's own setup (which may fail — e.g. if the
125+ /// environment for workload identity or service principal is absent), so it
126+ /// is called lazily when the Cosmos client is first built.
127+ fn credential ( & self ) -> azure_core:: Result < Arc < dyn TokenCredential > > {
128+ match self {
129+ Self :: DeveloperTools => Ok ( azure_identity:: DeveloperToolsCredential :: new ( None ) ?) ,
130+ Self :: ManagedIdentity { client_id } => {
131+ // Pass options only when a user-assigned client ID was given;
132+ // `None` keeps the SDK default of the system-assigned identity.
133+ let options =
134+ client_id
135+ . as_ref ( )
136+ . map ( |id| azure_identity:: ManagedIdentityCredentialOptions {
137+ user_assigned_id : Some ( azure_identity:: UserAssignedId :: ClientId (
138+ id. clone ( ) ,
139+ ) ) ,
140+ ..Default :: default ( )
141+ } ) ;
142+ Ok ( azure_identity:: ManagedIdentityCredential :: new ( options) ?)
143+ }
144+ Self :: WorkloadIdentity => Ok ( azure_identity:: WorkloadIdentityCredential :: new ( None ) ?) ,
145+ Self :: ServicePrincipal => {
146+ // azure_identity 1.0 removed the env-driven `EnvironmentCredential`,
147+ // so read the same variables it used and pass them to
148+ // `ClientSecretCredential` explicitly. A missing variable surfaces
149+ // here (lazily, when the client is first built) as a clear error.
150+ let tenant_id = service_principal_env ( "AZURE_TENANT_ID" ) ?;
151+ let client_id = service_principal_env ( "AZURE_CLIENT_ID" ) ?;
152+ let secret = service_principal_env ( "AZURE_CLIENT_SECRET" ) ?;
153+ Ok ( azure_identity:: ClientSecretCredential :: new (
154+ & tenant_id,
155+ client_id,
156+ secret. into ( ) ,
157+ None ,
158+ ) ?)
159+ }
160+ }
161+ }
162+ }
163+
164+ /// Reads a required Service Principal environment variable, mapping a missing
165+ /// value to a clear credential error.
166+ fn service_principal_env ( name : & str ) -> azure_core:: Result < String > {
167+ std:: env:: var ( name) . map_err ( |_| {
168+ azure_core:: Error :: with_message (
169+ azure_core:: error:: ErrorKind :: Credential ,
170+ format ! (
171+ "Azure Cosmos `service_principal` auth requires the `{name}` \
172+ environment variable to be set"
173+ ) ,
174+ )
175+ } )
57176}
58177
59178impl KeyValueAzureCosmos {
@@ -91,9 +210,8 @@ async fn build_cosmos_client(
91210 KeyValueAzureCosmosAuthOptions :: RuntimeConfigValues ( config) => {
92211 AccountReference :: with_authentication_key ( endpoint, Secret :: from ( config. key . clone ( ) ) )
93212 }
94- KeyValueAzureCosmosAuthOptions :: DeveloperTools => {
95- let credential: Arc < dyn TokenCredential > =
96- azure_identity:: DeveloperToolsCredential :: new ( None ) . map_err ( log_error) ?;
213+ KeyValueAzureCosmosAuthOptions :: AadCredential ( kind) => {
214+ let credential = kind. credential ( ) . map_err ( log_error) ?;
97215 AccountReference :: with_credential ( endpoint, credential)
98216 }
99217 } ;
0 commit comments