@@ -44,18 +44,14 @@ var (
44
44
)
45
45
46
46
const (
47
- resourceIDField = "resourceId"
48
- clientIDField = "clientId"
49
- tenantIDField = "tenantId"
50
- clientSecretField = "clientSecret"
51
- clientCertificateField = "clientCertificate"
52
- clientCertificatePasswordField = "clientCertificatePassword"
53
- accountKeyField = "accountKey"
54
-
55
- // Ref: https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal
56
- tenantField = "tenant"
57
- appIDField = "appId"
58
- passwordField = "password"
47
+ clientIDField = "clientId"
48
+ tenantIDField = "tenantId"
49
+ clientSecretField = "clientSecret"
50
+ clientCertificateField = "clientCertificate"
51
+ clientCertificatePasswordField = "clientCertificatePassword"
52
+ clientCertificateSendChainField = "clientCertificateSendChain"
53
+ authorityHostField = "authorityHost"
54
+ accountKeyField = "accountKey"
59
55
)
60
56
61
57
// BlobClient is a minimal Azure Blob client for fetching objects.
@@ -83,39 +79,53 @@ type BlobClient struct {
83
79
// - azblob.SharedKeyCredential when an `accountKey` field is found.
84
80
// The account name is extracted from the endpoint specified on the Bucket
85
81
// object.
82
+ // - azidentity.ChainedTokenCredential with azidentity.EnvironmentCredential
83
+ // and azidentity.ManagedIdentityCredential with defaults if no Secret is
84
+ // given.
86
85
//
87
- // If no credentials are found, a simple client without credentials is
88
- // returned.
86
+ // If no credentials are found, and the azidentity.ChainedTokenCredential can
87
+ // not be established. A simple client without credentials is returned.
89
88
func NewClient (obj * sourcev1.Bucket , secret * corev1.Secret ) (c * BlobClient , err error ) {
90
89
c = & BlobClient {}
91
90
92
- // Without a Secret, we can return a simple client.
93
- if secret == nil || len (secret .Data ) == 0 {
94
- c .ServiceClient , err = azblob .NewServiceClientWithNoCredential (obj .Spec .Endpoint , nil )
95
- return
96
- }
97
-
98
- // Attempt AAD Token Credential options first.
99
91
var token azcore.TokenCredential
100
- if token , err = tokenCredentialFromSecret (secret ); err != nil {
101
- return
102
- }
103
- if token != nil {
104
- c .ServiceClient , err = azblob .NewServiceClient (obj .Spec .Endpoint , token , nil )
105
- return
92
+
93
+ if secret != nil && len (secret .Data ) > 0 {
94
+ // Attempt AAD Token Credential options first.
95
+ if token , err = tokenCredentialFromSecret (secret ); err != nil {
96
+ err = fmt .Errorf ("failed to create token credential from '%s' Secret: %w" , secret .Name , err )
97
+ return
98
+ }
99
+ if token != nil {
100
+ c .ServiceClient , err = azblob .NewServiceClient (obj .Spec .Endpoint , token , nil )
101
+ return
102
+ }
103
+
104
+ // Fallback to Shared Key Credential.
105
+ var cred * azblob.SharedKeyCredential
106
+ if cred , err = sharedCredentialFromSecret (obj .Spec .Endpoint , secret ); err != nil {
107
+ return
108
+ }
109
+ if cred != nil {
110
+ c .ServiceClient , err = azblob .NewServiceClientWithSharedKey (obj .Spec .Endpoint , cred , & azblob.ClientOptions {})
111
+ return
112
+ }
106
113
}
107
114
108
- // Fallback to Shared Key Credential.
109
- cred , err := sharedCredentialFromSecret (obj .Spec .Endpoint , secret )
115
+ // Compose token chain based on environment.
116
+ // This functions as a replacement for azidentity.NewDefaultAzureCredential
117
+ // to not shell out.
118
+ token , err = chainCredentialWithSecret (secret )
110
119
if err != nil {
111
- return
120
+ err = fmt .Errorf ("failed to create environment credential chain: %w" , err )
121
+ return nil , err
112
122
}
113
- if cred != nil {
114
- c .ServiceClient , err = azblob .NewServiceClientWithSharedKey (obj .Spec .Endpoint , cred , & azblob. ClientOptions {} )
123
+ if token != nil {
124
+ c .ServiceClient , err = azblob .NewServiceClient (obj .Spec .Endpoint , token , nil )
115
125
return
116
126
}
117
127
118
- // Secret does not contain a valid set of credentials, fallback to simple client.
128
+ // Fallback to simple client.
119
129
c .ServiceClient , err = azblob .NewServiceClientWithNoCredential (obj .Spec .Endpoint , nil )
120
130
return
121
131
}
@@ -138,26 +148,19 @@ func ValidateSecret(secret *corev1.Secret) error {
138
148
}
139
149
}
140
150
}
141
- if _ , hasTenant := secret .Data [tenantField ]; hasTenant {
142
- if _ , hasAppID := secret .Data [appIDField ]; hasAppID {
143
- if _ , hasPassword := secret .Data [passwordField ]; hasPassword {
144
- valid = true
145
- }
146
- }
147
- }
148
- if _ , hasResourceID := secret .Data [resourceIDField ]; hasResourceID {
149
- valid = true
150
- }
151
151
if _ , hasClientID := secret .Data [clientIDField ]; hasClientID {
152
152
valid = true
153
153
}
154
154
if _ , hasAccountKey := secret .Data [accountKeyField ]; hasAccountKey {
155
155
valid = true
156
156
}
157
+ if _ , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
158
+ valid = true
159
+ }
157
160
158
161
if ! valid {
159
- return fmt .Errorf ("invalid '%s' secret data: requires a '%s', '%s', or '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'" ,
160
- secret .Name , resourceIDField , clientIDField , accountKeyField , tenantIDField , clientIDField , clientSecretField , tenantIDField , clientIDField , clientCertificateField )
162
+ return fmt .Errorf ("invalid '%s' secret data: requires a '%s' or '%s' field, a combination of '%s', '%s' and '%s', or '%s', '%s' and '%s'" ,
163
+ secret .Name , clientIDField , accountKeyField , tenantIDField , clientIDField , clientSecretField , tenantIDField , clientIDField , clientCertificateField )
161
164
}
162
165
return nil
163
166
}
@@ -285,40 +288,61 @@ func (c *BlobClient) ObjectIsNotFound(err error) bool {
285
288
return false
286
289
}
287
290
291
+ // tokenCredentialsFromSecret attempts to create an azcore.TokenCredential
292
+ // based on the data fields of the given Secret. It returns, in order:
293
+ // - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
294
+ // `clientSecret` fields are found.
295
+ // - azidentity.ClientSecretCredential when `tenant`, `appId` and `password`
296
+ // fields are found. To match with the JSON from:
297
+ // https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal
298
+ // - azidentity.ClientCertificateCredential when `tenantId`,
299
+ // `clientCertificate` (and optionally `clientCertificatePassword`) fields
300
+ // are found.
301
+ // - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
302
+ // field but no `tenantId` is found.
303
+ // - azidentity.ManagedIdentityCredential for a Resource ID, when a
304
+ // `resourceId` field is found.
305
+ // - Nil, if no valid set of credential fields was found.
288
306
func tokenCredentialFromSecret (secret * corev1.Secret ) (azcore.TokenCredential , error ) {
307
+ if secret == nil {
308
+ return nil , nil
309
+ }
310
+
289
311
clientID , hasClientID := secret .Data [clientIDField ]
290
312
if tenantID , hasTenantID := secret .Data [tenantIDField ]; hasTenantID && hasClientID {
291
313
if clientSecret , hasClientSecret := secret .Data [clientSecretField ]; hasClientSecret && len (clientSecret ) > 0 {
292
- return azidentity .NewClientSecretCredential (string (tenantID ), string (clientID ), string (clientSecret ), nil )
314
+ opts := & azidentity.ClientSecretCredentialOptions {}
315
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
316
+ opts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
317
+ }
318
+ return azidentity .NewClientSecretCredential (string (tenantID ), string (clientID ), string (clientSecret ), opts )
293
319
}
294
320
if clientCertificate , hasClientCertificate := secret .Data [clientCertificateField ]; hasClientCertificate && len (clientCertificate ) > 0 {
295
321
certs , key , err := azidentity .ParseCertificates (clientCertificate , secret .Data [clientCertificatePasswordField ])
296
322
if err != nil {
297
323
return nil , fmt .Errorf ("failed to parse client certificates: %w" , err )
298
324
}
299
- return azidentity .NewClientCertificateCredential (string (tenantID ), string (clientID ), certs , key , nil )
300
- }
301
- }
302
- if tenant , hasTenant := secret .Data [tenantField ]; hasTenant {
303
- if appId , hasAppID := secret .Data [appIDField ]; hasAppID {
304
- if password , hasPassword := secret .Data [passwordField ]; hasPassword {
305
- return azidentity .NewClientSecretCredential (string (tenant ), string (appId ), string (password ), nil )
325
+ opts := & azidentity.ClientCertificateCredentialOptions {}
326
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
327
+ opts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
328
+ }
329
+ if v , sendChain := secret .Data [clientCertificateSendChainField ]; sendChain {
330
+ opts .SendCertificateChain = string (v ) == "1" || strings .ToLower (string (v )) == "true"
306
331
}
332
+ return azidentity .NewClientCertificateCredential (string (tenantID ), string (clientID ), certs , key , opts )
307
333
}
308
334
}
309
335
if hasClientID {
310
336
return azidentity .NewManagedIdentityCredential (& azidentity.ManagedIdentityCredentialOptions {
311
337
ID : azidentity .ClientID (clientID ),
312
338
})
313
339
}
314
- if resourceID , hasResourceID := secret .Data [resourceIDField ]; hasResourceID {
315
- return azidentity .NewManagedIdentityCredential (& azidentity.ManagedIdentityCredentialOptions {
316
- ID : azidentity .ResourceID (resourceID ),
317
- })
318
- }
319
340
return nil , nil
320
341
}
321
342
343
+ // sharedCredentialFromSecret attempts to create an azblob.SharedKeyCredential
344
+ // based on the data fields of the given Secret. It returns nil if the Secret
345
+ // does not contain a valid set of credentials.
322
346
func sharedCredentialFromSecret (endpoint string , secret * corev1.Secret ) (* azblob.SharedKeyCredential , error ) {
323
347
if accountKey , hasAccountKey := secret .Data [accountKeyField ]; hasAccountKey {
324
348
accountName , err := extractAccountNameFromEndpoint (endpoint )
@@ -330,6 +354,37 @@ func sharedCredentialFromSecret(endpoint string, secret *corev1.Secret) (*azblob
330
354
return nil , nil
331
355
}
332
356
357
+ // chainCredentialWithSecret tries to create a set of tokens, and returns an
358
+ // azidentity.ChainedTokenCredential if at least one of the following tokens was
359
+ // successfully created:
360
+ // - azidentity.EnvironmentCredential
361
+ // - azidentity.ManagedIdentityCredential
362
+ // If a Secret with an `authorityHost` is provided, this is set on the
363
+ // azidentity.EnvironmentCredentialOptions. It may return nil.
364
+ func chainCredentialWithSecret (secret * corev1.Secret ) (azcore.TokenCredential , error ) {
365
+ var creds []azcore.TokenCredential
366
+
367
+ credOpts := & azidentity.EnvironmentCredentialOptions {}
368
+ if secret != nil {
369
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
370
+ credOpts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
371
+ }
372
+ }
373
+
374
+ if token , _ := azidentity .NewEnvironmentCredential (credOpts ); token != nil {
375
+ creds = append (creds , token )
376
+ }
377
+ if token , _ := azidentity .NewManagedIdentityCredential (nil ); token != nil {
378
+ creds = append (creds , token )
379
+ }
380
+
381
+ if len (creds ) > 0 {
382
+ return azidentity .NewChainedTokenCredential (creds , nil )
383
+ }
384
+
385
+ return nil , nil
386
+ }
387
+
333
388
// extractAccountNameFromEndpoint extracts the Azure account name from the
334
389
// provided endpoint URL. It parses the endpoint as a URL, and returns the
335
390
// first subdomain as the assumed account name.
0 commit comments