github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/azure/credentials_test.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure_test
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  
    10  	"github.com/juju/cmd/v3/cmdtesting"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/testing"
    13  	jc "github.com/juju/testing/checkers"
    14  	gc "gopkg.in/check.v1"
    15  
    16  	"github.com/juju/juju/cloud"
    17  	"github.com/juju/juju/environs"
    18  	envtesting "github.com/juju/juju/environs/testing"
    19  	"github.com/juju/juju/provider/azure"
    20  	"github.com/juju/juju/provider/azure/internal/azureauth"
    21  	"github.com/juju/juju/provider/azure/internal/azurecli"
    22  	"github.com/juju/juju/provider/azure/internal/azuretesting"
    23  )
    24  
    25  type credentialsSuite struct {
    26  	testing.IsolationSuite
    27  	servicePrincipalCreator servicePrincipalCreator
    28  	azureCLI                azureCLI
    29  	provider                environs.EnvironProvider
    30  	sender                  azuretesting.Senders
    31  }
    32  
    33  var _ = gc.Suite(&credentialsSuite{})
    34  
    35  func (s *credentialsSuite) SetUpTest(c *gc.C) {
    36  	s.IsolationSuite.SetUpTest(c)
    37  	s.servicePrincipalCreator = servicePrincipalCreator{}
    38  	s.azureCLI = azureCLI{}
    39  	s.provider = newProvider(c, azure.ProviderConfig{
    40  		ServicePrincipalCreator: &s.servicePrincipalCreator,
    41  		AzureCLI:                &s.azureCLI,
    42  		Sender:                  azuretesting.NewSerialSender(&s.sender),
    43  	})
    44  }
    45  
    46  func (s *credentialsSuite) TestCredentialSchemas(c *gc.C) {
    47  	envtesting.AssertProviderAuthTypes(c, s.provider,
    48  		"interactive",
    49  		"service-principal-secret",
    50  	)
    51  }
    52  
    53  func (s *credentialsSuite) TestServicePrincipalSecretCredentialsValid(c *gc.C) {
    54  	envtesting.AssertProviderCredentialsValid(c, s.provider, "service-principal-secret", map[string]string{
    55  		"application-id":          "application",
    56  		"application-password":    "password",
    57  		"subscription-id":         "subscription",
    58  		"managed-subscription-id": "managed-subscription",
    59  	})
    60  }
    61  
    62  func (s *credentialsSuite) TestServicePrincipalSecretHiddenAttributes(c *gc.C) {
    63  	envtesting.AssertProviderCredentialsAttributesHidden(c, s.provider, "service-principal-secret", "application-password")
    64  }
    65  
    66  func (s *credentialsSuite) TestDetectCredentialsNoAccounts(c *gc.C) {
    67  	_, err := s.provider.DetectCredentials("")
    68  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
    69  	calls := s.azureCLI.Calls()
    70  	c.Assert(calls, gc.HasLen, 1)
    71  	c.Assert(calls[0].FuncName, gc.Equals, "ListAccounts")
    72  }
    73  
    74  func (s *credentialsSuite) TestDetectCredentialsListError(c *gc.C) {
    75  	s.azureCLI.SetErrors(errors.New("test error"))
    76  	_, err := s.provider.DetectCredentials("")
    77  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
    78  }
    79  
    80  func (s *credentialsSuite) TestDetectCredentialsOneAccount(c *gc.C) {
    81  	s.azureCLI.Accounts = []azurecli.Account{{
    82  		CloudName:    "AzureCloud",
    83  		ID:           "test-account-id",
    84  		IsDefault:    true,
    85  		Name:         "test-account",
    86  		State:        "Enabled",
    87  		TenantId:     "tenant-id",
    88  		HomeTenantId: "home-tenant-id",
    89  	}}
    90  	s.azureCLI.Clouds = []azurecli.Cloud{{
    91  		IsActive: true,
    92  		Name:     "AzureCloud",
    93  	}}
    94  	cred, err := s.provider.DetectCredentials("")
    95  	c.Assert(err, jc.ErrorIsNil)
    96  	c.Assert(cred, gc.Not(gc.IsNil))
    97  	c.Assert(cred.DefaultCredential, gc.Equals, "test-account")
    98  	c.Assert(cred.DefaultRegion, gc.Equals, "")
    99  	c.Assert(cred.AuthCredentials, gc.HasLen, 1)
   100  	c.Assert(cred.AuthCredentials["test-account"].Label, gc.Equals, "AzureCloud subscription test-account")
   101  
   102  	calls := s.azureCLI.Calls()
   103  	c.Assert(calls, gc.HasLen, 2)
   104  	c.Assert(calls[0].FuncName, gc.Equals, "ListAccounts")
   105  	c.Assert(calls[1].FuncName, gc.Equals, "ListClouds")
   106  
   107  	calls = s.servicePrincipalCreator.Calls()
   108  	c.Assert(calls, gc.HasLen, 1)
   109  	c.Assert(calls[0].FuncName, gc.Equals, "Create")
   110  	params, ok := calls[0].Args[1].(azureauth.ServicePrincipalParams)
   111  	c.Assert(ok, jc.IsTrue)
   112  	params.Credential = nil
   113  	c.Assert(params, jc.DeepEquals, azureauth.ServicePrincipalParams{
   114  		SubscriptionId: "test-account-id",
   115  		TenantId:       "tenant-id",
   116  	})
   117  }
   118  
   119  func (s *credentialsSuite) TestDetectCredentialsCloudError(c *gc.C) {
   120  	s.azureCLI.Accounts = []azurecli.Account{{
   121  		CloudName: "AzureCloud",
   122  		ID:        "test-account-id",
   123  		IsDefault: true,
   124  		Name:      "test-account",
   125  		State:     "Enabled",
   126  		TenantId:  "tenant-id",
   127  	}}
   128  	s.azureCLI.Clouds = []azurecli.Cloud{{
   129  		IsActive: true,
   130  		Name:     "AzureCloud",
   131  	}}
   132  	s.azureCLI.SetErrors(nil, errors.New("test error"))
   133  	cred, err := s.provider.DetectCredentials("")
   134  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   135  	c.Assert(cred, gc.IsNil)
   136  
   137  	calls := s.azureCLI.Calls()
   138  	c.Assert(calls, gc.HasLen, 2)
   139  	c.Assert(calls[0].FuncName, gc.Equals, "ListAccounts")
   140  	c.Assert(calls[1].FuncName, gc.Equals, "ListClouds")
   141  
   142  	calls = s.servicePrincipalCreator.Calls()
   143  	c.Assert(calls, gc.HasLen, 0)
   144  }
   145  
   146  func (s *credentialsSuite) TestDetectCredentialsTwoAccounts(c *gc.C) {
   147  	s.azureCLI.Accounts = []azurecli.Account{{
   148  		CloudName: "AzureCloud",
   149  		ID:        "test-account1-id",
   150  		IsDefault: true,
   151  		Name:      "test-account1",
   152  		State:     "Enabled",
   153  		TenantId:  "tenant-id",
   154  	}, {
   155  		CloudName: "AzureCloud",
   156  		ID:        "test-account2-id",
   157  		IsDefault: false,
   158  		Name:      "test-account2",
   159  		State:     "Enabled",
   160  		TenantId:  "tenant-id2",
   161  	}}
   162  	s.azureCLI.Clouds = []azurecli.Cloud{{
   163  		IsActive: true,
   164  		Name:     "AzureCloud",
   165  	}}
   166  	cred, err := s.provider.DetectCredentials("")
   167  	c.Assert(err, jc.ErrorIsNil)
   168  	c.Assert(cred, gc.Not(gc.IsNil))
   169  	c.Assert(cred.DefaultCredential, gc.Equals, "test-account1")
   170  	c.Assert(cred.DefaultRegion, gc.Equals, "")
   171  	c.Assert(cred.AuthCredentials, gc.HasLen, 2)
   172  	c.Assert(cred.AuthCredentials["test-account1"].Label, gc.Equals, "AzureCloud subscription test-account1")
   173  	c.Assert(cred.AuthCredentials["test-account2"].Label, gc.Equals, "AzureCloud subscription test-account2")
   174  
   175  	calls := s.azureCLI.Calls()
   176  	c.Assert(calls, gc.HasLen, 2)
   177  	c.Assert(calls[0].FuncName, gc.Equals, "ListAccounts")
   178  	c.Assert(calls[1].FuncName, gc.Equals, "ListClouds")
   179  
   180  	calls = s.servicePrincipalCreator.Calls()
   181  	c.Assert(calls, gc.HasLen, 2)
   182  	c.Assert(calls[0].FuncName, gc.Equals, "Create")
   183  	params, ok := calls[0].Args[1].(azureauth.ServicePrincipalParams)
   184  	c.Assert(ok, jc.IsTrue)
   185  	params.Credential = nil
   186  	c.Assert(params, jc.DeepEquals, azureauth.ServicePrincipalParams{
   187  		SubscriptionId: "test-account1-id",
   188  		TenantId:       "tenant-id",
   189  	})
   190  	c.Assert(calls[1].FuncName, gc.Equals, "Create")
   191  	params, ok = calls[1].Args[1].(azureauth.ServicePrincipalParams)
   192  	c.Assert(ok, jc.IsTrue)
   193  	params.Credential = nil
   194  	c.Assert(params, jc.DeepEquals, azureauth.ServicePrincipalParams{
   195  		SubscriptionId: "test-account2-id",
   196  		TenantId:       "tenant-id2",
   197  	})
   198  }
   199  
   200  func (s *credentialsSuite) TestDetectCredentialsTwoAccountsOneError(c *gc.C) {
   201  	s.azureCLI.Accounts = []azurecli.Account{{
   202  		CloudName: "AzureCloud",
   203  		ID:        "test-account1-id",
   204  		IsDefault: false,
   205  		Name:      "test-account1",
   206  		State:     "Enabled",
   207  		TenantId:  "tenant-id",
   208  	}, {
   209  		CloudName: "AzureCloud",
   210  		ID:        "test-account2-id",
   211  		IsDefault: true,
   212  		Name:      "test-account2",
   213  		State:     "Enabled",
   214  		TenantId:  "tenant-id2",
   215  	}}
   216  	s.azureCLI.Clouds = []azurecli.Cloud{{
   217  		IsActive: true,
   218  		Name:     "AzureCloud",
   219  	}}
   220  	s.servicePrincipalCreator.SetErrors(nil, errors.New("test error"))
   221  	cred, err := s.provider.DetectCredentials("")
   222  	c.Assert(err, jc.ErrorIsNil)
   223  	c.Assert(cred, gc.Not(gc.IsNil))
   224  	c.Assert(cred.DefaultCredential, gc.Equals, "")
   225  	c.Assert(cred.DefaultRegion, gc.Equals, "")
   226  	c.Assert(cred.AuthCredentials, gc.HasLen, 1)
   227  	c.Assert(cred.AuthCredentials["test-account1"].Label, gc.Equals, "AzureCloud subscription test-account1")
   228  
   229  	calls := s.azureCLI.Calls()
   230  	c.Assert(calls, gc.HasLen, 2)
   231  	c.Assert(calls[0].FuncName, gc.Equals, "ListAccounts")
   232  	c.Assert(calls[1].FuncName, gc.Equals, "ListClouds")
   233  
   234  	calls = s.servicePrincipalCreator.Calls()
   235  	c.Assert(calls, gc.HasLen, 2)
   236  	c.Assert(calls[0].FuncName, gc.Equals, "Create")
   237  	params, ok := calls[0].Args[1].(azureauth.ServicePrincipalParams)
   238  	c.Assert(ok, jc.IsTrue)
   239  	params.Credential = nil
   240  	c.Assert(params, jc.DeepEquals, azureauth.ServicePrincipalParams{
   241  		SubscriptionId: "test-account1-id",
   242  		TenantId:       "tenant-id",
   243  	})
   244  	c.Assert(calls[1].FuncName, gc.Equals, "Create")
   245  	params, ok = calls[1].Args[1].(azureauth.ServicePrincipalParams)
   246  	c.Assert(ok, jc.IsTrue)
   247  	params.Credential = nil
   248  	c.Assert(params, jc.DeepEquals, azureauth.ServicePrincipalParams{
   249  		SubscriptionId: "test-account2-id",
   250  		TenantId:       "tenant-id2",
   251  	})
   252  }
   253  
   254  func (s *credentialsSuite) TestFinalizeCredentialInteractive(c *gc.C) {
   255  	s.sender = azuretesting.Senders{discoverAuthSender()}
   256  	in := cloud.NewCredential("interactive", map[string]string{"subscription-id": fakeSubscriptionId})
   257  	ctx := cmdtesting.Context(c)
   258  	out, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   259  		CloudName:             "azure",
   260  		Credential:            in,
   261  		CloudEndpoint:         "https://arm.invalid",
   262  		CloudStorageEndpoint:  "https://core.invalid",
   263  		CloudIdentityEndpoint: "https://graph.invalid",
   264  	})
   265  	c.Assert(err, jc.ErrorIsNil)
   266  	c.Assert(out, gc.NotNil)
   267  	c.Assert(out.AuthType(), gc.Equals, cloud.AuthType("service-principal-secret"))
   268  	c.Assert(out.Attributes(), jc.DeepEquals, map[string]string{
   269  		"application-id":        "appid",
   270  		"application-password":  "service-principal-password",
   271  		"application-object-id": "application-object-id",
   272  		"subscription-id":       fakeSubscriptionId,
   273  	})
   274  
   275  	s.servicePrincipalCreator.CheckCallNames(c, "InteractiveCreate")
   276  	args := s.servicePrincipalCreator.Calls()[0].Args
   277  	c.Assert(args[2], jc.DeepEquals, azureauth.ServicePrincipalParams{
   278  		CloudName:      "AzureCloud",
   279  		SubscriptionId: fakeSubscriptionId,
   280  		TenantId:       fakeTenantId,
   281  	})
   282  }
   283  
   284  func (s *credentialsSuite) TestFinalizeCredentialInteractiveError(c *gc.C) {
   285  	s.sender = azuretesting.Senders{discoverAuthSender()}
   286  	in := cloud.NewCredential("interactive", map[string]string{"subscription-id": fakeSubscriptionId})
   287  	s.servicePrincipalCreator.SetErrors(errors.New("blargh"))
   288  	ctx := cmdtesting.Context(c)
   289  	_, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   290  		CloudName:             "azure",
   291  		Credential:            in,
   292  		CloudEndpoint:         "https://arm.invalid",
   293  		CloudIdentityEndpoint: "https://graph.invalid",
   294  	})
   295  	c.Assert(err, gc.ErrorMatches, "blargh")
   296  }
   297  
   298  func (s *credentialsSuite) TestFinalizeCredentialAzureCLI(c *gc.C) {
   299  	s.azureCLI.Accounts = []azurecli.Account{{
   300  		CloudName: "AzureCloud",
   301  		ID:        "test-account1-id",
   302  		IsDefault: true,
   303  		Name:      "test-account1",
   304  		State:     "Enabled",
   305  		TenantId:  "tenant-id",
   306  	}, {
   307  		CloudName: "AzureCloud",
   308  		ID:        "test-account2-id",
   309  		IsDefault: false,
   310  		Name:      "test-account2",
   311  		State:     "Enabled",
   312  		TenantId:  "tenant-id",
   313  	}}
   314  	s.azureCLI.Clouds = []azurecli.Cloud{{
   315  		IsActive: true,
   316  		Name:     "AzureCloud",
   317  	}}
   318  	in := cloud.NewCredential("interactive", nil)
   319  	ctx := cmdtesting.Context(c)
   320  	cred, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   321  		Credential: in,
   322  		CloudName:  "azure",
   323  	})
   324  	c.Assert(err, jc.ErrorIsNil)
   325  	c.Assert(cred, gc.Not(gc.IsNil))
   326  	c.Assert(cred.AuthType(), gc.Equals, cloud.AuthType("service-principal-secret"))
   327  	attrs := cred.Attributes()
   328  	c.Assert(attrs["subscription-id"], gc.Equals, "test-account1-id")
   329  	c.Assert(attrs["application-id"], gc.Equals, "appid")
   330  	c.Assert(attrs["application-password"], gc.Equals, "service-principal-password")
   331  	c.Assert(attrs["application-object-id"], gc.Equals, "application-object-id")
   332  }
   333  
   334  func (s *credentialsSuite) TestFinalizeCredentialAzureCLIShowAccountError(c *gc.C) {
   335  	s.azureCLI.Accounts = []azurecli.Account{{
   336  		CloudName: "AzureCloud",
   337  		ID:        "test-account1-id",
   338  		IsDefault: true,
   339  		Name:      "test-account1",
   340  		State:     "Enabled",
   341  		TenantId:  "tenant-id",
   342  	}, {
   343  		CloudName: "AzureCloud",
   344  		ID:        "test-account2-id",
   345  		IsDefault: false,
   346  		Name:      "test-account2",
   347  		State:     "Enabled",
   348  		TenantId:  "tenant-id",
   349  	}}
   350  	s.azureCLI.Clouds = []azurecli.Cloud{{
   351  		IsActive: true,
   352  		Name:     "AzureCloud",
   353  	}}
   354  	s.azureCLI.SetErrors(errors.New("test error"))
   355  	in := cloud.NewCredential("interactive", nil)
   356  	ctx := cmdtesting.Context(c)
   357  	cred, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   358  		Credential: in,
   359  		CloudName:  "azure",
   360  	})
   361  	c.Assert(err, gc.ErrorMatches, `cannot get accounts: test error`)
   362  	c.Assert(cred, gc.IsNil)
   363  }
   364  
   365  func (s *credentialsSuite) TestFinalizeCredentialAzureCLIGraphTokenError(c *gc.C) {
   366  	s.azureCLI.Accounts = []azurecli.Account{{
   367  		CloudName: "AzureCloud",
   368  		ID:        "test-account1-id",
   369  		IsDefault: true,
   370  		Name:      "test-account1",
   371  		State:     "Enabled",
   372  		TenantId:  "tenant-id",
   373  	}, {
   374  		CloudName: "AzureCloud",
   375  		ID:        "test-account2-id",
   376  		IsDefault: false,
   377  		Name:      "test-account2",
   378  		State:     "Enabled",
   379  		TenantId:  "tenant-id",
   380  	}}
   381  	s.azureCLI.Clouds = []azurecli.Cloud{{
   382  		IsActive: true,
   383  		Name:     "AzureCloud",
   384  	}}
   385  	s.servicePrincipalCreator.SetErrors(errors.New("test error"))
   386  	in := cloud.NewCredential("interactive", nil)
   387  	ctx := cmdtesting.Context(c)
   388  	cred, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   389  		Credential: in,
   390  		CloudName:  "azure",
   391  	})
   392  	c.Assert(err, gc.ErrorMatches, `cannot create service principal: test error`)
   393  	c.Assert(cred, gc.IsNil)
   394  }
   395  
   396  func (s *credentialsSuite) TestFinalizeCredentialAzureCLIServicePrincipalError(c *gc.C) {
   397  	s.azureCLI.Accounts = []azurecli.Account{{
   398  		CloudName: "AzureCloud",
   399  		ID:        "test-account1-id",
   400  		IsDefault: true,
   401  		Name:      "test-account1",
   402  		State:     "Enabled",
   403  		TenantId:  "tenant-id",
   404  	}, {
   405  		CloudName: "AzureCloud",
   406  		ID:        "test-account2-id",
   407  		IsDefault: false,
   408  		Name:      "test-account2",
   409  		State:     "Enabled",
   410  		TenantId:  "tenant-id",
   411  	}}
   412  	s.azureCLI.Clouds = []azurecli.Cloud{{
   413  		IsActive: true,
   414  		Name:     "AzureCloud",
   415  	}}
   416  	s.servicePrincipalCreator.SetErrors(errors.New("test error"))
   417  	in := cloud.NewCredential("interactive", nil)
   418  	ctx := cmdtesting.Context(c)
   419  	cred, err := s.provider.FinalizeCredential(ctx, environs.FinalizeCredentialParams{
   420  		Credential: in,
   421  		CloudName:  "azure",
   422  	})
   423  	c.Assert(err, gc.ErrorMatches, `cannot create service principal: test error`)
   424  	c.Assert(cred, gc.IsNil)
   425  }
   426  
   427  type servicePrincipalCreator struct {
   428  	testing.Stub
   429  }
   430  
   431  func (c *servicePrincipalCreator) InteractiveCreate(sdkCtx context.Context, stderr io.Writer, params azureauth.ServicePrincipalParams) (appId, spId, password string, _ error) {
   432  	c.MethodCall(c, "InteractiveCreate", sdkCtx, stderr, params)
   433  	return "appid", "application-object-id", "service-principal-password", c.NextErr()
   434  }
   435  
   436  func (c *servicePrincipalCreator) Create(sdkCtx context.Context, params azureauth.ServicePrincipalParams) (appId, spId, password string, _ error) {
   437  	c.MethodCall(c, "Create", sdkCtx, params)
   438  	return "appid", "application-object-id", "service-principal-password", c.NextErr()
   439  }
   440  
   441  type azureCLI struct {
   442  	testing.Stub
   443  	Accounts []azurecli.Account
   444  	Clouds   []azurecli.Cloud
   445  }
   446  
   447  func (e *azureCLI) ListAccounts() ([]azurecli.Account, error) {
   448  	e.MethodCall(e, "ListAccounts")
   449  	if err := e.NextErr(); err != nil {
   450  		return nil, err
   451  	}
   452  	return e.Accounts, nil
   453  }
   454  
   455  func (e *azureCLI) FindAccountsWithCloudName(name string) ([]azurecli.Account, error) {
   456  	e.MethodCall(e, "FindAccountsWithCloudName", name)
   457  	if err := e.NextErr(); err != nil {
   458  		return nil, err
   459  	}
   460  	var accs []azurecli.Account
   461  	for _, acc := range e.Accounts {
   462  		if acc.CloudName == name {
   463  			accs = append(accs, acc)
   464  		}
   465  	}
   466  	return accs, nil
   467  }
   468  
   469  func (e *azureCLI) ShowAccount(subscription string) (*azurecli.Account, error) {
   470  	e.MethodCall(e, "ShowAccount", subscription)
   471  	if err := e.NextErr(); err != nil {
   472  		return nil, err
   473  	}
   474  	return e.findAccount(subscription)
   475  }
   476  
   477  func (e *azureCLI) findAccount(tenant string) (*azurecli.Account, error) {
   478  	for _, acc := range e.Accounts {
   479  		if acc.AuthTenantId() == tenant {
   480  			return &acc, nil
   481  		}
   482  		if tenant == "" && acc.IsDefault {
   483  			return &acc, nil
   484  		}
   485  	}
   486  	return nil, errors.New("account not found")
   487  }
   488  
   489  func (e *azureCLI) ShowCloud(name string) (*azurecli.Cloud, error) {
   490  	e.MethodCall(e, "ShowCloud", name)
   491  	if err := e.NextErr(); err != nil {
   492  		return nil, err
   493  	}
   494  	for _, cloud := range e.Clouds {
   495  		if cloud.Name == name || name == "" {
   496  			return &cloud, nil
   497  		}
   498  	}
   499  	return nil, errors.New("cloud not found")
   500  }
   501  
   502  func (e *azureCLI) ListClouds() ([]azurecli.Cloud, error) {
   503  	e.MethodCall(e, "ListClouds")
   504  	if err := e.NextErr(); err != nil {
   505  		return nil, err
   506  	}
   507  	return e.Clouds, nil
   508  }