github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/secretbackends/secrets_test.go (about)

     1  // Copyright 2022 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package secretbackends_test
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/juju/clock"
    10  	"github.com/juju/clock/testclock"
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/schema"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	"go.uber.org/mock/gomock"
    17  	gc "gopkg.in/check.v1"
    18  	"gopkg.in/juju/environschema.v1"
    19  
    20  	"github.com/juju/juju/apiserver/authentication"
    21  	"github.com/juju/juju/apiserver/common"
    22  	commonsecrets "github.com/juju/juju/apiserver/common/secrets"
    23  	apiservererrors "github.com/juju/juju/apiserver/errors"
    24  	facademocks "github.com/juju/juju/apiserver/facade/mocks"
    25  	"github.com/juju/juju/apiserver/facades/client/secretbackends"
    26  	"github.com/juju/juju/apiserver/facades/client/secretbackends/mocks"
    27  	"github.com/juju/juju/core/permission"
    28  	"github.com/juju/juju/core/secrets"
    29  	"github.com/juju/juju/rpc/params"
    30  	"github.com/juju/juju/secrets/provider"
    31  	"github.com/juju/juju/state"
    32  	coretesting "github.com/juju/juju/testing"
    33  )
    34  
    35  type SecretsSuite struct {
    36  	testing.IsolationSuite
    37  
    38  	clock        clock.Clock
    39  	authorizer   *facademocks.MockAuthorizer
    40  	backendState *mocks.MockSecretsBackendState
    41  	secretsState *mocks.MockSecretsState
    42  	statePool    *mocks.MockStatePool
    43  }
    44  
    45  var _ = gc.Suite(&SecretsSuite{})
    46  
    47  func (s *SecretsSuite) SetUpTest(c *gc.C) {
    48  	s.IsolationSuite.SetUpTest(c)
    49  }
    50  
    51  func (s *SecretsSuite) setup(c *gc.C) *gomock.Controller {
    52  	ctrl := gomock.NewController(c)
    53  
    54  	s.authorizer = facademocks.NewMockAuthorizer(ctrl)
    55  	s.backendState = mocks.NewMockSecretsBackendState(ctrl)
    56  	s.secretsState = mocks.NewMockSecretsState(ctrl)
    57  	s.statePool = mocks.NewMockStatePool(ctrl)
    58  
    59  	s.clock = testclock.NewClock(time.Now())
    60  
    61  	return ctrl
    62  }
    63  
    64  func (s *SecretsSuite) expectAuthClient() {
    65  	s.authorizer.EXPECT().AuthClient().Return(true)
    66  }
    67  
    68  func (s *SecretsSuite) TestListSecretBackendsIAAS(c *gc.C) {
    69  	s.assertListSecretBackends(c, state.ModelTypeIAAS, nil, false)
    70  }
    71  
    72  func (s *SecretsSuite) TestListSecretBackendsFilterOnName(c *gc.C) {
    73  	s.assertListSecretBackends(c, state.ModelTypeIAAS, []string{"myvault"}, false)
    74  }
    75  
    76  func (s *SecretsSuite) TestListSecretBackendsCAAS(c *gc.C) {
    77  	s.assertListSecretBackends(c, state.ModelTypeCAAS, nil, false)
    78  }
    79  
    80  func (s *SecretsSuite) TestListSecretBackendsReveal(c *gc.C) {
    81  	s.assertListSecretBackends(c, state.ModelTypeIAAS, nil, true)
    82  }
    83  
    84  func ptr[T any](v T) *T {
    85  	return &v
    86  }
    87  
    88  type providerWithConfig struct {
    89  	provider.ProviderConfig
    90  	provider.SupportAuthRefresh
    91  	provider.SecretBackendProvider
    92  }
    93  
    94  func (providerWithConfig) ConfigSchema() environschema.Fields {
    95  	return environschema.Fields{
    96  		"token": {
    97  			Secret: true,
    98  		},
    99  	}
   100  }
   101  
   102  func (providerWithConfig) ConfigDefaults() schema.Defaults {
   103  	return schema.Defaults{
   104  		"namespace": "foo",
   105  	}
   106  }
   107  
   108  func (providerWithConfig) ValidateConfig(oldCfg, newCfg provider.ConfigAttrs) error {
   109  	return nil
   110  }
   111  
   112  type mockModel struct {
   113  	common.Model
   114  	modelType state.ModelType
   115  }
   116  
   117  func (m *mockModel) Name() string {
   118  	return "fred"
   119  }
   120  
   121  func (m *mockModel) Type() state.ModelType {
   122  	return m.modelType
   123  }
   124  
   125  func (s *SecretsSuite) assertListSecretBackends(c *gc.C, modelType state.ModelType, names []string, reveal bool) {
   126  	ctrl := s.setup(c)
   127  	defer ctrl.Finish()
   128  
   129  	s.expectAuthClient()
   130  	if reveal {
   131  		s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(nil)
   132  	}
   133  
   134  	p := mocks.NewMockSecretBackendProvider(ctrl)
   135  	p.EXPECT().Type().Return("vault")
   136  	b := mocks.NewMockSecretsBackend(ctrl)
   137  	b.EXPECT().Ping().Return(errors.New("ping error"))
   138  	s.PatchValue(&commonsecrets.GetProvider, func(string) (provider.SecretBackendProvider, error) {
   139  		return providerWithConfig{
   140  			SecretBackendProvider: p,
   141  		}, nil
   142  	})
   143  
   144  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   145  	c.Assert(err, jc.ErrorIsNil)
   146  
   147  	uuid := coretesting.ModelTag.Id()
   148  	if modelType == state.ModelTypeCAAS {
   149  		s.statePool.EXPECT().GetModel(uuid).Return(&mockModel{modelType: modelType}, func() bool { return true }, nil)
   150  	}
   151  
   152  	vaultConfig := map[string]interface{}{
   153  		"endpoint": "http://vault",
   154  		"token":    "s.ajehjdee",
   155  	}
   156  	p.EXPECT().NewBackend(&provider.ModelBackendConfig{
   157  		BackendConfig: provider.BackendConfig{BackendType: "vault", Config: vaultConfig},
   158  	}).Return(b, nil)
   159  
   160  	backends := map[string]set.Strings{
   161  		coretesting.ControllerTag.Id(): set.NewStrings("a"),
   162  		"backend-id":                   set.NewStrings("b", "c", "d"),
   163  		"backend-id-notfound":          set.NewStrings("z"),
   164  	}
   165  	if modelType == state.ModelTypeCAAS {
   166  		backends[uuid] = set.NewStrings("e", "f")
   167  	}
   168  	s.secretsState.EXPECT().ListModelSecrets(true).Return(backends, nil)
   169  	s.backendState.EXPECT().ListSecretBackends().Return(nil, nil)
   170  	s.backendState.EXPECT().GetSecretBackendByID("backend-id").Return(&secrets.SecretBackend{
   171  		ID:                  "backend-id",
   172  		Name:                "myvault",
   173  		BackendType:         "vault",
   174  		TokenRotateInterval: ptr(666 * time.Minute),
   175  		Config:              vaultConfig,
   176  	}, nil)
   177  	s.backendState.EXPECT().GetSecretBackendByID("backend-id-notfound").Return(nil, errors.NotFoundf(""))
   178  
   179  	results, err := facade.ListSecretBackends(params.ListSecretBackendsArgs{Names: names, Reveal: reveal})
   180  	c.Assert(err, jc.ErrorIsNil)
   181  	resultVaultCfg := map[string]interface{}{
   182  		"endpoint": "http://vault",
   183  		"token":    "s.ajehjdee",
   184  	}
   185  	if !reveal {
   186  		delete(resultVaultCfg, "token")
   187  	}
   188  	wanted := set.NewStrings(names...)
   189  	var backendResults []params.SecretBackendResult
   190  	if wanted.IsEmpty() || wanted.Contains("myvault") {
   191  		backendResults = []params.SecretBackendResult{{
   192  			Result: params.SecretBackend{
   193  				Name:                "myvault",
   194  				BackendType:         "vault",
   195  				TokenRotateInterval: ptr(666 * time.Minute),
   196  				Config:              resultVaultCfg,
   197  			},
   198  			ID:         "backend-id",
   199  			NumSecrets: 3,
   200  			Status:     "error",
   201  			Message:    "ping error",
   202  		}}
   203  	}
   204  	if modelType == state.ModelTypeCAAS && (wanted.IsEmpty() || wanted.Contains("fred-local")) {
   205  		backendResults = append(backendResults, params.SecretBackendResult{
   206  			Result: params.SecretBackend{
   207  				Name:        "fred-local",
   208  				BackendType: "kubernetes",
   209  				Config:      map[string]interface{}{},
   210  			},
   211  			ID:         coretesting.ModelTag.Id(),
   212  			Status:     "active",
   213  			NumSecrets: 2,
   214  		})
   215  	}
   216  	if wanted.IsEmpty() || wanted.Contains("internal") {
   217  		backendResults = append(backendResults, params.SecretBackendResult{
   218  			Result: params.SecretBackend{
   219  				Name:        "internal",
   220  				BackendType: "controller",
   221  				Config:      map[string]interface{}{},
   222  			},
   223  			ID:         coretesting.ControllerTag.Id(),
   224  			Status:     "active",
   225  			NumSecrets: 1,
   226  		})
   227  	}
   228  	c.Assert(results, jc.DeepEquals, params.ListSecretBackendsResults{
   229  		Results: backendResults,
   230  	})
   231  }
   232  
   233  func (s *SecretsSuite) TestListSecretBackendsPermissionDeniedReveal(c *gc.C) {
   234  	defer s.setup(c).Finish()
   235  
   236  	s.expectAuthClient()
   237  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(
   238  		errors.WithType(apiservererrors.ErrPerm, authentication.ErrorEntityMissingPermission))
   239  
   240  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   241  	c.Assert(err, jc.ErrorIsNil)
   242  
   243  	_, err = facade.ListSecretBackends(params.ListSecretBackendsArgs{Reveal: true})
   244  	c.Assert(err, gc.ErrorMatches, "permission denied")
   245  }
   246  
   247  func (s *SecretsSuite) TestAddSecretBackends(c *gc.C) {
   248  	ctrl := s.setup(c)
   249  	defer ctrl.Finish()
   250  
   251  	s.expectAuthClient()
   252  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(nil)
   253  
   254  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   255  	c.Assert(err, jc.ErrorIsNil)
   256  
   257  	p := mocks.NewMockSecretBackendProvider(ctrl)
   258  	p.EXPECT().Type().Return("vault").Times(2)
   259  	b := mocks.NewMockSecretsBackend(ctrl)
   260  	b.EXPECT().Ping().Return(nil).Times(2)
   261  	s.PatchValue(&commonsecrets.GetProvider, func(pType string) (provider.SecretBackendProvider, error) {
   262  		if pType != "vault" {
   263  			return provider.Provider(pType)
   264  		}
   265  		return providerWithConfig{
   266  			SecretBackendProvider: p,
   267  		}, nil
   268  	})
   269  
   270  	addedConfig := map[string]interface{}{
   271  		"endpoint":  "http://vault",
   272  		"namespace": "foo",
   273  	}
   274  	p.EXPECT().NewBackend(&provider.ModelBackendConfig{
   275  		BackendConfig: provider.BackendConfig{BackendType: "vault", Config: addedConfig},
   276  	}).Return(b, nil).Times(2)
   277  
   278  	s.backendState.EXPECT().CreateSecretBackend(state.CreateSecretBackendParams{
   279  		Name:                "myvault",
   280  		BackendType:         "vault",
   281  		TokenRotateInterval: ptr(200 * time.Minute),
   282  		NextRotateTime:      ptr(s.clock.Now().Add(150 * time.Minute)),
   283  		Config:              addedConfig,
   284  	}).Return("backend-id", nil)
   285  	s.backendState.EXPECT().CreateSecretBackend(state.CreateSecretBackendParams{
   286  		ID:          "existing-id",
   287  		Name:        "myvault2",
   288  		BackendType: "vault",
   289  		Config:      addedConfig,
   290  	}).Return("", errors.AlreadyExistsf(""))
   291  
   292  	results, err := facade.AddSecretBackends(params.AddSecretBackendArgs{
   293  		Args: []params.AddSecretBackendArg{{
   294  			SecretBackend: params.SecretBackend{
   295  				Name:                "myvault",
   296  				BackendType:         "vault",
   297  				TokenRotateInterval: ptr(200 * time.Minute),
   298  				Config:              map[string]interface{}{"endpoint": "http://vault"},
   299  			},
   300  		}, {
   301  			SecretBackend: params.SecretBackend{
   302  				Name:        "invalid",
   303  				BackendType: "something",
   304  			},
   305  		}, {
   306  			ID: "existing-id",
   307  			SecretBackend: params.SecretBackend{
   308  				Name:        "myvault2",
   309  				BackendType: "vault",
   310  				Config:      map[string]interface{}{"endpoint": "http://vault"},
   311  			},
   312  		}},
   313  	})
   314  	c.Assert(err, jc.ErrorIsNil)
   315  	c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{
   316  		{},
   317  		{Error: &params.Error{
   318  			Code:    "not found",
   319  			Message: `creating backend provider type "something": no registered provider for "something"`}},
   320  		{Error: &params.Error{
   321  			Code:    "already exists",
   322  			Message: `secret backend with ID "existing-id" already exists`}},
   323  	})
   324  }
   325  
   326  func (s *SecretsSuite) TestAddSecretBackendsPermissionDenied(c *gc.C) {
   327  	defer s.setup(c).Finish()
   328  
   329  	s.expectAuthClient()
   330  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(
   331  		errors.WithType(apiservererrors.ErrPerm, authentication.ErrorEntityMissingPermission))
   332  
   333  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   334  	c.Assert(err, jc.ErrorIsNil)
   335  
   336  	_, err = facade.AddSecretBackends(params.AddSecretBackendArgs{})
   337  	c.Assert(err, gc.ErrorMatches, "permission denied")
   338  }
   339  
   340  func (s *SecretsSuite) TestRemoveSecretBackends(c *gc.C) {
   341  	defer s.setup(c).Finish()
   342  
   343  	s.expectAuthClient()
   344  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(nil)
   345  
   346  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   347  	c.Assert(err, jc.ErrorIsNil)
   348  
   349  	s.backendState.EXPECT().DeleteSecretBackend("myvault", true).Return(nil)
   350  	s.backendState.EXPECT().DeleteSecretBackend("myvault2", false).Return(errors.NotSupportedf("remove with revisions"))
   351  
   352  	results, err := facade.RemoveSecretBackends(params.RemoveSecretBackendArgs{
   353  		Args: []params.RemoveSecretBackendArg{{
   354  			Name:  "myvault",
   355  			Force: true,
   356  		}, {
   357  			Name: "myvault2",
   358  		}},
   359  	})
   360  	c.Assert(err, jc.ErrorIsNil)
   361  	c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{
   362  		{},
   363  		{Error: &params.Error{
   364  			Code:    "not supported",
   365  			Message: `remove with revisions not supported`}},
   366  	})
   367  }
   368  
   369  func (s *SecretsSuite) TestRemoveSecretBackendsPermissionDenied(c *gc.C) {
   370  	defer s.setup(c).Finish()
   371  
   372  	s.expectAuthClient()
   373  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(
   374  		errors.WithType(apiservererrors.ErrPerm, authentication.ErrorEntityMissingPermission))
   375  
   376  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   377  	c.Assert(err, jc.ErrorIsNil)
   378  
   379  	_, err = facade.RemoveSecretBackends(params.RemoveSecretBackendArgs{})
   380  	c.Assert(err, gc.ErrorMatches, "permission denied")
   381  }
   382  
   383  func (s *SecretsSuite) TestUpdateSecretBackends(c *gc.C) {
   384  	ctrl := s.setup(c)
   385  	defer ctrl.Finish()
   386  
   387  	s.expectAuthClient()
   388  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(nil)
   389  
   390  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   391  	c.Assert(err, jc.ErrorIsNil)
   392  
   393  	p := mocks.NewMockSecretBackendProvider(ctrl)
   394  	p.EXPECT().Type().Return("vault")
   395  	b := mocks.NewMockSecretsBackend(ctrl)
   396  	b.EXPECT().Ping().Return(nil)
   397  	s.PatchValue(&commonsecrets.GetProvider, func(string) (provider.SecretBackendProvider, error) {
   398  		return providerWithConfig{
   399  			SecretBackendProvider: p,
   400  		}, nil
   401  	})
   402  
   403  	updatedConfig := map[string]interface{}{
   404  		"endpoint":        "http://vault",
   405  		"namespace":       "foo",
   406  		"tls-server-name": "server-name",
   407  	}
   408  	p.EXPECT().NewBackend(&provider.ModelBackendConfig{
   409  		BackendConfig: provider.BackendConfig{BackendType: "vault", Config: updatedConfig},
   410  	}).Return(b, nil)
   411  
   412  	s.backendState.EXPECT().GetSecretBackend("myvault").Return(&secrets.SecretBackend{
   413  		ID:          "backend-id",
   414  		BackendType: "vault",
   415  		Config:      map[string]interface{}{"endpoint": "http://vault"},
   416  	}, nil)
   417  	s.backendState.EXPECT().GetSecretBackend("invalid").Return(nil, errors.NotFoundf("backend"))
   418  
   419  	s.backendState.EXPECT().UpdateSecretBackend(state.UpdateSecretBackendParams{
   420  		ID:                  "backend-id",
   421  		NameChange:          ptr("new-name"),
   422  		TokenRotateInterval: ptr(200 * time.Minute),
   423  		NextRotateTime:      ptr(s.clock.Now().Add(150 * time.Minute)),
   424  		Config:              updatedConfig,
   425  	}).Return(nil)
   426  
   427  	results, err := facade.UpdateSecretBackends(params.UpdateSecretBackendArgs{
   428  		Args: []params.UpdateSecretBackendArg{{
   429  			Name:                "myvault",
   430  			NameChange:          ptr("new-name"),
   431  			TokenRotateInterval: ptr(200 * time.Minute),
   432  			Config:              map[string]interface{}{"tls-server-name": "server-name"},
   433  			Reset:               []string{"namespace"},
   434  		}, {
   435  			Name: "invalid",
   436  		}},
   437  	})
   438  	c.Assert(err, jc.ErrorIsNil)
   439  	c.Assert(results.Results, jc.DeepEquals, []params.ErrorResult{
   440  		{},
   441  		{Error: &params.Error{
   442  			Code:    "not found",
   443  			Message: `backend not found`}},
   444  	})
   445  }
   446  
   447  func (s *SecretsSuite) TestUpdateSecretBackendsPermissionDenied(c *gc.C) {
   448  	defer s.setup(c).Finish()
   449  
   450  	s.expectAuthClient()
   451  	s.authorizer.EXPECT().HasPermission(permission.SuperuserAccess, coretesting.ControllerTag).Return(
   452  		errors.WithType(apiservererrors.ErrPerm, authentication.ErrorEntityMissingPermission))
   453  
   454  	facade, err := secretbackends.NewTestAPI(s.backendState, s.secretsState, s.statePool, s.authorizer, s.clock)
   455  	c.Assert(err, jc.ErrorIsNil)
   456  
   457  	_, err = facade.UpdateSecretBackends(params.UpdateSecretBackendArgs{})
   458  	c.Assert(err, gc.ErrorMatches, "permission denied")
   459  }