@@ -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,48 @@ 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
106
- }
107
92
108
- // Fallback to Shared Key Credential.
109
- cred , err := sharedCredentialFromSecret (obj .Spec .Endpoint , secret )
110
- if err != nil {
111
- return
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
+ }
112
113
}
113
- if cred != nil {
114
- c .ServiceClient , err = azblob .NewServiceClientWithSharedKey (obj .Spec .Endpoint , cred , & azblob.ClientOptions {})
115
- return
114
+
115
+ // Compose token chain based on environment.
116
+ // This functions as a replacement for azidentity.NewDefaultAzureCredential
117
+ // to not shell out.
118
+ if token , err = chainCredentialWithSecret (secret ); err != nil {
119
+ err = fmt .Errorf ("failed to create environment credential chain: %w" , err )
120
+ return nil , err
116
121
}
117
122
118
- // Secret does not contain a valid set of credentials, fallback to simple client.
123
+ // Fallback to simple client.
119
124
c .ServiceClient , err = azblob .NewServiceClientWithNoCredential (obj .Spec .Endpoint , nil )
120
125
return
121
126
}
@@ -138,26 +143,19 @@ func ValidateSecret(secret *corev1.Secret) error {
138
143
}
139
144
}
140
145
}
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
146
if _ , hasClientID := secret .Data [clientIDField ]; hasClientID {
152
147
valid = true
153
148
}
154
149
if _ , hasAccountKey := secret .Data [accountKeyField ]; hasAccountKey {
155
150
valid = true
156
151
}
152
+ if _ , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
153
+ valid = true
154
+ }
157
155
158
156
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 )
157
+ 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'" ,
158
+ secret .Name , clientIDField , accountKeyField , tenantIDField , clientIDField , clientSecretField , tenantIDField , clientIDField , clientCertificateField )
161
159
}
162
160
return nil
163
161
}
@@ -285,40 +283,61 @@ func (c *BlobClient) ObjectIsNotFound(err error) bool {
285
283
return false
286
284
}
287
285
286
+ // tokenCredentialsFromSecret attempts to create an azcore.TokenCredential
287
+ // based on the data fields of the given Secret. It returns, in order:
288
+ // - azidentity.ClientSecretCredential when `tenantId`, `clientId` and
289
+ // `clientSecret` fields are found.
290
+ // - azidentity.ClientSecretCredential when `tenant`, `appId` and `password`
291
+ // fields are found. To match with the JSON from:
292
+ // https://docs.microsoft.com/en-us/azure/aks/kubernetes-service-principal?tabs=azure-cli#manually-create-a-service-principal
293
+ // - azidentity.ClientCertificateCredential when `tenantId`,
294
+ // `clientCertificate` (and optionally `clientCertificatePassword`) fields
295
+ // are found.
296
+ // - azidentity.ManagedIdentityCredential for a User ID, when a `clientId`
297
+ // field but no `tenantId` is found.
298
+ // - azidentity.ManagedIdentityCredential for a Resource ID, when a
299
+ // `resourceId` field is found.
300
+ // - Nil, if no valid set of credential fields was found.
288
301
func tokenCredentialFromSecret (secret * corev1.Secret ) (azcore.TokenCredential , error ) {
302
+ if secret == nil {
303
+ return nil , nil
304
+ }
305
+
289
306
clientID , hasClientID := secret .Data [clientIDField ]
290
307
if tenantID , hasTenantID := secret .Data [tenantIDField ]; hasTenantID && hasClientID {
291
308
if clientSecret , hasClientSecret := secret .Data [clientSecretField ]; hasClientSecret && len (clientSecret ) > 0 {
292
- return azidentity .NewClientSecretCredential (string (tenantID ), string (clientID ), string (clientSecret ), nil )
309
+ opts := & azidentity.ClientSecretCredentialOptions {}
310
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
311
+ opts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
312
+ }
313
+ return azidentity .NewClientSecretCredential (string (tenantID ), string (clientID ), string (clientSecret ), opts )
293
314
}
294
315
if clientCertificate , hasClientCertificate := secret .Data [clientCertificateField ]; hasClientCertificate && len (clientCertificate ) > 0 {
295
316
certs , key , err := azidentity .ParseCertificates (clientCertificate , secret .Data [clientCertificatePasswordField ])
296
317
if err != nil {
297
318
return nil , fmt .Errorf ("failed to parse client certificates: %w" , err )
298
319
}
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 )
320
+ opts := & azidentity.ClientCertificateCredentialOptions {}
321
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
322
+ opts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
306
323
}
324
+ if v , sendChain := secret .Data [clientCertificateSendChainField ]; sendChain {
325
+ opts .SendCertificateChain = string (v ) == "1" || strings .ToLower (string (v )) == "true"
326
+ }
327
+ return azidentity .NewClientCertificateCredential (string (tenantID ), string (clientID ), certs , key , opts )
307
328
}
308
329
}
309
330
if hasClientID {
310
331
return azidentity .NewManagedIdentityCredential (& azidentity.ManagedIdentityCredentialOptions {
311
332
ID : azidentity .ClientID (clientID ),
312
333
})
313
334
}
314
- if resourceID , hasResourceID := secret .Data [resourceIDField ]; hasResourceID {
315
- return azidentity .NewManagedIdentityCredential (& azidentity.ManagedIdentityCredentialOptions {
316
- ID : azidentity .ResourceID (resourceID ),
317
- })
318
- }
319
335
return nil , nil
320
336
}
321
337
338
+ // sharedCredentialFromSecret attempts to create an azblob.SharedKeyCredential
339
+ // based on the data fields of the given Secret. It returns nil if the Secret
340
+ // does not contain a valid set of credentials.
322
341
func sharedCredentialFromSecret (endpoint string , secret * corev1.Secret ) (* azblob.SharedKeyCredential , error ) {
323
342
if accountKey , hasAccountKey := secret .Data [accountKeyField ]; hasAccountKey {
324
343
accountName , err := extractAccountNameFromEndpoint (endpoint )
@@ -330,6 +349,37 @@ func sharedCredentialFromSecret(endpoint string, secret *corev1.Secret) (*azblob
330
349
return nil , nil
331
350
}
332
351
352
+ // chainCredentialWithSecret tries to create a set of tokens, and returns an
353
+ // azidentity.ChainedTokenCredential if at least one of the following tokens was
354
+ // successfully created:
355
+ // - azidentity.EnvironmentCredential
356
+ // - azidentity.ManagedIdentityCredential
357
+ // If a Secret with an `authorityHost` is provided, this is set on the
358
+ // azidentity.EnvironmentCredentialOptions. It may return nil.
359
+ func chainCredentialWithSecret (secret * corev1.Secret ) (azcore.TokenCredential , error ) {
360
+ var creds []azcore.TokenCredential
361
+
362
+ credOpts := & azidentity.EnvironmentCredentialOptions {}
363
+ if secret != nil {
364
+ if authorityHost , hasAuthorityHost := secret .Data [authorityHostField ]; hasAuthorityHost {
365
+ credOpts .AuthorityHost = azidentity .AuthorityHost (authorityHost )
366
+ }
367
+ }
368
+
369
+ if token , _ := azidentity .NewEnvironmentCredential (credOpts ); token != nil {
370
+ creds = append (creds , token )
371
+ }
372
+ if token , _ := azidentity .NewManagedIdentityCredential (nil ); token != nil {
373
+ creds = append (creds , token )
374
+ }
375
+
376
+ if len (creds ) > 0 {
377
+ return azidentity .NewChainedTokenCredential (creds , nil )
378
+ }
379
+
380
+ return nil , nil
381
+ }
382
+
333
383
// extractAccountNameFromEndpoint extracts the Azure account name from the
334
384
// provided endpoint URL. It parses the endpoint as a URL, and returns the
335
385
// first subdomain as the assumed account name.
0 commit comments