Skip to content

Commit 0742f84

Browse files
committed
feat: fall back to env vars for clientID/tenantID with OIDCTokenFile
When Credentials.Source is OIDCTokenFile, clientID and tenantID are now optional in the ProviderConfig spec. oidcAuth() reads AZURE_CLIENT_ID and AZURE_TENANT_ID from the environment when the spec fields are absent, allowing the Azure Workload Identity webhook to inject per-pod managed identity credentials. Spec values take precedence over env vars when set. refs: #1166 Signed-off-by: Jeff Davis <mr.jefedavis@gmail.com>
1 parent 7db2e5a commit 0742f84

File tree

5 files changed

+216
-18
lines changed

5 files changed

+216
-18
lines changed

apis/namespaced/v1beta1/types.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ type ProviderConfigSpec struct {
1616
// Credentials required to authenticate to this provider.
1717
Credentials ProviderCredentials `json:"credentials"`
1818

19-
// ClientID is the user-assigned managed identity's ID
20-
// when Credentials.Source is `InjectedIdentity`. If unset and
21-
// Credentials.Source is `InjectedIdentity`, then a system-assigned
22-
// managed identity is used.
19+
// ClientID is the client ID of the managed identity or service principal.
20+
// When Credentials.Source is `InjectedIdentity` (MSI), this selects a
21+
// user-assigned identity; if unset, the system-assigned identity is used.
22+
// When Credentials.Source is `OIDCTokenFile`, this field is optional: if
23+
// unset, the value is read from the AZURE_CLIENT_ID environment variable,
24+
// which the Azure Workload Identity webhook injects per pod from the
25+
// ServiceAccount annotation. This allows a single ClusterProviderConfig to
26+
// serve multiple providers, each using its own managed identity.
2327
// +optional
2428
ClientID *string `json:"clientID,omitempty"`
2529

@@ -32,6 +36,9 @@ type ProviderConfigSpec struct {
3236
// TenantID is the Azure AD tenant ID to be used.
3337
// If unset, tenant ID from Credentials will be used.
3438
// Required if Credentials.Source is InjectedIdentity.
39+
// When Credentials.Source is `OIDCTokenFile`, this field is optional: if
40+
// unset, the value is read from the AZURE_TENANT_ID environment variable,
41+
// which the Azure Workload Identity webhook injects per pod.
3542
// +kubebuilder:validation:Optional
3643
TenantID *string `json:"tenantID,omitempty"`
3744

internal/clients/azure.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package clients
77
import (
88
"context"
99
"encoding/json"
10+
"os"
1011
"strings"
1112
"sync/atomic"
1213

@@ -32,8 +33,11 @@ const (
3233
errExtractCredentials = "cannot extract credentials"
3334
errUnmarshalCredentials = "cannot unmarshal Azure credentials as JSON"
3435
errSubscriptionIDNotSet = "subscription ID must be set in ProviderConfig when credential source is InjectedIdentity, OIDCTokenFile or Upbound"
35-
errTenantIDNotSet = "tenant ID must be set in ProviderConfig when credential source is InjectedIdentity, OIDCTokenFile or Upbound"
36-
errClientIDNotSet = "client ID must be set in ProviderConfig when credential source is OIDCTokenFile or Upbound"
36+
errTenantIDNotSet = "tenant ID must be set in ProviderConfig or AZURE_TENANT_ID env var when credential source is InjectedIdentity, OIDCTokenFile or Upbound"
37+
errClientIDNotSet = "client ID must be set in ProviderConfig or AZURE_CLIENT_ID env var when credential source is OIDCTokenFile or Upbound"
38+
// Environment variable names for Azure Workload Identity webhook-injected values
39+
envAzureClientID = "AZURE_CLIENT_ID"
40+
envAzureTenantID = "AZURE_TENANT_ID"
3741
// Azure service principal credentials file JSON keys
3842
keyAzureSubscriptionID = "subscriptionId"
3943
keyAzureClientID = "clientId"
@@ -204,20 +208,35 @@ func oidcAuth(pcSpec *namespacedv1beta1.ProviderConfigSpec, ps *terraform.Setup)
204208
if pcSpec.SubscriptionID == nil || len(*pcSpec.SubscriptionID) == 0 {
205209
return errors.New(errSubscriptionIDNotSet)
206210
}
207-
if pcSpec.TenantID == nil || len(*pcSpec.TenantID) == 0 {
211+
212+
// tenantID and clientID are optional in the spec for OIDCTokenFile: when the
213+
// Azure Workload Identity webhook is enabled on the provider pod, it injects
214+
// AZURE_TENANT_ID and AZURE_CLIENT_ID per pod, enabling per-provider managed
215+
// identities without a cluster-wide ClusterProviderConfig per identity.
216+
tenantID := os.Getenv(envAzureTenantID)
217+
if pcSpec.TenantID != nil && len(*pcSpec.TenantID) > 0 {
218+
tenantID = *pcSpec.TenantID
219+
}
220+
if tenantID == "" {
208221
return errors.New(errTenantIDNotSet)
209222
}
210-
if pcSpec.ClientID == nil || len(*pcSpec.ClientID) == 0 {
223+
224+
clientID := os.Getenv(envAzureClientID)
225+
if pcSpec.ClientID != nil && len(*pcSpec.ClientID) > 0 {
226+
clientID = *pcSpec.ClientID
227+
}
228+
if clientID == "" {
211229
return errors.New(errClientIDNotSet)
212230
}
231+
213232
// OIDC Token File Path defaults to a projected-volume path mounted in the pod running in the AKS cluster, when workload identity is enabled on the pod.
214233
ps.Configuration[keyOidcTokenFilePath] = defaultOidcTokenFilePath
215234
if pcSpec.OidcTokenFilePath != nil {
216235
ps.Configuration[keyOidcTokenFilePath] = *pcSpec.OidcTokenFilePath
217236
}
218237
ps.Configuration[keySubscriptionID] = *pcSpec.SubscriptionID
219-
ps.Configuration[keyTenantID] = *pcSpec.TenantID
220-
ps.Configuration[keyClientID] = *pcSpec.ClientID
238+
ps.Configuration[keyTenantID] = tenantID
239+
ps.Configuration[keyClientID] = clientID
221240
ps.Configuration[keyUseOIDC] = "true"
222241
if pcSpec.Environment != nil {
223242
ps.Configuration[keyEnvironment] = *pcSpec.Environment

internal/clients/azure_test.go

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-FileCopyrightText: 2024 The Crossplane Authors <https://crossplane.io>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package clients
6+
7+
import (
8+
"testing"
9+
10+
"github.com/crossplane/crossplane-runtime/v2/pkg/test"
11+
"github.com/google/go-cmp/cmp"
12+
"github.com/crossplane/upjet/v2/pkg/terraform"
13+
14+
namespacedv1beta1 "github.com/upbound/provider-azure/v2/apis/namespaced/v1beta1"
15+
)
16+
17+
func ptr(s string) *string { return &s }
18+
19+
func TestOIDCAuth(t *testing.T) {
20+
subscriptionID := "sub-123"
21+
22+
cases := map[string]struct {
23+
pcSpec *namespacedv1beta1.ProviderConfigSpec
24+
envVars map[string]string
25+
want terraform.ProviderConfiguration
26+
wantErr string
27+
}{
28+
"AllFieldsInSpec": {
29+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
30+
SubscriptionID: ptr(subscriptionID),
31+
TenantID: ptr("tenant-spec"),
32+
ClientID: ptr("client-spec"),
33+
},
34+
want: terraform.ProviderConfiguration{
35+
keyOidcTokenFilePath: defaultOidcTokenFilePath,
36+
keySubscriptionID: subscriptionID,
37+
keyTenantID: "tenant-spec",
38+
keyClientID: "client-spec",
39+
keyUseOIDC: "true",
40+
},
41+
},
42+
"ClientIDAndTenantIDFromEnvVars": {
43+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
44+
SubscriptionID: ptr(subscriptionID),
45+
},
46+
envVars: map[string]string{
47+
envAzureClientID: "client-env",
48+
envAzureTenantID: "tenant-env",
49+
},
50+
want: terraform.ProviderConfiguration{
51+
keyOidcTokenFilePath: defaultOidcTokenFilePath,
52+
keySubscriptionID: subscriptionID,
53+
keyTenantID: "tenant-env",
54+
keyClientID: "client-env",
55+
keyUseOIDC: "true",
56+
},
57+
},
58+
"SpecTakesPrecedenceOverEnvVars": {
59+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
60+
SubscriptionID: ptr(subscriptionID),
61+
TenantID: ptr("tenant-spec"),
62+
ClientID: ptr("client-spec"),
63+
},
64+
envVars: map[string]string{
65+
envAzureClientID: "client-env",
66+
envAzureTenantID: "tenant-env",
67+
},
68+
want: terraform.ProviderConfiguration{
69+
keyOidcTokenFilePath: defaultOidcTokenFilePath,
70+
keySubscriptionID: subscriptionID,
71+
keyTenantID: "tenant-spec",
72+
keyClientID: "client-spec",
73+
keyUseOIDC: "true",
74+
},
75+
},
76+
"CustomTokenFilePath": {
77+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
78+
SubscriptionID: ptr(subscriptionID),
79+
TenantID: ptr("tenant-spec"),
80+
ClientID: ptr("client-spec"),
81+
OidcTokenFilePath: ptr("/custom/token/path"),
82+
},
83+
want: terraform.ProviderConfiguration{
84+
keyOidcTokenFilePath: "/custom/token/path",
85+
keySubscriptionID: subscriptionID,
86+
keyTenantID: "tenant-spec",
87+
keyClientID: "client-spec",
88+
keyUseOIDC: "true",
89+
},
90+
},
91+
"MissingSubscriptionID": {
92+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{},
93+
wantErr: errSubscriptionIDNotSet,
94+
},
95+
"MissingTenantIDNoEnvVar": {
96+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
97+
SubscriptionID: ptr(subscriptionID),
98+
ClientID: ptr("client-spec"),
99+
},
100+
wantErr: errTenantIDNotSet,
101+
},
102+
"MissingClientIDNoEnvVar": {
103+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
104+
SubscriptionID: ptr(subscriptionID),
105+
TenantID: ptr("tenant-spec"),
106+
},
107+
wantErr: errClientIDNotSet,
108+
},
109+
"TenantIDFromEnvClientIDMissing": {
110+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
111+
SubscriptionID: ptr(subscriptionID),
112+
},
113+
envVars: map[string]string{
114+
envAzureTenantID: "tenant-env",
115+
},
116+
wantErr: errClientIDNotSet,
117+
},
118+
"ClientIDFromEnvTenantIDMissing": {
119+
pcSpec: &namespacedv1beta1.ProviderConfigSpec{
120+
SubscriptionID: ptr(subscriptionID),
121+
},
122+
envVars: map[string]string{
123+
envAzureClientID: "client-env",
124+
},
125+
wantErr: errTenantIDNotSet,
126+
},
127+
}
128+
129+
for name, tc := range cases {
130+
t.Run(name, func(t *testing.T) {
131+
// Set env vars for this test case.
132+
for k, v := range tc.envVars {
133+
t.Setenv(k, v)
134+
}
135+
136+
ps := &terraform.Setup{
137+
Configuration: terraform.ProviderConfiguration{},
138+
}
139+
140+
err := oidcAuth(tc.pcSpec, ps)
141+
142+
if tc.wantErr != "" {
143+
if diff := cmp.Diff(tc.wantErr, err.Error(), test.EquateConditions()); diff != "" {
144+
t.Errorf("oidcAuth() error mismatch (-want +got):\n%s", diff)
145+
}
146+
return
147+
}
148+
149+
if err != nil {
150+
t.Fatalf("oidcAuth() unexpected error: %v", err)
151+
}
152+
153+
if diff := cmp.Diff(tc.want, ps.Configuration); diff != "" {
154+
t.Errorf("oidcAuth() configuration mismatch (-want +got):\n%s", diff)
155+
}
156+
})
157+
}
158+
}

