From 09018c89c8a576a141d9090edd843da275e6f764 Mon Sep 17 00:00:00 2001 From: Le Tran Date: Mon, 5 Sep 2022 17:18:16 -0400 Subject: [PATCH] Add azure managed identity to azure package in blockstorage (#1615) * Add utilities functions for Azure authentication + unit test to be added for utilities Signed-off-by: Le Tran * Add an interface for Azure blockstorage to Authenticate credentials to be added fro authenticate Signed-off-by: Le Tran * Add an option to use MSI Authorizer for Azure Signed-off-by: Le Tran * Add guarding for nil + clean up comment Signed-off-by: Le Tran * Update gomod and gosum files Signed-off-by: Le Tran * Expose functions checking for creds Signed-off-by: Le Tran * Add authenticator for client secret credentials type Signed-off-by: Le Tran * Clean up according to PR reviews Signed-off-by: Le Tran * Remove CredAvailibility, authentication status will be inferred from error Signed-off-by: Le Tran * Address review comments Signed-off-by: Le Tran * Make Azure authenticator a public interface that can be used directly Signed-off-by: Le Tran Signed-off-by: Le Tran Co-authored-by: Le Tran Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 3 +- pkg/blockstorage/azure/auth.go | 108 +++++++++++++++++++++++++++ pkg/blockstorage/azure/auth_tests.go | 94 +++++++++++++++++++++++ pkg/blockstorage/azure/client.go | 41 ++++++---- 5 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 pkg/blockstorage/azure/auth.go create mode 100644 pkg/blockstorage/azure/auth_tests.go diff --git a/go.mod b/go.mod index 714c6b1b1b..44d26507a9 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ replace ( require ( github.com/Azure/azure-sdk-for-go v66.0.0+incompatible github.com/Azure/go-autorest/autorest v0.11.28 + github.com/Azure/go-autorest/autorest/adal v0.9.21 github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 github.com/Azure/go-autorest/autorest/to v0.4.0 github.com/BurntSushi/toml v1.2.0 @@ -72,7 +73,6 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect diff --git a/go.sum b/go.sum index c0f141ee69..dd7a7469ec 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,9 @@ github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEg github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/adal v0.9.16/go.mod h1:tGMin8I49Yij6AQ+rvV+Xa/zwxYQB5hmsd6DkfAx2+A= -github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= +github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA= github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg= github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4= diff --git a/pkg/blockstorage/azure/auth.go b/pkg/blockstorage/azure/auth.go new file mode 100644 index 0000000000..3156b73159 --- /dev/null +++ b/pkg/blockstorage/azure/auth.go @@ -0,0 +1,108 @@ +package azure + +import ( + "context" + + "github.com/Azure/go-autorest/autorest/adal" + "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/kanisterio/kanister/pkg/blockstorage" + "github.com/pkg/errors" +) + +// currently avaialble types: https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authorization +// to be available with azidentity: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#readme-credential-types +// determine if the combination of creds are client secret creds +func isClientCredsAvailable(config map[string]string) bool { + return (config[blockstorage.AzureTenantID] != "" && + config[blockstorage.AzureCientID] != "" && + config[blockstorage.AzureClentSecret] != "") +} + +// determine if the combination of creds are MSI creds +func isMSICredsAvailable(config map[string]string) bool { + return (config[blockstorage.AzureTenantID] == "" && + config[blockstorage.AzureCientID] != "" && + config[blockstorage.AzureClentSecret] == "") +} + +// Public interface to authenticate with different Azure credentials type +type AzureAuthenticator interface { + Authenticate(creds map[string]string) error +} + +func NewAzureAutheticator(config map[string]string) (AzureAuthenticator, error) { + switch { + case isMSICredsAvailable(config): + return &msiAuthenticator{}, nil + case isClientCredsAvailable(config): + return &clientSecretAuthenticator{}, nil + default: + return nil, errors.New("Fail to get an authenticator for provided creds combination") + } +} + +// authenticate with MSI creds +type msiAuthenticator struct{} + +func (m *msiAuthenticator) Authenticate(creds map[string]string) error { + // check if MSI endpoint is available + if !adal.MSIAvailable(context.Background(), nil) { + return errors.New("MSI endpoint is not supported") + } + // create a service principal token + msiConfig := auth.NewMSIConfig() + msiConfig.ClientID = creds[blockstorage.AzureCientID] + spt, err := msiConfig.ServicePrincipalToken() + if err != nil { + return errors.Wrap(err, "Failed to create a service principal token") + } + // network call to check for token + err = spt.Refresh() + if err != nil { + return errors.Wrap(err, "Failed to refresh token") + } + // creds passed authentication + return nil +} + +// authenticate with client secret creds +type clientSecretAuthenticator struct{} + +func (c *clientSecretAuthenticator) Authenticate(creds map[string]string) error { + credConfig, err := getCredConfigForAuth(creds) + if err != nil { + return errors.Wrap(err, "Failed to get Client Secret config") + } + // create a service principal token + spt, err := credConfig.ServicePrincipalToken() + if err != nil { + return errors.Wrap(err, "Failed to create a service principal token") + } + // network call to check for token + err = spt.Refresh() + if err != nil { + return errors.Wrap(err, "Failed to refresh token") + } + // creds passed authentication + return nil +} + +func getCredConfigForAuth(config map[string]string) (auth.ClientCredentialsConfig, error) { + tenantID, ok := config[blockstorage.AzureTenantID] + if !ok { + return auth.ClientCredentialsConfig{}, errors.New("Cannot get tenantID from config") + } + + clientID, ok := config[blockstorage.AzureCientID] + if !ok { + return auth.ClientCredentialsConfig{}, errors.New("Cannot get clientID from config") + } + + clientSecret, ok := config[blockstorage.AzureClentSecret] + if !ok { + return auth.ClientCredentialsConfig{}, errors.New("Cannot get clientSecret from config") + } + + credConfig := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID) + return credConfig, nil +} diff --git a/pkg/blockstorage/azure/auth_tests.go b/pkg/blockstorage/azure/auth_tests.go new file mode 100644 index 0000000000..20c76fe8e2 --- /dev/null +++ b/pkg/blockstorage/azure/auth_tests.go @@ -0,0 +1,94 @@ +// Copyright 2022 The Kanister Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package azure + +import ( + "github.com/kanisterio/kanister/pkg/blockstorage" + . "gopkg.in/check.v1" +) + +type AuthSuite struct{} + +var _ = Suite(&AuthSuite{}) + +func (s *AuthSuite) SetUpSuite(c *C) { +} + +func (s *AuthSuite) TestIsClientCredsvailable(c *C) { + // success + config := map[string]string{ + blockstorage.AzureTenantID: "some-tenant-id", + blockstorage.AzureCientID: "some-client-id", + blockstorage.AzureClentSecret: "someclient-secret", + } + c.Assert(isClientCredsAvailable(config), Equals, true) + + // remove tenantID + delete(config, blockstorage.AzureTenantID) + c.Assert(isClientCredsAvailable(config), Equals, false) + + // remove client secret, only client ID left + delete(config, blockstorage.AzureClentSecret) + c.Assert(isClientCredsAvailable(config), Equals, false) +} + +func (s *AuthSuite) TestIsMSICredsAvailable(c *C) { + // success + config := map[string]string{ + blockstorage.AzureTenantID: "some-tenant-id", + blockstorage.AzureCientID: "some-client-id", + blockstorage.AzureClentSecret: "someclient-secret", + } + c.Assert(isMSICredsAvailable(config), Equals, false) + + // remove tenantID + delete(config, blockstorage.AzureTenantID) + c.Assert(isMSICredsAvailable(config), Equals, false) + + // remove client secret, only client ID left + delete(config, blockstorage.AzureClentSecret) + c.Assert(isMSICredsAvailable(config), Equals, true) +} + +func (s *AuthSuite) TestNewAzureAutheticator(c *C) { + // successful with client secret creds + config := map[string]string{ + blockstorage.AzureTenantID: "some-tenant-id", + blockstorage.AzureCientID: "some-client-id", + blockstorage.AzureClentSecret: "some-client-secret", + } + authenticator, err := NewAzureAutheticator(config) + c.Assert(err, IsNil) + _, ok := authenticator.(*clientSecretAuthenticator) + c.Assert(ok, Equals, true) + + // successful with msi creds + config = map[string]string{ + blockstorage.AzureCientID: "some-client-id", + } + authenticator, err = NewAzureAutheticator(config) + c.Assert(err, IsNil) + _, ok = authenticator.(*msiAuthenticator) + c.Assert(ok, Equals, true) + + // unsuccessful with an undefined combo of creds + config = map[string]string{ + blockstorage.AzureCientID: "some-client-id", + blockstorage.AzureClentSecret: "some-client-secret", + } + authenticator, err = NewAzureAutheticator(config) + c.Assert(err, NotNil) + c.Assert(authenticator, IsNil) +} diff --git a/pkg/blockstorage/azure/client.go b/pkg/blockstorage/azure/client.go index 333b29a144..5233157f1b 100644 --- a/pkg/blockstorage/azure/client.go +++ b/pkg/blockstorage/azure/client.go @@ -98,15 +98,23 @@ func NewClient(ctx context.Context, config map[string]string) (*Client, error) { // nolint:unparam func getAuthorizer(env azure.Environment, config map[string]string) (*autorest.BearerAuthorizer, error) { + if isClientCredsAvailable(config) { + return getClientCredsAuthorizer(env, config) + } else if isMSICredsAvailable(config) { + return getMSIsAuthorizer(config) + } + return nil, errors.New("Missing credentials, or credential type not supported") +} + +func getClientCredsAuthorizer(env azure.Environment, config map[string]string) (*autorest.BearerAuthorizer, error) { credConfig, err := getCredConfig(env, config) if err != nil { - return nil, errors.Wrap(err, "Failed to get Azure ClientCredentialsConfig") + return nil, errors.Wrap(err, "Failed to get Azure Client Credentials Config") } a, err := credConfig.Authorizer() if err != nil { - return nil, errors.Wrap(err, "Failed to get Azure authorizer") + return nil, errors.Wrap(err, "Failed to get Azure Client Credentials authorizer") } - ba, ok := a.(*autorest.BearerAuthorizer) if !ok { return nil, errors.New("Failed to get Azure authorizer") @@ -114,23 +122,26 @@ func getAuthorizer(env azure.Environment, config map[string]string) (*autorest.B return ba, nil } -func getCredConfig(env azure.Environment, config map[string]string) (auth.ClientCredentialsConfig, error) { - tenantID, ok := config[blockstorage.AzureTenantID] - if !ok { - return auth.ClientCredentialsConfig{}, errors.New("Cannot get tenantID from config") +func getMSIsAuthorizer(config map[string]string) (*autorest.BearerAuthorizer, error) { + msiConfig := auth.NewMSIConfig() + msiConfig.ClientID = config[blockstorage.AzureCientID] + a, err := msiConfig.Authorizer() + if err != nil { + return nil, errors.Wrap(err, "Failed to get Azure MSI authorizer") } - - clientID, ok := config[blockstorage.AzureCientID] + ba, ok := a.(*autorest.BearerAuthorizer) if !ok { - return auth.ClientCredentialsConfig{}, errors.New("Cannot get clientID from config") + return nil, errors.New("Failed to get Azure authorizer") } + return ba, nil +} - clientSecret, ok := config[blockstorage.AzureClentSecret] - if !ok { - return auth.ClientCredentialsConfig{}, errors.New("Cannot get clientSecret from config") +func getCredConfig(env azure.Environment, config map[string]string) (auth.ClientCredentialsConfig, error) { + credConfig, err := getCredConfigForAuth(config) + if err != nil { + return auth.ClientCredentialsConfig{}, err } - - credConfig := auth.NewClientCredentialsConfig(clientID, clientSecret, tenantID) + var ok bool if credConfig.AADEndpoint, ok = config[blockstorage.AzureActiveDirEndpoint]; !ok || credConfig.AADEndpoint == "" { credConfig.AADEndpoint = env.ActiveDirectoryEndpoint config[blockstorage.AzureActiveDirEndpoint] = credConfig.AADEndpoint