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: ¶ms.Error{ 318 Code: "not found", 319 Message: `creating backend provider type "something": no registered provider for "something"`}}, 320 {Error: ¶ms.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: ¶ms.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: ¶ms.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 }