package/crds/azure.m.upbound.io_clusterproviderconfigs.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ spec:
5353
properties:
5454
clientID:
5555
description: |-
56-
ClientID is the user-assigned managed identity's ID
57-
when Credentials.Source is `InjectedIdentity`. If unset and
58-
Credentials.Source is `InjectedIdentity`, then a system-assigned
59-
managed identity is used.
56+
ClientID is the client ID of the managed identity or service principal.
57+
When Credentials.Source is `InjectedIdentity` (MSI), this selects a
58+
user-assigned identity; if unset, the system-assigned identity is used.
59+
When Credentials.Source is `OIDCTokenFile`, this field is optional: if
60+
unset, the value is read from the AZURE_CLIENT_ID environment variable,
61+
which the Azure Workload Identity webhook injects per pod from the
62+
ServiceAccount annotation. This allows a single ClusterProviderConfig to
63+
serve multiple providers, each using its own managed identity.
6064
type: string
6165
credentials:
6266
description: Credentials required to authenticate to this provider.
@@ -148,6 +152,9 @@ spec:
148152
TenantID is the Azure AD tenant ID to be used.
149153
If unset, tenant ID from Credentials will be used.
150154
Required if Credentials.Source is InjectedIdentity.
155+
When Credentials.Source is `OIDCTokenFile`, this field is optional: if
156+
unset, the value is read from the AZURE_TENANT_ID environment variable,
157+
which the Azure Workload Identity webhook injects per pod.
151158
type: string
152159
required:
153160
- credentials

