Skip to content

Commit 49027c6

Browse files
mt5225claude
andauthored
object/azure: add managed identity authentication (#6424)
Co-authored-by: Claude <[email protected]> Co-authored-by: mt5225 <[email protected]>
1 parent a1b0b09 commit 49027c6

File tree

3 files changed

+114
-15
lines changed

3 files changed

+114
-15
lines changed

go.mod

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
cloud.google.com/go/compute/metadata v0.5.2
77
cloud.google.com/go/storage v1.48.0
88
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.13.0
9+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
910
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.1
1011
github.com/DataDog/zstd v1.5.6
1112
github.com/IBM/ibm-cos-sdk-go v1.12.1
@@ -113,6 +114,7 @@ require (
113114
git.apache.org/thrift.git v0.13.0 // indirect
114115
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
115116
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect
117+
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
116118
github.com/BurntSushi/toml v1.3.2 // indirect
117119
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
118120
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
@@ -188,6 +190,7 @@ require (
188190
github.com/go-resty/resty/v2 v2.13.1 // indirect
189191
github.com/gogo/protobuf v1.3.2 // indirect
190192
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
193+
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
191194
github.com/golang/glog v1.2.2 // indirect
192195
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
193196
github.com/golang/protobuf v1.5.4 // indirect
@@ -228,6 +231,7 @@ require (
228231
github.com/klauspost/readahead v1.3.1 // indirect
229232
github.com/klauspost/reedsolomon v1.9.11 // indirect
230233
github.com/kr/fs v0.1.0 // indirect
234+
github.com/kylelemons/godebug v1.1.0 // indirect
231235
github.com/leodido/go-urn v1.4.0 // indirect
232236
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
233237
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -243,7 +247,7 @@ require (
243247
github.com/mitchellh/mapstructure v1.5.0 // indirect
244248
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
245249
github.com/modern-go/reflect2 v1.0.2 // indirect
246-
github.com/montanaflynn/stats v0.5.0 // indirect
250+
github.com/montanaflynn/stats v0.7.0 // indirect
247251
github.com/mozillazg/go-httpheader v0.2.1 // indirect
248252
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
249253
github.com/ncw/directio v1.0.5 // indirect
@@ -255,6 +259,7 @@ require (
255259
github.com/pingcap/errors v0.11.5-0.20211224045212-9687c2b0f87c // indirect
256260
github.com/pingcap/failpoint v0.0.0-20220801062533-2eaa32854a6c // indirect
257261
github.com/pingcap/kvproto v0.0.0-20230403051650-e166ae588106 // indirect
262+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
258263
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
259264
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
260265
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect

go.sum

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,9 @@ github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfE
310310
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
311311
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
312312
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
313-
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
314313
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
314+
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
315+
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
315316
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
316317
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
317318
github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY=
@@ -597,8 +598,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
597598
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
598599
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
599600
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
600-
github.com/montanaflynn/stats v0.5.0 h1:2EkzeTSqBB4V4bJwWrt5gIIrZmpJBcoIRGS2kWLgzmk=
601-
github.com/montanaflynn/stats v0.5.0/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
601+
github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU=
602+
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
602603
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
603604
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
604605
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -997,6 +998,7 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
997998
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
998999
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9991000
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
1001+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10001002
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10011003
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10021004
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

pkg/object/azure.go

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import (
3232
"github.com/aws/aws-sdk-go-v2/aws"
3333

3434
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
35+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
3536
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
3637
blob2 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
3738
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
@@ -41,10 +42,11 @@ import (
4142

4243
type wasb struct {
4344
DefaultObjectStorage
44-
container *container.Client
45-
azblobCli *azblob.Client
46-
sc string
47-
cName string
45+
container *container.Client
46+
azblobCli *azblob.Client
47+
sc string
48+
cName string
49+
useTokenAuth bool // true when using managed identity/token-based auth, false for shared key/connection string
4850
}
4951

5052
func (b *wasb) String() string {
@@ -117,11 +119,25 @@ func (b *wasb) Copy(ctx context.Context, dst, src string) error {
117119
if b.sc != "" {
118120
options.Tier = str2Tier(b.sc)
119121
}
120-
srcSASUrl, err := srcCli.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(10*time.Second), nil)
121-
if err != nil {
122-
return err
122+
123+
var srcURL string
124+
var err error
125+
126+
if b.useTokenAuth {
127+
// Token-based authentication: use direct blob URL
128+
// Azure will authenticate using the OAuth token from the credential chain
129+
srcURL = srcCli.URL()
130+
logger.Debugf("Using token-based authentication for Copy operation (direct URL without SAS)")
131+
} else {
132+
// Shared key authentication: generate SAS token for source blob
133+
srcURL, err = srcCli.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(10*time.Second), nil)
134+
if err != nil {
135+
return err
136+
}
137+
logger.Debugf("Using shared key authentication for Copy operation (SAS URL)")
123138
}
124-
_, err = dstCli.CopyFromURL(ctx, srcSASUrl, options)
139+
140+
_, err = dstCli.CopyFromURL(ctx, srcURL, options)
125141
return err
126142
}
127143

@@ -180,6 +196,23 @@ func (b *wasb) SetStorageClass(sc string) error {
180196
return nil
181197
}
182198

199+
// createAzureCredential creates a credential for Azure authentication.
200+
// Uses DefaultAzureCredential which attempts authentication via:
201+
// - Environment variables (service principal)
202+
// - Workload Identity (Kubernetes)
203+
// - Managed Identity (system-assigned and user-assigned)
204+
// - Azure CLI
205+
// - Azure Developer CLI
206+
func createAzureCredential() (azcore.TokenCredential, error) {
207+
logger.Debugf("Creating DefaultAzureCredential for token-based authentication")
208+
cred, err := azidentity.NewDefaultAzureCredential(nil)
209+
if err != nil {
210+
logger.Debugf("Failed to create DefaultAzureCredential: %v", err)
211+
return nil, err
212+
}
213+
return cred, nil
214+
}
215+
183216
func autoWasbEndpoint(containerName, accountName, scheme string, credential *azblob.SharedKeyCredential) (string, error) {
184217
baseURLs := []string{"blob.core.windows.net", "blob.core.chinacloudapi.cn"}
185218
endpoint := ""
@@ -206,6 +239,32 @@ func autoWasbEndpoint(containerName, accountName, scheme string, credential *azb
206239
return endpoint, nil
207240
}
208241

242+
func autoWasbEndpointWithToken(containerName, accountName, scheme string, credential azcore.TokenCredential) (string, error) {
243+
baseURLs := []string{"blob.core.windows.net", "blob.core.chinacloudapi.cn"}
244+
endpoint := ""
245+
for _, baseURL := range baseURLs {
246+
if _, err := net.LookupIP(fmt.Sprintf("%s.%s", accountName, baseURL)); err != nil {
247+
logger.Debugf("Attempt to resolve domain name %s failed: %s", baseURL, err)
248+
continue
249+
}
250+
client, err := azblob.NewClient(fmt.Sprintf("%s://%s.%s", scheme, accountName, baseURL), credential, nil)
251+
if err != nil {
252+
return "", err
253+
}
254+
if _, err = client.ServiceClient().GetProperties(ctx, nil); err != nil {
255+
logger.Debugf("Try to get service properties at %s failed: %s", baseURL, err)
256+
continue
257+
}
258+
endpoint = baseURL
259+
break
260+
}
261+
262+
if endpoint == "" {
263+
return "", fmt.Errorf("fail to get endpoint for container %s", containerName)
264+
}
265+
return endpoint, nil
266+
}
267+
209268
func newWasb(endpoint, accountName, accountKey, token string) (ObjectStorage, error) {
210269
if !strings.Contains(endpoint, "://") {
211270
endpoint = fmt.Sprintf("https://%s", endpoint)
@@ -216,19 +275,52 @@ func newWasb(endpoint, accountName, accountKey, token string) (ObjectStorage, er
216275
}
217276
hostParts := strings.SplitN(uri.Host, ".", 2)
218277
containerName := hostParts[0]
219-
// Connection string support: DefaultEndpointsProtocol=[http|https];AccountName=***;AccountKey=***;EndpointSuffix=[core.windows.net|core.chinacloudapi.cn]
278+
279+
// Priority 1: Connection string support
280+
// DefaultEndpointsProtocol=[http|https];AccountName=***;AccountKey=***;EndpointSuffix=[core.windows.net|core.chinacloudapi.cn]
220281
if connString := os.Getenv("AZURE_STORAGE_CONNECTION_STRING"); connString != "" {
282+
logger.Debugf("Using Azure connection string authentication")
221283
var client *azblob.Client
222284
if client, err = azblob.NewClientFromConnectionString(connString, nil); err != nil {
223285
return nil, err
224286
}
225-
return &wasb{container: client.ServiceClient().NewContainerClient(containerName), azblobCli: client, cName: containerName}, nil
287+
return &wasb{container: client.ServiceClient().NewContainerClient(containerName), azblobCli: client, cName: containerName, useTokenAuth: false}, nil
226288
}
227289

290+
// Priority 2: Try managed identity / token-based authentication if no account key provided
291+
if accountKey == "" {
292+
logger.Debugf("No account key provided, attempting token-based authentication (managed identity, Azure CLI, etc.)")
293+
tokenCred, err := createAzureCredential()
294+
if err != nil {
295+
return nil, fmt.Errorf("Failed to create Azure credential (managed identity/Azure CLI): %v", err)
296+
}
297+
298+
var domain string
299+
if len(hostParts) > 1 {
300+
domain = hostParts[1]
301+
if !strings.HasPrefix(hostParts[1], "blob") {
302+
domain = fmt.Sprintf("blob.%s", hostParts[1])
303+
}
304+
} else if domain, err = autoWasbEndpointWithToken(containerName, accountName, uri.Scheme, tokenCred); err != nil {
305+
return nil, fmt.Errorf("Unable to get endpoint of container %s: %s", containerName, err)
306+
}
307+
308+
serviceURL := fmt.Sprintf("%s://%s.%s", uri.Scheme, accountName, domain)
309+
client, err := azblob.NewClient(serviceURL, tokenCred, nil)
310+
if err != nil {
311+
return nil, fmt.Errorf("Failed to create Azure blob client with token credential: %v", err)
312+
}
313+
logger.Debugf("Successfully authenticated using token-based credential")
314+
return &wasb{container: client.ServiceClient().NewContainerClient(containerName), azblobCli: client, cName: containerName, useTokenAuth: true}, nil
315+
}
316+
317+
// Priority 3: Shared key authentication (existing behavior)
318+
logger.Debugf("Using Azure shared key authentication")
228319
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
229320
if err != nil {
230321
return nil, err
231322
}
323+
232324
var domain string
233325
if len(hostParts) > 1 {
234326
domain = hostParts[1]
@@ -243,7 +335,7 @@ func newWasb(endpoint, accountName, accountKey, token string) (ObjectStorage, er
243335
if err != nil {
244336
return nil, err
245337
}
246-
return &wasb{container: client.ServiceClient().NewContainerClient(containerName), azblobCli: client, cName: containerName}, nil
338+
return &wasb{container: client.ServiceClient().NewContainerClient(containerName), azblobCli: client, cName: containerName, useTokenAuth: false}, nil
247339
}
248340

249341
func init() {

0 commit comments

Comments
 (0)