mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-14 02:20:41 +02:00
* remote_storage/azure: allow a per-request HTTP client Thread an optional *http.Client through NewAzBlobClient and add azure.MakeWithHTTPClient, mirroring the S3 backend. When set, the client overrides the azblob transport so a caller can pin the dial path. The existing makers pass nil, so behavior is unchanged. * volume: extend the remote-endpoint guard to the azure backend The endpoint validation and rebinding-safe dialer in FetchAndWriteNeedle covered the S3-SDK backends. The azure backend also dials a caller-supplied AzureEndpoint, so route both families through a single guardedRemoteClient helper that returns the endpoint each backend dials and a constructor bound to the guarded HTTP client. azure is guarded only when AzureEndpoint is set; an empty endpoint derives the public host from the account. -volume.allowUntrustedRemoteEndpoints still opts out. * rust volume: assert the azure endpoint has no remote-client path The Rust volume server has no azure backend, so make_remote_storage_client rejects the type before any client is built. Add a regression test pinning that invariant.
112 lines
4.2 KiB
Go
112 lines
4.2 KiB
Go
package azure
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"regexp"
|
|
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
|
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
)
|
|
|
|
// Azure storage account names are 3 to 24 lowercase letters and digits. The
|
|
// name lands in the service URL, where a stray "/", "?" or "@" would move the
|
|
// authority somewhere else and send an authenticated request to that host.
|
|
var validAzureAccountName = regexp.MustCompile(`^[a-z0-9]{3,24}$`)
|
|
|
|
// NewAzBlobClient builds a blob service client for accountName.
|
|
//
|
|
// An empty accountKey selects Entra ID instead of a shared key: azidentity
|
|
// resolves a workload identity, a managed identity, or a developer login from
|
|
// the environment, and access is granted through RBAC. Fleets that cannot
|
|
// distribute and rotate storage account keys authenticate that way. clientID
|
|
// pins a user-assigned identity when the environment offers more than one.
|
|
//
|
|
// endpoint is the blob service URL, for accounts outside the public cloud. An
|
|
// empty endpoint derives the public one from accountName.
|
|
//
|
|
// A non-nil httpClient overrides the SDK transport; the volume server passes one
|
|
// whose dialer pins the validated endpoint against DNS rebinding.
|
|
func NewAzBlobClient(accountName, accountKey, clientID, endpoint string, httpClient *http.Client) (*azblob.Client, error) {
|
|
|
|
if accountName == "" {
|
|
return nil, fmt.Errorf("azure account name is required")
|
|
}
|
|
if !validAzureAccountName.MatchString(accountName) {
|
|
return nil, fmt.Errorf("invalid azure account name %q: expecting 3 to 24 lowercase letters and digits", accountName)
|
|
}
|
|
|
|
serviceURL, err := azureServiceURL(accountName, endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
options := DefaultAzBlobClientOptions()
|
|
if httpClient != nil {
|
|
options.Transport = httpClient
|
|
}
|
|
|
|
if accountKey == "" {
|
|
credential, err := newAzureTokenCredential(clientID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Azure Entra ID credential for account %s: %w", accountName, err)
|
|
}
|
|
glog.V(1).Infof("azure %s: authenticating with Entra ID", accountName)
|
|
client, err := azblob.NewClient(serviceURL, credential, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Azure client: %w", err)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
credential, err := azblob.NewSharedKeyCredential(accountName, accountKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Azure credential with account name:%s: %w", accountName, err)
|
|
}
|
|
client, err := azblob.NewClientWithSharedKeyCredential(serviceURL, credential, options)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Azure client: %w", err)
|
|
}
|
|
return client, nil
|
|
}
|
|
|
|
// azureServiceURL locates the blob service. Sovereign clouds and private
|
|
// endpoints do not live under blob.core.windows.net, so they name the service
|
|
// URL outright rather than having it derived from the account.
|
|
func azureServiceURL(accountName, endpoint string) (string, error) {
|
|
if endpoint == "" {
|
|
return fmt.Sprintf("https://%s.blob.core.windows.net/", accountName), nil
|
|
}
|
|
parsed, err := url.Parse(endpoint)
|
|
if err != nil {
|
|
return "", fmt.Errorf("invalid azure endpoint %q: %w", endpoint, err)
|
|
}
|
|
// plain http would carry the account key or the bearer token in the clear
|
|
if parsed.Scheme != "https" || parsed.Hostname() == "" {
|
|
return "", fmt.Errorf("invalid azure endpoint %q: expecting an https service url, such as https://%s.blob.core.usgovcloudapi.net/", endpoint, accountName)
|
|
}
|
|
return endpoint, nil
|
|
}
|
|
|
|
// newAzureTokenCredential resolves an Entra ID credential. Without a pinned
|
|
// clientID the default chain discovers whatever the host offers. With one, the
|
|
// federated token file projected by the Azure workload identity webhook tells
|
|
// the two identity flavors apart.
|
|
func newAzureTokenCredential(clientID string) (azcore.TokenCredential, error) {
|
|
if clientID == "" {
|
|
return azidentity.NewDefaultAzureCredential(nil)
|
|
}
|
|
if os.Getenv("AZURE_FEDERATED_TOKEN_FILE") != "" {
|
|
return azidentity.NewWorkloadIdentityCredential(&azidentity.WorkloadIdentityCredentialOptions{
|
|
ClientID: clientID,
|
|
})
|
|
}
|
|
return azidentity.NewManagedIdentityCredential(&azidentity.ManagedIdentityCredentialOptions{
|
|
ID: azidentity.ClientID(clientID),
|
|
})
|
|
}
|