package/crds/azure.m.upbound.io_providerconfigs.yaml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,14 @@ spec:
5353
properties:
5454
clientID:
5555
description: |-
56-
ClientID is the user-assigned managed identity's ID
57-
when Credentials.Source is `InjectedIdentity`. If unset and
58-
Credentials.Source is `InjectedIdentity`, then a system-assigned
59-
managed identity is used.
56+
ClientID is the client ID of the managed identity or service principal.
57+
When Credentials.Source is `InjectedIdentity` (MSI), this selects a
58+
user-assigned identity; if unset, the system-assigned identity is used.
59+
When Credentials.Source is `OIDCTokenFile`, this field is optional: if
60+
unset, the value is read from the AZURE_CLIENT_ID environment variable,
61+
which the Azure Workload Identity webhook injects per pod from the
62+
ServiceAccount annotation. This allows a single ClusterProviderConfig to
63+
serve multiple providers, each using its own managed identity.
6064
type: string
6165
credentials:
6266
description: Credentials required to authenticate to this provider.
@@ -148,6 +152,9 @@ spec:
148152
TenantID is the Azure AD tenant ID to be used.
149153
If unset, tenant ID from Credentials will be used.
150154
Required if Credentials.Source is InjectedIdentity.
155+
When Credentials.Source is `OIDCTokenFile`, this field is optional: if
156+
unset, the value is read from the AZURE_TENANT_ID environment variable,
157+
which the Azure Workload Identity webhook injects per pod.
151158
type: string
152159
required:
153160
- credentials

0 commit comments

Comments
 (0)