client_test.go 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191
  1. /*
  2. Copyright © The ESO Authors
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package onepasswordsdk
  14. import (
  15. "context"
  16. "errors"
  17. "fmt"
  18. "testing"
  19. "time"
  20. "github.com/1password/onepassword-sdk-go"
  21. "github.com/hashicorp/golang-lru/v2/expirable"
  22. "github.com/stretchr/testify/assert"
  23. "github.com/stretchr/testify/require"
  24. corev1 "k8s.io/api/core/v1"
  25. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  26. v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  28. )
  29. func TestProviderGetSecret(t *testing.T) {
  30. tests := []struct {
  31. name string
  32. ref v1.ExternalSecretDataRemoteRef
  33. want []byte
  34. assertError func(t *testing.T, err error)
  35. client func() *onepassword.Client
  36. }{
  37. {
  38. name: "get secret successfully",
  39. client: func() *onepassword.Client {
  40. fc := &fakeClient{
  41. resolveResult: "secret",
  42. }
  43. return &onepassword.Client{
  44. SecretsAPI: fc,
  45. VaultsAPI: fc,
  46. }
  47. },
  48. assertError: func(t *testing.T, err error) {
  49. require.NoError(t, err)
  50. },
  51. ref: v1.ExternalSecretDataRemoteRef{
  52. Key: "secret",
  53. },
  54. want: []byte("secret"),
  55. },
  56. {
  57. name: "get secret with error",
  58. client: func() *onepassword.Client {
  59. fc := &fakeClient{
  60. resolveError: errors.New("fobar"),
  61. }
  62. return &onepassword.Client{
  63. SecretsAPI: fc,
  64. VaultsAPI: fc,
  65. }
  66. },
  67. assertError: func(t *testing.T, err error) {
  68. require.ErrorContains(t, err, "fobar")
  69. },
  70. ref: v1.ExternalSecretDataRemoteRef{
  71. Key: "secret",
  72. },
  73. },
  74. {
  75. name: "get secret version not implemented",
  76. client: func() *onepassword.Client {
  77. fc := &fakeClient{
  78. resolveResult: "secret",
  79. }
  80. return &onepassword.Client{
  81. SecretsAPI: fc,
  82. VaultsAPI: fc,
  83. }
  84. },
  85. ref: v1.ExternalSecretDataRemoteRef{
  86. Key: "secret",
  87. Version: "1",
  88. },
  89. assertError: func(t *testing.T, err error) {
  90. require.ErrorContains(t, err, "is not implemented in the 1Password SDK provider")
  91. },
  92. },
  93. }
  94. for _, tt := range tests {
  95. t.Run(tt.name, func(t *testing.T) {
  96. p := &SecretsClient{
  97. client: tt.client(),
  98. vaultPrefix: "op://vault/",
  99. }
  100. got, err := p.GetSecret(t.Context(), tt.ref)
  101. tt.assertError(t, err)
  102. require.Equal(t, string(got), string(tt.want))
  103. })
  104. }
  105. }
  106. func TestProviderGetSecretMap(t *testing.T) {
  107. tests := []struct {
  108. name string
  109. ref v1.ExternalSecretDataRemoteRef
  110. want map[string][]byte
  111. assertError func(t *testing.T, err error)
  112. client func() *onepassword.Client
  113. }{
  114. {
  115. name: "get secret successfully for files",
  116. client: func() *onepassword.Client {
  117. fc := &fakeClient{}
  118. fl := &fakeLister{
  119. listAllResult: []onepassword.ItemOverview{
  120. {
  121. ID: "test-item-id",
  122. Title: "key",
  123. Category: "login",
  124. VaultID: "vault-id",
  125. },
  126. },
  127. getResult: onepassword.Item{
  128. ID: "test-item-id",
  129. Title: "key",
  130. Category: "login",
  131. VaultID: "vault-id",
  132. Files: []onepassword.ItemFile{
  133. {
  134. Attributes: onepassword.FileAttributes{
  135. Name: "name",
  136. ID: "id",
  137. },
  138. FieldID: "field-id",
  139. },
  140. },
  141. },
  142. fileLister: &fakeFileLister{
  143. readContent: []byte("content"),
  144. },
  145. }
  146. return &onepassword.Client{
  147. SecretsAPI: fc,
  148. ItemsAPI: fl,
  149. VaultsAPI: fc,
  150. }
  151. },
  152. assertError: func(t *testing.T, err error) {
  153. require.NoError(t, err)
  154. },
  155. ref: v1.ExternalSecretDataRemoteRef{
  156. Key: "key",
  157. Property: "file/name",
  158. },
  159. want: map[string][]byte{
  160. "name": []byte("content"),
  161. },
  162. },
  163. {
  164. name: "get secret successfully for fields",
  165. client: func() *onepassword.Client {
  166. fc := &fakeClient{}
  167. fl := &fakeLister{
  168. listAllResult: []onepassword.ItemOverview{
  169. {
  170. ID: "test-item-id",
  171. Title: "key",
  172. Category: "login",
  173. VaultID: "vault-id",
  174. },
  175. },
  176. getResult: onepassword.Item{
  177. ID: "test-item-id",
  178. Title: "key",
  179. Category: "login",
  180. VaultID: "vault-id",
  181. Fields: []onepassword.ItemField{
  182. {
  183. ID: "field-id",
  184. Title: "name",
  185. FieldType: onepassword.ItemFieldTypeConcealed,
  186. Value: "value",
  187. },
  188. },
  189. },
  190. fileLister: &fakeFileLister{
  191. readContent: []byte("content"),
  192. },
  193. }
  194. return &onepassword.Client{
  195. SecretsAPI: fc,
  196. ItemsAPI: fl,
  197. VaultsAPI: fc,
  198. }
  199. },
  200. assertError: func(t *testing.T, err error) {
  201. require.NoError(t, err)
  202. },
  203. ref: v1.ExternalSecretDataRemoteRef{
  204. Key: "key",
  205. Property: "field/name",
  206. },
  207. want: map[string][]byte{
  208. "name": []byte("value"),
  209. },
  210. },
  211. {
  212. name: "get secret fails with fields with same title",
  213. client: func() *onepassword.Client {
  214. fc := &fakeClient{}
  215. fl := &fakeLister{
  216. listAllResult: []onepassword.ItemOverview{
  217. {
  218. ID: "test-item-id",
  219. Title: "key",
  220. Category: "login",
  221. VaultID: "vault-id",
  222. },
  223. },
  224. getResult: onepassword.Item{
  225. ID: "test-item-id",
  226. Title: "key",
  227. Category: "login",
  228. VaultID: "vault-id",
  229. Fields: []onepassword.ItemField{
  230. {
  231. ID: "field-id",
  232. Title: "name",
  233. FieldType: onepassword.ItemFieldTypeConcealed,
  234. Value: "value",
  235. },
  236. {
  237. ID: "field-id",
  238. Title: "name",
  239. FieldType: onepassword.ItemFieldTypeConcealed,
  240. Value: "value",
  241. },
  242. },
  243. },
  244. fileLister: &fakeFileLister{
  245. readContent: []byte("content"),
  246. },
  247. }
  248. return &onepassword.Client{
  249. SecretsAPI: fc,
  250. ItemsAPI: fl,
  251. VaultsAPI: fc,
  252. }
  253. },
  254. assertError: func(t *testing.T, err error) {
  255. require.ErrorContains(t, err, "found more than 1 fields with title 'name' in 'key', got 2")
  256. },
  257. ref: v1.ExternalSecretDataRemoteRef{
  258. Key: "key",
  259. Property: "field/name",
  260. },
  261. },
  262. }
  263. for _, tt := range tests {
  264. t.Run(tt.name, func(t *testing.T) {
  265. p := &SecretsClient{
  266. client: tt.client(),
  267. vaultPrefix: "op://vault/",
  268. }
  269. got, err := p.GetSecretMap(t.Context(), tt.ref)
  270. tt.assertError(t, err)
  271. require.Equal(t, tt.want, got)
  272. })
  273. }
  274. }
  275. func TestProviderValidate(t *testing.T) {
  276. tests := []struct {
  277. name string
  278. want v1.ValidationResult
  279. assertError func(t *testing.T, err error)
  280. client func() *onepassword.Client
  281. vaultPrefix string
  282. }{
  283. {
  284. name: "validate successfully",
  285. client: func() *onepassword.Client {
  286. fc := &fakeClient{
  287. listAllResult: []onepassword.VaultOverview{
  288. {
  289. ID: "test",
  290. Title: "test",
  291. },
  292. },
  293. }
  294. return &onepassword.Client{
  295. SecretsAPI: fc,
  296. VaultsAPI: fc,
  297. }
  298. },
  299. want: v1.ValidationResultReady,
  300. assertError: func(t *testing.T, err error) {
  301. require.NoError(t, err)
  302. },
  303. vaultPrefix: "op://vault/",
  304. },
  305. }
  306. for _, tt := range tests {
  307. t.Run(tt.name, func(t *testing.T) {
  308. p := &SecretsClient{
  309. client: tt.client(),
  310. vaultPrefix: tt.vaultPrefix,
  311. }
  312. got, err := p.Validate()
  313. tt.assertError(t, err)
  314. require.Equal(t, got, tt.want)
  315. })
  316. }
  317. }
  318. func TestPushSecret(t *testing.T) {
  319. fc := &fakeClient{
  320. listAllResult: []onepassword.VaultOverview{
  321. {
  322. ID: "test",
  323. Title: "test",
  324. },
  325. },
  326. }
  327. tests := []struct {
  328. name string
  329. ref v1alpha1.PushSecretData
  330. secret *corev1.Secret
  331. assertError func(t *testing.T, err error)
  332. lister func() *fakeLister
  333. assertLister func(t *testing.T, lister *fakeLister)
  334. }{
  335. {
  336. name: "create is called",
  337. lister: func() *fakeLister {
  338. return &fakeLister{
  339. listAllResult: []onepassword.ItemOverview{},
  340. }
  341. },
  342. secret: &corev1.Secret{
  343. Data: map[string][]byte{
  344. "foo": []byte("bar"),
  345. },
  346. ObjectMeta: metav1.ObjectMeta{
  347. Name: "secret",
  348. Namespace: "default",
  349. },
  350. },
  351. ref: v1alpha1.PushSecretData{
  352. Match: v1alpha1.PushSecretMatch{
  353. SecretKey: "foo",
  354. RemoteRef: v1alpha1.PushSecretRemoteRef{
  355. RemoteKey: "key",
  356. },
  357. },
  358. },
  359. assertError: func(t *testing.T, err error) {
  360. require.NoError(t, err)
  361. },
  362. assertLister: func(t *testing.T, lister *fakeLister) {
  363. assert.True(t, lister.createCalled)
  364. },
  365. },
  366. {
  367. name: "update is called",
  368. lister: func() *fakeLister {
  369. return &fakeLister{
  370. listAllResult: []onepassword.ItemOverview{
  371. {
  372. ID: "test-item-id",
  373. Title: "key",
  374. Category: "login",
  375. VaultID: "vault-id",
  376. },
  377. },
  378. }
  379. },
  380. secret: &corev1.Secret{
  381. Data: map[string][]byte{
  382. "foo": []byte("bar"),
  383. },
  384. ObjectMeta: metav1.ObjectMeta{
  385. Name: "secret",
  386. Namespace: "default",
  387. },
  388. },
  389. ref: v1alpha1.PushSecretData{
  390. Match: v1alpha1.PushSecretMatch{
  391. SecretKey: "foo",
  392. RemoteRef: v1alpha1.PushSecretRemoteRef{
  393. RemoteKey: "key",
  394. },
  395. },
  396. },
  397. assertError: func(t *testing.T, err error) {
  398. require.NoError(t, err)
  399. },
  400. assertLister: func(t *testing.T, lister *fakeLister) {
  401. assert.True(t, lister.putCalled)
  402. },
  403. },
  404. }
  405. for _, tt := range tests {
  406. t.Run(tt.name, func(t *testing.T) {
  407. ctx := t.Context()
  408. lister := tt.lister()
  409. p := &SecretsClient{
  410. client: &onepassword.Client{
  411. SecretsAPI: fc,
  412. VaultsAPI: fc,
  413. ItemsAPI: lister,
  414. },
  415. }
  416. err := p.PushSecret(ctx, tt.secret, tt.ref)
  417. tt.assertError(t, err)
  418. tt.assertLister(t, lister)
  419. })
  420. }
  421. }
  422. func TestDeleteItemField(t *testing.T) {
  423. fc := &fakeClient{
  424. listAllResult: []onepassword.VaultOverview{
  425. {
  426. ID: "test",
  427. Title: "test",
  428. },
  429. },
  430. }
  431. testCases := []struct {
  432. name string
  433. lister func() *fakeLister
  434. ref *v1alpha1.PushSecretRemoteRef
  435. assertError func(t *testing.T, err error)
  436. assertLister func(t *testing.T, lister *fakeLister)
  437. }{
  438. {
  439. name: "update is called",
  440. ref: &v1alpha1.PushSecretRemoteRef{
  441. RemoteKey: "key",
  442. Property: "password",
  443. },
  444. assertLister: func(t *testing.T, lister *fakeLister) {
  445. require.True(t, lister.putCalled)
  446. },
  447. lister: func() *fakeLister {
  448. fl := &fakeLister{
  449. listAllResult: []onepassword.ItemOverview{
  450. {
  451. ID: "test-item-id",
  452. Title: "key",
  453. Category: "login",
  454. VaultID: "vault-id",
  455. },
  456. },
  457. getResult: onepassword.Item{
  458. ID: "test-item-id",
  459. Title: "key",
  460. Category: "login",
  461. VaultID: "vault-id",
  462. Fields: []onepassword.ItemField{
  463. {
  464. ID: "field-1",
  465. Title: "password",
  466. FieldType: onepassword.ItemFieldTypeConcealed,
  467. Value: "password",
  468. },
  469. {
  470. ID: "field-2",
  471. Title: "other-field",
  472. FieldType: onepassword.ItemFieldTypeConcealed,
  473. Value: "username",
  474. },
  475. },
  476. },
  477. }
  478. return fl
  479. },
  480. assertError: func(t *testing.T, err error) {
  481. require.NoError(t, err)
  482. },
  483. },
  484. {
  485. name: "delete is called",
  486. ref: &v1alpha1.PushSecretRemoteRef{
  487. RemoteKey: "key",
  488. Property: "password",
  489. },
  490. assertLister: func(t *testing.T, lister *fakeLister) {
  491. require.True(t, lister.deleteCalled, "delete should have been called as the item should have existed")
  492. },
  493. lister: func() *fakeLister {
  494. fl := &fakeLister{
  495. listAllResult: []onepassword.ItemOverview{
  496. {
  497. ID: "test-item-id",
  498. Title: "key",
  499. Category: "login",
  500. VaultID: "vault-id",
  501. },
  502. },
  503. getResult: onepassword.Item{
  504. ID: "test-item-id",
  505. Title: "key",
  506. Category: "login",
  507. VaultID: "vault-id",
  508. Fields: []onepassword.ItemField{
  509. {
  510. ID: "field-1",
  511. Title: "password",
  512. FieldType: onepassword.ItemFieldTypeConcealed,
  513. Value: "password",
  514. },
  515. },
  516. },
  517. }
  518. return fl
  519. },
  520. assertError: func(t *testing.T, err error) {
  521. require.NoError(t, err)
  522. },
  523. },
  524. }
  525. for _, testCase := range testCases {
  526. t.Run(testCase.name, func(t *testing.T) {
  527. ctx := t.Context()
  528. lister := testCase.lister()
  529. p := &SecretsClient{
  530. client: &onepassword.Client{
  531. SecretsAPI: fc,
  532. VaultsAPI: fc,
  533. ItemsAPI: lister,
  534. },
  535. }
  536. testCase.assertError(t, p.DeleteSecret(ctx, testCase.ref))
  537. testCase.assertLister(t, lister)
  538. })
  539. }
  540. }
  541. func TestGetVault(t *testing.T) {
  542. fc := &fakeClient{
  543. listAllResult: []onepassword.VaultOverview{
  544. {
  545. ID: "vault-id",
  546. Title: "vault-title",
  547. },
  548. },
  549. }
  550. p := &SecretsClient{
  551. client: &onepassword.Client{
  552. VaultsAPI: fc,
  553. },
  554. }
  555. titleOrUuids := []string{"vault-title", "vault-id"}
  556. for _, titleOrUuid := range titleOrUuids {
  557. t.Run(titleOrUuid, func(t *testing.T) {
  558. vaultID, err := p.GetVault(t.Context(), titleOrUuid)
  559. require.NoError(t, err)
  560. require.Equal(t, fc.listAllResult[0].ID, vaultID)
  561. })
  562. }
  563. }
  564. type fakeLister struct {
  565. listAllResult []onepassword.ItemOverview
  566. createCalled bool
  567. putCalled bool
  568. deleteCalled bool
  569. getResult onepassword.Item
  570. fileLister onepassword.ItemsFilesAPI
  571. }
  572. func (f *fakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  573. f.createCalled = true
  574. return onepassword.Item{}, nil
  575. }
  576. func (f *fakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  577. return f.getResult, nil
  578. }
  579. func (f *fakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  580. f.putCalled = true
  581. return onepassword.Item{}, nil
  582. }
  583. func (f *fakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  584. f.deleteCalled = true
  585. return nil
  586. }
  587. func (f *fakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  588. return nil
  589. }
  590. func (f *fakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  591. return f.listAllResult, nil
  592. }
  593. func (f *fakeLister) Shares() onepassword.ItemsSharesAPI {
  594. return nil
  595. }
  596. func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
  597. return f.fileLister
  598. }
  599. type fakeFileLister struct {
  600. readContent []byte
  601. }
  602. func (f *fakeFileLister) Attach(ctx context.Context, item onepassword.Item, fileParams onepassword.FileCreateParams) (onepassword.Item, error) {
  603. return onepassword.Item{}, nil
  604. }
  605. func (f *fakeFileLister) Read(ctx context.Context, vaultID, itemID string, attr onepassword.FileAttributes) ([]byte, error) {
  606. return f.readContent, nil
  607. }
  608. func (f *fakeFileLister) Delete(ctx context.Context, item onepassword.Item, sectionID, fieldID string) (onepassword.Item, error) {
  609. return onepassword.Item{}, nil
  610. }
  611. func (f *fakeFileLister) ReplaceDocument(ctx context.Context, item onepassword.Item, docParams onepassword.DocumentCreateParams) (onepassword.Item, error) {
  612. return onepassword.Item{}, nil
  613. }
  614. var _ onepassword.ItemsFilesAPI = (*fakeFileLister)(nil)
  615. type statefulFakeLister struct {
  616. listAllResult []onepassword.ItemOverview
  617. items map[string]onepassword.Item
  618. deletedItems map[string]bool
  619. createCalled bool
  620. putCalled bool
  621. deleteCalled bool
  622. fileLister onepassword.ItemsFilesAPI
  623. }
  624. func (f *statefulFakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  625. f.createCalled = true
  626. return onepassword.Item{}, nil
  627. }
  628. func (f *statefulFakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  629. if f.deletedItems != nil && f.deletedItems[itemID] {
  630. return onepassword.Item{}, fmt.Errorf("item not found")
  631. }
  632. if item, ok := f.items[itemID]; ok {
  633. return item, nil
  634. }
  635. return onepassword.Item{}, fmt.Errorf("item not found")
  636. }
  637. func (f *statefulFakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  638. f.putCalled = true
  639. if f.items == nil {
  640. f.items = make(map[string]onepassword.Item)
  641. }
  642. f.items[item.ID] = item
  643. return item, nil
  644. }
  645. func (f *statefulFakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  646. f.deleteCalled = true
  647. if f.deletedItems == nil {
  648. f.deletedItems = make(map[string]bool)
  649. }
  650. f.deletedItems[itemID] = true
  651. delete(f.items, itemID)
  652. f.listAllResult = nil
  653. return nil
  654. }
  655. func (f *statefulFakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  656. return nil
  657. }
  658. func (f *statefulFakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  659. return f.listAllResult, nil
  660. }
  661. func (f *statefulFakeLister) Shares() onepassword.ItemsSharesAPI {
  662. return nil
  663. }
  664. func (f *statefulFakeLister) Files() onepassword.ItemsFilesAPI {
  665. return f.fileLister
  666. }
  667. var _ onepassword.ItemsAPI = (*statefulFakeLister)(nil)
  668. type fakeClient struct {
  669. resolveResult string
  670. resolveError error
  671. resolveAll onepassword.ResolveAllResponse
  672. resolveAllError error
  673. listAllResult []onepassword.VaultOverview
  674. listAllError error
  675. }
  676. func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
  677. return f.listAllResult, f.listAllError
  678. }
  679. func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
  680. return f.resolveResult, f.resolveError
  681. }
  682. func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string) (onepassword.ResolveAllResponse, error) {
  683. return f.resolveAll, f.resolveAllError
  684. }
  685. func TestDeleteMultipleFieldsFromSameItem(t *testing.T) {
  686. fc := &fakeClient{
  687. listAllResult: []onepassword.VaultOverview{
  688. {
  689. ID: "test",
  690. Title: "test",
  691. },
  692. },
  693. }
  694. t.Run("deleting second field after item was deleted should not error", func(t *testing.T) {
  695. fl := &statefulFakeLister{
  696. listAllResult: []onepassword.ItemOverview{
  697. {
  698. ID: "test-item-id",
  699. Title: "key",
  700. Category: "login",
  701. VaultID: "vault-id",
  702. },
  703. },
  704. items: map[string]onepassword.Item{
  705. "test-item-id": {
  706. ID: "test-item-id",
  707. Title: "key",
  708. Category: "login",
  709. VaultID: "vault-id",
  710. Fields: []onepassword.ItemField{
  711. {
  712. ID: "field-1",
  713. Title: "username",
  714. FieldType: onepassword.ItemFieldTypeConcealed,
  715. Value: "testuser",
  716. },
  717. {
  718. ID: "field-2",
  719. Title: "password",
  720. FieldType: onepassword.ItemFieldTypeConcealed,
  721. Value: "testpass",
  722. },
  723. },
  724. },
  725. },
  726. }
  727. p := &SecretsClient{
  728. client: &onepassword.Client{
  729. SecretsAPI: fc,
  730. VaultsAPI: fc,
  731. ItemsAPI: fl,
  732. },
  733. }
  734. ctx := t.Context()
  735. err := p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  736. RemoteKey: "key",
  737. Property: "username",
  738. })
  739. require.NoError(t, err, "first field deletion should succeed")
  740. assert.True(t, fl.putCalled, "Put should have been called to update the item")
  741. assert.False(t, fl.deleteCalled, "Delete should not have been called yet")
  742. fl.putCalled = false
  743. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  744. RemoteKey: "key",
  745. Property: "password",
  746. })
  747. require.NoError(t, err, "second field deletion should succeed")
  748. assert.True(t, fl.deleteCalled, "Delete should have been called to remove the item")
  749. fl.listAllResult = nil
  750. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  751. RemoteKey: "key",
  752. Property: "some-other-field",
  753. })
  754. require.NoError(t, err, "deleting a field from an already-deleted item should not error (this is the bug!)")
  755. })
  756. }
  757. func TestCachingGetSecret(t *testing.T) {
  758. t.Run("cache hit returns cached value", func(t *testing.T) {
  759. fcWithCounter := &fakeClientWithCounter{
  760. fakeClient: &fakeClient{
  761. resolveResult: "secret-value",
  762. },
  763. }
  764. p := &SecretsClient{
  765. client: &onepassword.Client{
  766. SecretsAPI: fcWithCounter,
  767. VaultsAPI: fcWithCounter.fakeClient,
  768. },
  769. vaultPrefix: "op://vault/",
  770. }
  771. // Initialize cache
  772. p.cache = expirable.NewLRU[string, []byte](100, nil, time.Minute)
  773. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  774. // First call - cache miss
  775. val1, err := p.GetSecret(t.Context(), ref)
  776. require.NoError(t, err)
  777. assert.Equal(t, []byte("secret-value"), val1)
  778. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  779. // Second call - cache hit, should not call API
  780. val2, err := p.GetSecret(t.Context(), ref)
  781. require.NoError(t, err)
  782. assert.Equal(t, []byte("secret-value"), val2)
  783. assert.Equal(t, 1, fcWithCounter.resolveCallCount, "API should not be called on cache hit")
  784. })
  785. t.Run("cache disabled works normally", func(t *testing.T) {
  786. fcWithCounter := &fakeClientWithCounter{
  787. fakeClient: &fakeClient{
  788. resolveResult: "secret-value",
  789. },
  790. }
  791. p := &SecretsClient{
  792. client: &onepassword.Client{
  793. SecretsAPI: fcWithCounter,
  794. VaultsAPI: fcWithCounter.fakeClient,
  795. },
  796. vaultPrefix: "op://vault/",
  797. cache: nil, // Cache disabled
  798. }
  799. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  800. // Multiple calls should always hit API
  801. _, err := p.GetSecret(t.Context(), ref)
  802. require.NoError(t, err)
  803. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  804. _, err = p.GetSecret(t.Context(), ref)
  805. require.NoError(t, err)
  806. assert.Equal(t, 2, fcWithCounter.resolveCallCount)
  807. })
  808. }
  809. func TestCachingGetSecretMap(t *testing.T) {
  810. t.Run("cache hit returns cached map", func(t *testing.T) {
  811. fc := &fakeClient{}
  812. flWithCounter := &fakeListerWithCounter{
  813. fakeLister: &fakeLister{
  814. listAllResult: []onepassword.ItemOverview{
  815. {
  816. ID: "item-id",
  817. Title: "item",
  818. Category: "login",
  819. VaultID: "vault-id",
  820. },
  821. },
  822. getResult: onepassword.Item{
  823. ID: "item-id",
  824. Title: "item",
  825. Category: "login",
  826. VaultID: "vault-id",
  827. Fields: []onepassword.ItemField{
  828. {Title: "username", Value: "user1"},
  829. {Title: "password", Value: "pass1"},
  830. },
  831. },
  832. },
  833. }
  834. p := &SecretsClient{
  835. client: &onepassword.Client{
  836. SecretsAPI: fc,
  837. VaultsAPI: fc,
  838. ItemsAPI: flWithCounter,
  839. },
  840. vaultPrefix: "op://vault/",
  841. vaultID: "vault-id",
  842. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  843. }
  844. ref := v1.ExternalSecretDataRemoteRef{Key: "item"}
  845. // First call - cache miss
  846. val1, err := p.GetSecretMap(t.Context(), ref)
  847. require.NoError(t, err)
  848. assert.Equal(t, map[string][]byte{
  849. "username": []byte("user1"),
  850. "password": []byte("pass1"),
  851. }, val1)
  852. assert.Equal(t, 1, flWithCounter.getCallCount)
  853. // Second call - cache hit
  854. val2, err := p.GetSecretMap(t.Context(), ref)
  855. require.NoError(t, err)
  856. assert.Equal(t, val1, val2)
  857. assert.Equal(t, 1, flWithCounter.getCallCount, "API should not be called on cache hit")
  858. })
  859. }
  860. func TestCacheInvalidationPushSecret(t *testing.T) {
  861. t.Run("push secret invalidates cache", func(t *testing.T) {
  862. fcWithCounter := &fakeClientWithCounter{
  863. fakeClient: &fakeClient{
  864. resolveResult: "secret-value",
  865. },
  866. }
  867. fl := &fakeLister{
  868. listAllResult: []onepassword.ItemOverview{
  869. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  870. },
  871. getResult: onepassword.Item{
  872. ID: "item-id",
  873. Title: "item",
  874. VaultID: "vault-id",
  875. Fields: []onepassword.ItemField{{Title: "password", Value: "old"}},
  876. },
  877. }
  878. p := &SecretsClient{
  879. client: &onepassword.Client{
  880. SecretsAPI: fcWithCounter,
  881. VaultsAPI: fcWithCounter.fakeClient,
  882. ItemsAPI: fl,
  883. },
  884. vaultPrefix: "op://vault/",
  885. vaultID: "vault-id",
  886. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  887. }
  888. ref := v1.ExternalSecretDataRemoteRef{Key: "item/password"}
  889. // Populate cache
  890. val1, err := p.GetSecret(t.Context(), ref)
  891. require.NoError(t, err)
  892. assert.Equal(t, []byte("secret-value"), val1)
  893. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  894. // Push new value (should invalidate cache)
  895. pushRef := v1alpha1.PushSecretData{
  896. Match: v1alpha1.PushSecretMatch{
  897. SecretKey: "key",
  898. RemoteRef: v1alpha1.PushSecretRemoteRef{
  899. RemoteKey: "item",
  900. Property: "password",
  901. },
  902. },
  903. }
  904. secret := &corev1.Secret{
  905. Data: map[string][]byte{"key": []byte("new-value")},
  906. }
  907. err = p.PushSecret(t.Context(), secret, pushRef)
  908. require.NoError(t, err)
  909. // Next GetSecret should fetch fresh value (cache was invalidated)
  910. val2, err := p.GetSecret(t.Context(), ref)
  911. require.NoError(t, err)
  912. assert.Equal(t, []byte("secret-value"), val2)
  913. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  914. })
  915. }
  916. func TestCacheInvalidationDeleteSecret(t *testing.T) {
  917. t.Run("delete secret invalidates cache", func(t *testing.T) {
  918. fcWithCounter := &fakeClientWithCounter{
  919. fakeClient: &fakeClient{
  920. resolveResult: "cached-value",
  921. },
  922. }
  923. fl := &fakeLister{
  924. listAllResult: []onepassword.ItemOverview{
  925. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  926. },
  927. getResult: onepassword.Item{
  928. ID: "item-id",
  929. Title: "item",
  930. VaultID: "vault-id",
  931. Fields: []onepassword.ItemField{
  932. {Title: "field1", Value: "val1"},
  933. {Title: "field2", Value: "val2"},
  934. },
  935. },
  936. }
  937. p := &SecretsClient{
  938. client: &onepassword.Client{
  939. SecretsAPI: fcWithCounter,
  940. VaultsAPI: fcWithCounter.fakeClient,
  941. ItemsAPI: fl,
  942. },
  943. vaultPrefix: "op://vault/",
  944. vaultID: "vault-id",
  945. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  946. }
  947. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field1"}
  948. // Populate cache
  949. _, err := p.GetSecret(t.Context(), ref)
  950. require.NoError(t, err)
  951. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  952. // Delete field (should invalidate cache)
  953. deleteRef := v1alpha1.PushSecretRemoteRef{
  954. RemoteKey: "item",
  955. Property: "field1",
  956. }
  957. err = p.DeleteSecret(t.Context(), deleteRef)
  958. require.NoError(t, err)
  959. // Next GetSecret should miss cache
  960. _, err = p.GetSecret(t.Context(), ref)
  961. require.NoError(t, err)
  962. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  963. })
  964. }
  965. func TestInvalidateCacheByPrefix(t *testing.T) {
  966. t.Run("invalidates all entries with prefix", func(t *testing.T) {
  967. p := &SecretsClient{
  968. vaultPrefix: "op://vault/",
  969. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  970. }
  971. // Add multiple cache entries
  972. p.cache.Add("op://vault/item1/field1", []byte("val1"))
  973. p.cache.Add("op://vault/item1/field2", []byte("val2"))
  974. p.cache.Add("op://vault/item2/field1", []byte("val3"))
  975. // Invalidate item1 entries
  976. p.invalidateCacheByPrefix("op://vault/item1")
  977. // item1 entries should be gone
  978. _, ok1 := p.cache.Get("op://vault/item1/field1")
  979. assert.False(t, ok1)
  980. _, ok2 := p.cache.Get("op://vault/item1/field2")
  981. assert.False(t, ok2)
  982. // item2 entry should still exist
  983. val3, ok3 := p.cache.Get("op://vault/item2/field1")
  984. assert.True(t, ok3)
  985. assert.Equal(t, []byte("val3"), val3)
  986. })
  987. t.Run("handles nil cache gracefully", func(t *testing.T) {
  988. p := &SecretsClient{
  989. vaultPrefix: "op://vault/",
  990. cache: nil,
  991. }
  992. // Should not panic
  993. p.invalidateCacheByPrefix("op://vault/item1")
  994. })
  995. t.Run("does not invalidate entries with similar prefixes", func(t *testing.T) {
  996. p := &SecretsClient{
  997. vaultPrefix: "op://vault/",
  998. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  999. }
  1000. p.cache.Add("op://vault/item/field1", []byte("val1"))
  1001. p.cache.Add("op://vault/item/field2", []byte("val2"))
  1002. p.cache.Add("op://vault/item|property", []byte("val3"))
  1003. p.cache.Add("op://vault/item-backup/field1", []byte("val4"))
  1004. p.cache.Add("op://vault/prod-db/secret", []byte("val5"))
  1005. p.cache.Add("op://vault/prod-db-replica/secret", []byte("val6"))
  1006. p.cache.Add("op://vault/prod-db-replica/secret|property", []byte("val7"))
  1007. p.invalidateCacheByPrefix("op://vault/item")
  1008. _, ok1 := p.cache.Get("op://vault/item/field1")
  1009. assert.False(t, ok1)
  1010. _, ok2 := p.cache.Get("op://vault/item/field2")
  1011. assert.False(t, ok2)
  1012. _, ok3 := p.cache.Get("op://vault/item|property")
  1013. assert.False(t, ok3)
  1014. val4, ok4 := p.cache.Get("op://vault/item-backup/field1")
  1015. assert.True(t, ok4, "item-backup should not be invalidated")
  1016. assert.Equal(t, []byte("val4"), val4)
  1017. p.invalidateCacheByPrefix("op://vault/prod-db")
  1018. _, ok5 := p.cache.Get("op://vault/prod-db/secret")
  1019. assert.False(t, ok5)
  1020. val6, ok6 := p.cache.Get("op://vault/prod-db-replica/secret")
  1021. assert.True(t, ok6, "prod-db-replica/secret should not be invalidated")
  1022. assert.Equal(t, []byte("val6"), val6)
  1023. val7, ok7 := p.cache.Get("op://vault/prod-db-replica/secret|property")
  1024. assert.True(t, ok7, "prod-db-replica/secret|property should not be invalidated")
  1025. assert.Equal(t, []byte("val7"), val7)
  1026. })
  1027. }
  1028. // fakeClientWithCounter wraps fakeClient and tracks Resolve call count.
  1029. type fakeClientWithCounter struct {
  1030. *fakeClient
  1031. resolveCallCount int
  1032. }
  1033. func (f *fakeClientWithCounter) Resolve(ctx context.Context, secretReference string) (string, error) {
  1034. f.resolveCallCount++
  1035. return f.fakeClient.Resolve(ctx, secretReference)
  1036. }
  1037. // fakeListerWithCounter wraps fakeLister and tracks Get call count.
  1038. type fakeListerWithCounter struct {
  1039. *fakeLister
  1040. getCallCount int
  1041. }
  1042. func (f *fakeListerWithCounter) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  1043. f.getCallCount++
  1044. return f.fakeLister.Get(ctx, vaultID, itemID)
  1045. }
  1046. func (f *fakeListerWithCounter) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  1047. return f.fakeLister.Put(ctx, item)
  1048. }
  1049. func (f *fakeListerWithCounter) Delete(ctx context.Context, vaultID, itemID string) error {
  1050. return f.fakeLister.Delete(ctx, vaultID, itemID)
  1051. }
  1052. func (f *fakeListerWithCounter) Archive(ctx context.Context, vaultID, itemID string) error {
  1053. return f.fakeLister.Archive(ctx, vaultID, itemID)
  1054. }
  1055. func (f *fakeListerWithCounter) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  1056. return f.fakeLister.List(ctx, vaultID, opts...)
  1057. }
  1058. func (f *fakeListerWithCounter) Shares() onepassword.ItemsSharesAPI {
  1059. return f.fakeLister.Shares()
  1060. }
  1061. func (f *fakeListerWithCounter) Files() onepassword.ItemsFilesAPI {
  1062. return f.fakeLister.Files()
  1063. }
  1064. func (f *fakeListerWithCounter) Create(ctx context.Context, item onepassword.ItemCreateParams) (onepassword.Item, error) {
  1065. return f.fakeLister.Create(ctx, item)
  1066. }
  1067. var _ onepassword.SecretsAPI = &fakeClient{}
  1068. var _ onepassword.VaultsAPI = &fakeClient{}
  1069. var _ onepassword.ItemsAPI = &fakeLister{}
  1070. var _ onepassword.SecretsAPI = &fakeClientWithCounter{}
  1071. var _ onepassword.ItemsAPI = &fakeListerWithCounter{}