Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add azure managed identity to azure package in blockstorage #1615

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
108 changes: 108 additions & 0 deletions pkg/blockstorage/azure/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package azure

import (
"context"

"github.com/Azure/go-autorest/autorest/adal"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this package will be deprecated next year. Can we look into using azidentity instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of interface Authenticate() is to validate Azure creds as a one-of action. These creds are then passed on to an Authorizer() that later authenticates and return a token for every Azure API calls that interact with Azure resources.
And for Authorizer(), we are still using adal indirectly as part of github.com/Azure/go-autorest/autorest/azure/auth

Furthermore, azidentity and adal differs slightly in their credential definitions and workflows. So I'd prefer not to use azidentity for Authenticate() and adal for Authorizer() in case we don't completely understand the nuances in expectations.

Instead, I had abstracted out the logics as much as I can, which should make APIs swapping easier during migration :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you create a GH issue to track the migration task for both the adal and auth packages? Thanks.

Copy link
Contributor Author

@leuyentran leuyentran Sep 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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] != "" &&
leuyentran marked this conversation as resolved.
Show resolved Hide resolved
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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add comments for exported methods/structs in this PR.

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{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider renaming to msiAzureAuthenticator


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
Comment on lines +59 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIOLI: Consider just return spt.Refresh(), since Refresh() already returns comprehensive error messages. Same with clientSecretAuthenticator.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller, i.e. Authenticate() directly return results of these methods. Therefore, the explicitly here is meant for code readability for future developers to understand: err==nil is considered passing authentication.

}

// authenticate with client secret creds
type clientSecretAuthenticator struct{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: consider renaming to clientSecretAzureAuthenitcator.

Copy link
Contributor Author

@leuyentran leuyentran Sep 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the package name is azure , I think it is okay to leave the naming as is. If they need to be public for some reason in the future, it would still be azure.ClientSecretAuthenticator


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
}
94 changes: 94 additions & 0 deletions pkg/blockstorage/azure/auth_tests.go
Original file line number Diff line number Diff line change
@@ -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)
}
41 changes: 26 additions & 15 deletions pkg/blockstorage/azure/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,39 +98,50 @@ 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")
}
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
Expand Down