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:
Alessandro (Ale) Segala 2021-08-11 00:06:24 +02:00 committed by GitHub
parent aa7d2ee1dd
commit d0816e32a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 500 additions and 278 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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"

View File

@ -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
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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

View File

@ -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)
})
}

View File

@ -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