Updated to use common Azure auth logic (#972)
* Common Azure auth logic - Currently implemented on secretstores/azure/keyvault and state/azure/blobstorage - Supports Azure AD via service principal (client credentials, client certificate, MSI) - based on the previous authorizer for AKV - Allows using other Azure clouds (China, Germany, etc) - For Blob Storage state, supports using custom endpoints (like emulators like Azurite) * Add environment variable aliases * Address linter warnings * another lint thing * Fixed typo in method description * Updated metadata key names so they're more consistent * Fix test * Some more linter things Co-authored-by: Bernd Verst <me@bernd.dev> Co-authored-by: Yaron Schneider <yaronsc@microsoft.com> Co-authored-by: Bernd Verst <berndverst@users.noreply.github.com>
This commit is contained in:
parent
aa7d2ee1dd
commit
d0816e32a8
|
@ -0,0 +1,295 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
// NewEnvironmentSettings returns a new EnvironmentSettings configured for a given Azure resource.
|
||||
func NewEnvironmentSettings(resourceName string, values map[string]string) (EnvironmentSettings, error) {
|
||||
es := EnvironmentSettings{
|
||||
Values: values,
|
||||
}
|
||||
azureEnv, err := es.GetAzureEnvironment()
|
||||
if err != nil {
|
||||
return es, err
|
||||
}
|
||||
es.AzureEnvironment = azureEnv
|
||||
switch resourceName {
|
||||
case "azure":
|
||||
// Azure Resource Manager (management plane)
|
||||
es.Resource = azureEnv.TokenAudience
|
||||
case "keyvault":
|
||||
// Azure Key Vault (data plane)
|
||||
es.Resource = azureEnv.ResourceIdentifiers.KeyVault
|
||||
case "storage":
|
||||
// Azure Storage (data plane)
|
||||
es.Resource = azureEnv.ResourceIdentifiers.Storage
|
||||
default:
|
||||
return es, errors.New("invalid resource name: " + resourceName)
|
||||
}
|
||||
|
||||
return es, nil
|
||||
}
|
||||
|
||||
// EnvironmentSettings hold settings to authenticate with Azure.
|
||||
type EnvironmentSettings struct {
|
||||
Values map[string]string
|
||||
Resource string
|
||||
AzureEnvironment *azure.Environment
|
||||
}
|
||||
|
||||
// GetAzureEnvironment returns the Azure environment for a given name.
|
||||
func (s EnvironmentSettings) GetAzureEnvironment() (*azure.Environment, error) {
|
||||
envName, ok := s.GetEnvironment("AzureEnvironment")
|
||||
if !ok || envName == "" {
|
||||
envName = DefaultAzureEnvironment
|
||||
}
|
||||
env, err := azure.EnvironmentFromName(envName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &env, err
|
||||
}
|
||||
|
||||
// GetAuthorizer creates an Authorizer retrieved from, in order:
|
||||
// 1. Client credentials
|
||||
// 2. Client certificate
|
||||
// 3. MSI
|
||||
func (s EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
|
||||
spt, err := s.GetServicePrincipalToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return autorest.NewBearerAuthorizer(spt), nil
|
||||
}
|
||||
|
||||
// GetServicePrincipalToken returns a Service Principal Token retrieved from, in order:
|
||||
// 1. Client credentials
|
||||
// 2. Client certificate
|
||||
// 3. MSI
|
||||
func (s EnvironmentSettings) GetServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
|
||||
// 1. Client credentials
|
||||
if c, e := s.GetClientCredentials(); e == nil {
|
||||
return c.ServicePrincipalToken()
|
||||
}
|
||||
|
||||
// 2. Client Certificate
|
||||
if c, e := s.GetClientCert(); e == nil {
|
||||
return c.ServicePrincipalToken()
|
||||
}
|
||||
|
||||
// 3. MSI
|
||||
return s.GetMSI().ServicePrincipalToken()
|
||||
}
|
||||
|
||||
// GetClientCredentials creates a config object from the available client credentials.
|
||||
// An error is returned if no certificate credentials are available.
|
||||
func (s EnvironmentSettings) GetClientCredentials() (CredentialsConfig, error) {
|
||||
azureEnv, err := s.GetAzureEnvironment()
|
||||
if err != nil {
|
||||
return CredentialsConfig{}, err
|
||||
}
|
||||
|
||||
clientID, _ := s.GetEnvironment("ClientID")
|
||||
clientSecret, _ := s.GetEnvironment("ClientSecret")
|
||||
tenantID, _ := s.GetEnvironment("TenantID")
|
||||
|
||||
if clientID == "" || clientSecret == "" || tenantID == "" {
|
||||
return CredentialsConfig{}, errors.New("parameters clientId, clientSecret, and tenantId must all be present")
|
||||
}
|
||||
|
||||
authorizer := NewCredentialsConfig(clientID, tenantID, clientSecret, s.Resource, azureEnv)
|
||||
|
||||
return authorizer, nil
|
||||
}
|
||||
|
||||
// GetClientCert creates a config object from the available certificate credentials.
|
||||
// An error is returned if no certificate credentials are available.
|
||||
func (s EnvironmentSettings) GetClientCert() (CertConfig, error) {
|
||||
azureEnv, err := s.GetAzureEnvironment()
|
||||
if err != nil {
|
||||
return CertConfig{}, err
|
||||
}
|
||||
|
||||
certFilePath, certFilePathPresent := s.GetEnvironment("CertificateFile")
|
||||
certBytes, certBytesPresent := s.GetEnvironment("Certificate")
|
||||
certPassword, _ := s.GetEnvironment("CertificatePassword")
|
||||
clientID, _ := s.GetEnvironment("ClientID")
|
||||
tenantID, _ := s.GetEnvironment("TenantID")
|
||||
|
||||
if !certFilePathPresent && !certBytesPresent {
|
||||
return CertConfig{}, fmt.Errorf("missing client certificate")
|
||||
}
|
||||
|
||||
authorizer := NewCertConfig(clientID, tenantID, certFilePath, []byte(certBytes), certPassword, s.Resource, azureEnv)
|
||||
|
||||
return authorizer, nil
|
||||
}
|
||||
|
||||
// GetMSI creates a MSI config object from the available client ID.
|
||||
func (s EnvironmentSettings) GetMSI() MSIConfig {
|
||||
config := NewMSIConfig(s.Resource)
|
||||
// This is optional and it's ok if value is empty
|
||||
config.ClientID, _ = s.GetEnvironment("ClientID")
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// CredentialsConfig provides the options to get a bearer authorizer from client credentials
|
||||
type CredentialsConfig struct {
|
||||
*auth.ClientCredentialsConfig
|
||||
}
|
||||
|
||||
// NewCredentialsConfig creates an CredentialsConfig object configured to obtain an Authorizer through Client Credentials.
|
||||
func NewCredentialsConfig(clientID string, tenantID string, clientSecret string, resource string, env *azure.Environment) CredentialsConfig {
|
||||
return CredentialsConfig{
|
||||
&auth.ClientCredentialsConfig{
|
||||
ClientSecret: clientSecret,
|
||||
ClientID: clientID,
|
||||
TenantID: tenantID,
|
||||
Resource: resource,
|
||||
AADEndpoint: env.ActiveDirectoryEndpoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ServicePrincipalToken gets a ServicePrincipalToken object from the credentials.
|
||||
func (c CredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return adal.NewServicePrincipalToken(*oauthConfig, c.ClientID, c.ClientSecret, c.Resource)
|
||||
}
|
||||
|
||||
// CertConfig provides the options to get a bearer authorizer from a client certificate.
|
||||
type CertConfig struct {
|
||||
*auth.ClientCertificateConfig
|
||||
CertificateData []byte
|
||||
}
|
||||
|
||||
// NewCertConfig creates an CertConfig object configured to obtain an Authorizer through Client Credentials, using a certificate.
|
||||
func NewCertConfig(clientID string, tenantID string, certificatePath string, certificateBytes []byte, certificatePassword string, resource string, env *azure.Environment) CertConfig {
|
||||
return CertConfig{
|
||||
&auth.ClientCertificateConfig{
|
||||
CertificatePath: certificatePath,
|
||||
CertificatePassword: certificatePassword,
|
||||
ClientID: clientID,
|
||||
TenantID: tenantID,
|
||||
Resource: resource,
|
||||
AADEndpoint: env.ActiveDirectoryEndpoint,
|
||||
},
|
||||
certificateBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// ServicePrincipalToken gets a ServicePrincipalToken object from client certificate.
|
||||
func (c CertConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
|
||||
if c.ClientCertificateConfig.CertificatePath != "" {
|
||||
// in standalone mode, component yaml will pass cert path
|
||||
return c.ClientCertificateConfig.ServicePrincipalToken()
|
||||
} else if len(c.CertificateData) > 0 {
|
||||
// in kubernetes mode, runtime will get the secret from K8S secret store and pass byte array
|
||||
return c.ServicePrincipalTokenByCertBytes()
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("certificate is not given")
|
||||
}
|
||||
|
||||
// ServicePrincipalTokenByCertBytes gets the service principal token by CertificateBytes.
|
||||
func (c CertConfig) ServicePrincipalTokenByCertBytes() (*adal.ServicePrincipalToken, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, rsaPrivateKey, err := c.decodePkcs12(c.CertificateData, c.CertificatePassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||
}
|
||||
|
||||
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, c.ClientID, certificate, rsaPrivateKey, c.Resource)
|
||||
}
|
||||
|
||||
func (c CertConfig) decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
|
||||
if !isRsaKey {
|
||||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
|
||||
}
|
||||
|
||||
return certificate, rsaPrivateKey, nil
|
||||
}
|
||||
|
||||
// MSIConfig provides the options to get a bearer authorizer through MSI.
|
||||
type MSIConfig struct {
|
||||
Resource string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// NewMSIConfig creates an MSIConfig object configured to obtain an Authorizer through MSI.
|
||||
func NewMSIConfig(resource string) MSIConfig {
|
||||
return MSIConfig{
|
||||
Resource: resource,
|
||||
}
|
||||
}
|
||||
|
||||
// ServicePrincipalToken gets the ServicePrincipalToken object from MSI.
|
||||
func (mc MSIConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
|
||||
msiEndpoint, err := adal.GetMSIEndpoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var spToken *adal.ServicePrincipalToken
|
||||
if mc.ClientID == "" {
|
||||
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
|
||||
}
|
||||
} else {
|
||||
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return spToken, nil
|
||||
}
|
||||
|
||||
// GetAzureEnvironment returns the Azure environment for a given name, supporting aliases too.
|
||||
func (s EnvironmentSettings) GetEnvironment(key string) (string, bool) {
|
||||
var (
|
||||
val string
|
||||
ok bool
|
||||
)
|
||||
for _, k := range MetadataKeys[key] {
|
||||
val, ok = s.Values[k]
|
||||
if ok {
|
||||
return val, true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package keyvault
|
||||
package azure
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
|
@ -23,16 +23,18 @@ const (
|
|||
)
|
||||
|
||||
func TestGetClientCert(t *testing.T) {
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNCertificateFile: "testfile",
|
||||
componentSPNCertificate: "testcert",
|
||||
componentSPNCertificatePassword: "1234",
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentSPNTenantID: fakeTenantID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureCertificateFile": "testfile",
|
||||
"azureCertificate": "testcert",
|
||||
"azureCertificatePassword": "1234",
|
||||
"azureClientId": fakeClientID,
|
||||
"azureTenantId": fakeTenantID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig, _ := settings.GetClientCert()
|
||||
|
||||
|
@ -53,15 +55,17 @@ func TestAuthorizorWithCertFile(t *testing.T) {
|
|||
err := ioutil.WriteFile(testCertFileName, certBytes, 0o644)
|
||||
assert.NoError(t, err)
|
||||
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNCertificateFile: testCertFileName,
|
||||
componentSPNCertificatePassword: "",
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentSPNTenantID: fakeTenantID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureCertificateFile": testCertFileName,
|
||||
"azureCertificatePassword": "",
|
||||
"azureClientId": fakeClientID,
|
||||
"azureTenantId": fakeTenantID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig, _ := settings.GetClientCert()
|
||||
assert.NotNil(t, testCertConfig)
|
||||
|
@ -79,55 +83,61 @@ func TestAuthorizorWithCertBytes(t *testing.T) {
|
|||
t.Run("Certificate is valid", func(t *testing.T) {
|
||||
certBytes := getTestCert()
|
||||
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNCertificate: string(certBytes),
|
||||
componentSPNCertificatePassword: "",
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentSPNTenantID: fakeTenantID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureCertificate": string(certBytes),
|
||||
"azureCertificatePassword": "",
|
||||
"azureClientId": fakeClientID,
|
||||
"azureTenantId": fakeTenantID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig, _ := settings.GetClientCert()
|
||||
assert.NotNil(t, testCertConfig)
|
||||
assert.NotNil(t, testCertConfig.ClientCertificateConfig)
|
||||
|
||||
authorizer, err := testCertConfig.Authorizer()
|
||||
spt, err := testCertConfig.ServicePrincipalToken()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, authorizer)
|
||||
assert.NotNil(t, spt)
|
||||
})
|
||||
|
||||
t.Run("Certificate is invalid", func(t *testing.T) {
|
||||
certBytes := getTestCert()
|
||||
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNCertificate: string(certBytes[0:20]),
|
||||
componentSPNCertificatePassword: "",
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentSPNTenantID: fakeTenantID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureCertificate": string(certBytes[0:20]),
|
||||
"azureCertificatePassword": "",
|
||||
"azureClientId": fakeClientID,
|
||||
"azureTenantId": fakeTenantID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig, _ := settings.GetClientCert()
|
||||
assert.NotNil(t, testCertConfig)
|
||||
assert.NotNil(t, testCertConfig.ClientCertificateConfig)
|
||||
|
||||
_, err := testCertConfig.Authorizer()
|
||||
_, err = testCertConfig.ServicePrincipalToken()
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetMSI(t *testing.T) {
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureClientId": fakeClientID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig := settings.GetMSI()
|
||||
|
||||
|
@ -136,49 +146,55 @@ func TestGetMSI(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestFallbackToMSI(t *testing.T) {
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureClientId": fakeClientID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
authorizer, err := settings.GetAuthorizer()
|
||||
spt, err := settings.GetServicePrincipalToken()
|
||||
|
||||
assert.NotNil(t, authorizer)
|
||||
assert.NotNil(t, spt)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAuthorizorWithMSI(t *testing.T) {
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureClientId": fakeClientID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig := settings.GetMSI()
|
||||
assert.NotNil(t, testCertConfig)
|
||||
|
||||
authorizer, err := testCertConfig.Authorizer()
|
||||
spt, err := testCertConfig.ServicePrincipalToken()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, authorizer)
|
||||
assert.NotNil(t, spt)
|
||||
}
|
||||
|
||||
func TestAuthorizorWithMSIAndUserAssignedID(t *testing.T) {
|
||||
settings := EnvironmentSettings{
|
||||
Values: map[string]string{
|
||||
componentSPNClientID: fakeClientID,
|
||||
componentVaultName: "vaultName",
|
||||
settings, err := NewEnvironmentSettings(
|
||||
"keyvault",
|
||||
map[string]string{
|
||||
"azureClientId": fakeClientID,
|
||||
"vaultName": "vaultName",
|
||||
},
|
||||
}
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
testCertConfig := settings.GetMSI()
|
||||
assert.NotNil(t, testCertConfig)
|
||||
|
||||
authorizer, err := testCertConfig.Authorizer()
|
||||
spt, err := testCertConfig.ServicePrincipalToken()
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, authorizer)
|
||||
assert.NotNil(t, spt)
|
||||
}
|
||||
|
||||
func getTestCert() []byte {
|
|
@ -0,0 +1,33 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package azure
|
||||
|
||||
// MetadataKeys : Keys for all metadata properties
|
||||
var MetadataKeys = map[string][]string{ // nolint: gochecknoglobals
|
||||
// clientId, clientSecret, tenantId are supported for backwards-compatibility as they're used by some components, but should be considered deprecated
|
||||
|
||||
// Certificate contains the raw certificate data
|
||||
"Certificate": {"azureCertificate", "spnCertificate"},
|
||||
// Path to a certificate
|
||||
"CertificateFile": {"azureCertificateFile", "spnCertificateFile"},
|
||||
// Password for the certificate
|
||||
"CertificatePassword": {"azureCertificatePassword", "spnCertificatePassword"},
|
||||
// Client ID for the Service Principal
|
||||
// The "clientId" alias is supported for backwards-compatibility as it's used by some components, but should be considered deprecated
|
||||
"ClientID": {"azureClientId", "spnClientId", "clientId"},
|
||||
// Client secret for the Service Principal
|
||||
// The "clientSecret" alias is supported for backwards-compatibility as it's used by some components, but should be considered deprecated
|
||||
"ClientSecret": {"azureClientSecret", "spnClientSecret", "clientSecret"},
|
||||
// Tenant ID for the Service Principal
|
||||
// The "tenantId" alias is supported for backwards-compatibility as it's used by some components, but should be considered deprecated
|
||||
"TenantID": {"azureTenantId", "spnTenantId", "tenantId"},
|
||||
// Identifier for the Azure environment
|
||||
// Allowed values (case-insensitive): AZUREPUBLICCLOUD, AZURECHINACLOUD, AZUREGERMANCLOUD, AZUREUSGOVERNMENTCLOUD
|
||||
"AzureEnvironment": {"azureEnvironment"},
|
||||
}
|
||||
|
||||
// Default Azure environment
|
||||
const DefaultAzureEnvironment = "AZUREPUBLICCLOUD"
|
|
@ -0,0 +1,59 @@
|
|||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
|
||||
"github.com/dapr/kit/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
storageAccountKeyKey = "accountKey"
|
||||
)
|
||||
|
||||
// GetAzureStorageCredentials returns a azblob.Credential object that can be used to authenticate an Azure Blob Storage SDK pipeline.
|
||||
// First it tries to authenticate using shared key credentials (using an account key) if present. It falls back to attempting to use Azure AD (via a service principal or MSI).
|
||||
func GetAzureStorageCredentials(log logger.Logger, accountName string, metadata map[string]string) (azblob.Credential, *azure.Environment, error) {
|
||||
settings, err := NewEnvironmentSettings("storage", metadata)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Try using shared key credentials first
|
||||
accountKey, ok := metadata[storageAccountKeyKey]
|
||||
if ok && accountKey != "" {
|
||||
credential, newSharedKeyErr := azblob.NewSharedKeyCredential(accountName, accountKey)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid credentials with error: %s", newSharedKeyErr.Error())
|
||||
}
|
||||
|
||||
return credential, settings.AzureEnvironment, nil
|
||||
}
|
||||
|
||||
// Fallback to using Azure AD
|
||||
spt, err := settings.GetServicePrincipalToken()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var tokenRefresher azblob.TokenRefresher = func(credential azblob.TokenCredential) time.Duration {
|
||||
log.Debug("Refreshing Azure Storage auth token")
|
||||
err := spt.Refresh()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
token := spt.Token()
|
||||
credential.SetToken(token.AccessToken)
|
||||
|
||||
// Make the token expire 2 minutes earlier to get some extra buffer
|
||||
exp := token.Expires().Sub(time.Now().Add(2 * time.Minute))
|
||||
log.Debug("Received new token, valid for", exp)
|
||||
|
||||
return exp
|
||||
}
|
||||
credential := azblob.NewTokenCredential("", tokenRefresher)
|
||||
|
||||
return credential, settings.AzureEnvironment, nil
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package keyvault
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"golang.org/x/crypto/pkcs12"
|
||||
)
|
||||
|
||||
// EnvironmentSettings hold settings to authenticate with the Key Vault.
|
||||
type EnvironmentSettings struct {
|
||||
Values map[string]string
|
||||
}
|
||||
|
||||
// CertConfig provides the options to get a bearer authorizer from a client certificate.
|
||||
type CertConfig struct {
|
||||
*auth.ClientCertificateConfig
|
||||
CertificateData []byte
|
||||
}
|
||||
|
||||
// GetClientCert creates a config object from the available certificate credentials.
|
||||
// An error is returned if no certificate credentials are available.
|
||||
func (s EnvironmentSettings) GetClientCert() (CertConfig, error) {
|
||||
certFilePath, certFilePathPresent := s.Values[componentSPNCertificateFile]
|
||||
certBytes, certBytesPresent := s.Values[componentSPNCertificate]
|
||||
certPassword := s.Values[componentSPNCertificatePassword]
|
||||
clientID := s.Values[componentSPNClientID]
|
||||
tenantID := s.Values[componentSPNTenantID]
|
||||
|
||||
if !certFilePathPresent && !certBytesPresent {
|
||||
return CertConfig{}, fmt.Errorf("missing client secret")
|
||||
}
|
||||
|
||||
authorizer := NewCertConfig(certFilePath, []byte(certBytes), certPassword, clientID, tenantID)
|
||||
|
||||
return authorizer, nil
|
||||
}
|
||||
|
||||
// NewCertConfig creates an ClientAuthorizer object configured to obtain an Authorizer through Client Credentials.
|
||||
func NewCertConfig(certificatePath string, certificateBytes []byte, certificatePassword string, clientID string, tenantID string) CertConfig {
|
||||
return CertConfig{
|
||||
&auth.ClientCertificateConfig{
|
||||
CertificatePath: certificatePath,
|
||||
CertificatePassword: certificatePassword,
|
||||
ClientID: clientID,
|
||||
TenantID: tenantID,
|
||||
Resource: azure.PublicCloud.ResourceIdentifiers.KeyVault,
|
||||
AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
|
||||
},
|
||||
certificateBytes,
|
||||
}
|
||||
}
|
||||
|
||||
// Authorizer gets an authorizer object from client certificate.
|
||||
func (c CertConfig) Authorizer() (autorest.Authorizer, error) {
|
||||
if c.ClientCertificateConfig.CertificatePath != "" {
|
||||
// in standalone mode, component yaml will pass cert path
|
||||
return c.ClientCertificateConfig.Authorizer()
|
||||
} else if len(c.CertificateData) > 0 {
|
||||
// in kubernetes mode, runtime will get the secret from K8S secret store and pass byte array
|
||||
spToken, err := c.ServicePrincipalTokenByCertBytes()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
|
||||
}
|
||||
|
||||
return autorest.NewBearerAuthorizer(spToken), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("certificate is not given")
|
||||
}
|
||||
|
||||
// ServicePrincipalTokenByCertBytes gets the service principal token by CertificateBytes.
|
||||
func (c CertConfig) ServicePrincipalTokenByCertBytes() (*adal.ServicePrincipalToken, error) {
|
||||
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, rsaPrivateKey, err := c.decodePkcs12(c.CertificateData, c.CertificatePassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
|
||||
}
|
||||
|
||||
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, c.ClientID, certificate, rsaPrivateKey, c.Resource)
|
||||
}
|
||||
|
||||
// MSIConfig provides the options to get a bearer authorizer through MSI.
|
||||
type MSIConfig struct {
|
||||
Resource string
|
||||
ClientID string
|
||||
}
|
||||
|
||||
// NewMSIConfig creates an MSIConfig object configured to obtain an Authorizer through MSI.
|
||||
func NewMSIConfig() MSIConfig {
|
||||
return MSIConfig{
|
||||
Resource: azure.PublicCloud.ResourceManagerEndpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// GetMSI creates a MSI config object from the available client ID.
|
||||
func (s EnvironmentSettings) GetMSI() MSIConfig {
|
||||
config := NewMSIConfig()
|
||||
config.Resource = azure.PublicCloud.ResourceIdentifiers.KeyVault
|
||||
config.ClientID = s.Values[componentSPNClientID]
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// Authorizer gets the authorizer from MSI.
|
||||
func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
|
||||
msiEndpoint, err := adal.GetMSIEndpoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var spToken *adal.ServicePrincipalToken
|
||||
if mc.ClientID == "" {
|
||||
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
|
||||
}
|
||||
} else {
|
||||
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return autorest.NewBearerAuthorizer(spToken), nil
|
||||
}
|
||||
|
||||
// GetAuthorizer creates an Authorizer configured from environment variables in the order:
|
||||
// 1. Client certificate
|
||||
// 2. MSI
|
||||
func (s EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
|
||||
// 1. Client Certificate
|
||||
if c, e := s.GetClientCert(); e == nil {
|
||||
return c.Authorizer()
|
||||
}
|
||||
|
||||
// 2. MSI
|
||||
return s.GetMSI().Authorizer()
|
||||
}
|
||||
|
||||
func (c CertConfig) decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
|
||||
if !isRsaKey {
|
||||
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
|
||||
}
|
||||
|
||||
return certificate, rsaPrivateKey, nil
|
||||
}
|
|
@ -12,37 +12,29 @@ import (
|
|||
"strings"
|
||||
|
||||
kv "github.com/Azure/azure-sdk-for-go/profiles/latest/keyvault/keyvault"
|
||||
|
||||
azauth "github.com/dapr/components-contrib/authentication/azure"
|
||||
"github.com/dapr/components-contrib/secretstores"
|
||||
"github.com/dapr/kit/logger"
|
||||
)
|
||||
|
||||
// Keyvault secret store component metadata properties
|
||||
// This is in addition to what's defined in authentication/azure
|
||||
const (
|
||||
componentSPNCertificate = "spnCertificate"
|
||||
componentSPNCertificateFile = "spnCertificateFile"
|
||||
componentSPNCertificatePassword = "spnCertificatePassword"
|
||||
componentSPNClientID = "spnClientId"
|
||||
componentSPNTenantID = "spnTenantId"
|
||||
componentVaultName = "vaultName"
|
||||
VersionID = "version_id"
|
||||
secretItemIDPrefix = "/secrets/"
|
||||
|
||||
// AzureCloud urls refer to https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#dns-suffixes-for-base-url
|
||||
AzureCloud = ".vault.azure.net"
|
||||
AzureChinaCloud = ".vault.azure.cn"
|
||||
AzureUSGov = ".vault.usgovcloudapi.net"
|
||||
AzureGermanCloud = ".vault.microsoftazure.de"
|
||||
https = "https://"
|
||||
componentVaultName = "vaultName"
|
||||
VersionID = "version_id"
|
||||
secretItemIDPrefix = "/secrets/"
|
||||
)
|
||||
|
||||
type keyvaultSecretStore struct {
|
||||
vaultName string
|
||||
vaultClient kv.BaseClient
|
||||
vaultName string
|
||||
vaultClient kv.BaseClient
|
||||
vaultDNSSuffix string
|
||||
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewAzureKeyvaultSecretStore returns a new Kubernetes secret store
|
||||
// NewAzureKeyvaultSecretStore returns a new Azure Key Vault secret store
|
||||
func NewAzureKeyvaultSecretStore(logger logger.Logger) secretstores.SecretStore {
|
||||
return &keyvaultSecretStore{
|
||||
vaultName: "",
|
||||
|
@ -51,10 +43,11 @@ func NewAzureKeyvaultSecretStore(logger logger.Logger) secretstores.SecretStore
|
|||
}
|
||||
}
|
||||
|
||||
// Init creates a Kubernetes client
|
||||
// Init creates a Azure Key Vault client
|
||||
func (k *keyvaultSecretStore) Init(metadata secretstores.Metadata) error {
|
||||
settings := EnvironmentSettings{
|
||||
Values: metadata.Properties,
|
||||
settings, err := azauth.NewEnvironmentSettings("keyvault", metadata.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authorizer, err := settings.GetAuthorizer()
|
||||
|
@ -63,6 +56,7 @@ func (k *keyvaultSecretStore) Init(metadata secretstores.Metadata) error {
|
|||
}
|
||||
|
||||
k.vaultName = settings.Values[componentVaultName]
|
||||
k.vaultDNSSuffix = settings.AzureEnvironment.KeyVaultDNSSuffix
|
||||
|
||||
return err
|
||||
}
|
||||
|
@ -138,17 +132,7 @@ func (k *keyvaultSecretStore) BulkGetSecret(req secretstores.BulkGetSecretReques
|
|||
|
||||
// getVaultURI returns Azure Key Vault URI
|
||||
func (k *keyvaultSecretStore) getVaultURI() string {
|
||||
for _, suffix := range []string{AzureCloud, AzureChinaCloud, AzureGermanCloud, AzureUSGov} {
|
||||
if strings.HasSuffix(k.vaultName, suffix) {
|
||||
if strings.HasPrefix(k.vaultName, https) {
|
||||
return k.vaultName
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s", https, k.vaultName)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s%s%s", https, k.vaultName, AzureCloud)
|
||||
return fmt.Sprintf("https://%s.%s", k.vaultName, k.vaultDNSSuffix)
|
||||
}
|
||||
|
||||
func (k *keyvaultSecretStore) getMaxResultsFromMetadata(metadata map[string]string) (*int32, error) {
|
||||
|
|
|
@ -39,6 +39,7 @@ import (
|
|||
"github.com/agrea/ptr"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
azauth "github.com/dapr/components-contrib/authentication/azure"
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/kit/logger"
|
||||
)
|
||||
|
@ -46,8 +47,8 @@ import (
|
|||
const (
|
||||
keyDelimiter = "||"
|
||||
accountNameKey = "accountName"
|
||||
accountKeyKey = "accountKey"
|
||||
containerNameKey = "containerName"
|
||||
endpointKey = "endpoint"
|
||||
contentType = "ContentType"
|
||||
contentMD5 = "ContentMD5"
|
||||
contentEncoding = "ContentEncoding"
|
||||
|
@ -68,7 +69,6 @@ type StateStore struct {
|
|||
|
||||
type blobStorageMetadata struct {
|
||||
accountName string
|
||||
accountKey string
|
||||
containerName string
|
||||
}
|
||||
|
||||
|
@ -79,15 +79,25 @@ func (r *StateStore) Init(metadata state.Metadata) error {
|
|||
return err
|
||||
}
|
||||
|
||||
credential, err := azblob.NewSharedKeyCredential(meta.accountName, meta.accountKey)
|
||||
credential, env, err := azauth.GetAzureStorageCredentials(r.logger, meta.accountName, metadata.Properties)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid credentials with error: %s", err.Error())
|
||||
}
|
||||
|
||||
p := azblob.NewPipeline(credential, azblob.PipelineOptions{})
|
||||
|
||||
URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", meta.accountName, meta.containerName))
|
||||
containerURL := azblob.NewContainerURL(*URL, p)
|
||||
var containerURL azblob.ContainerURL
|
||||
customEndpoint, ok := metadata.Properties[endpointKey]
|
||||
if ok && customEndpoint != "" {
|
||||
URL, parseErr := url.Parse(fmt.Sprintf("%s/%s/%s", customEndpoint, meta.accountName, meta.containerName))
|
||||
if parseErr != nil {
|
||||
return err
|
||||
}
|
||||
containerURL = azblob.NewContainerURL(*URL, p)
|
||||
} else {
|
||||
URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.%s/%s", meta.accountName, env.StorageEndpointSuffix, meta.containerName))
|
||||
containerURL = azblob.NewContainerURL(*URL, p)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = containerURL.Create(ctx, azblob.Metadata{}, azblob.PublicAccessNone)
|
||||
|
@ -169,16 +179,10 @@ func getBlobStorageMetadata(metadata map[string]string) (*blobStorageMetadata, e
|
|||
return nil, fmt.Errorf("missing or empty %s field from metadata", accountNameKey)
|
||||
}
|
||||
|
||||
if val, ok := metadata[accountKeyKey]; ok && val != "" {
|
||||
meta.accountKey = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing of empty %s field from metadata", accountKeyKey)
|
||||
}
|
||||
|
||||
if val, ok := metadata[containerNameKey]; ok && val != "" {
|
||||
meta.containerName = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing of empty %s field from metadata", containerNameKey)
|
||||
return nil, fmt.Errorf("missing or empty %s field from metadata", containerNameKey)
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
|
|
|
@ -50,13 +50,11 @@ func TestGetBlobStorageMetaData(t *testing.T) {
|
|||
t.Run("All parameters passed and parsed", func(t *testing.T) {
|
||||
m := make(map[string]string)
|
||||
m["accountName"] = "acc"
|
||||
m["accountKey"] = "key"
|
||||
m["containerName"] = "dapr"
|
||||
meta, err := getBlobStorageMetadata(m)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "acc", meta.accountName)
|
||||
assert.Equal(t, "key", meta.accountKey)
|
||||
assert.Equal(t, "dapr", meta.containerName)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -170,13 +170,13 @@ func getTablesMetadata(metadata map[string]string) (*tablesMetadata, error) {
|
|||
if val, ok := metadata[accountKeyKey]; ok && val != "" {
|
||||
meta.accountKey = val
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf("missing of empty %s field from metadata", accountKeyKey))
|
||||
return nil, errors.New(fmt.Sprintf("missing or empty %s field from metadata", accountKeyKey))
|
||||
}
|
||||
|
||||
if val, ok := metadata[tableNameKey]; ok && val != "" {
|
||||
meta.tableName = val
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf("missing of empty %s field from metadata", tableNameKey))
|
||||
return nil, errors.New(fmt.Sprintf("missing or empty %s field from metadata", tableNameKey))
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
|
|
Loading…
Reference in New Issue