client_test.go 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154
  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. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  29. )
  30. func TestProviderGetSecret(t *testing.T) {
  31. tests := []struct {
  32. name string
  33. ref v1.ExternalSecretDataRemoteRef
  34. want []byte
  35. assertError func(t *testing.T, err error)
  36. client func() *onepassword.Client
  37. }{
  38. {
  39. name: "get secret successfully",
  40. client: func() *onepassword.Client {
  41. fc := &fakeClient{
  42. resolveResult: "secret",
  43. }
  44. return &onepassword.Client{
  45. SecretsAPI: fc,
  46. VaultsAPI: fc,
  47. }
  48. },
  49. assertError: func(t *testing.T, err error) {
  50. require.NoError(t, err)
  51. },
  52. ref: v1.ExternalSecretDataRemoteRef{
  53. Key: "secret",
  54. },
  55. want: []byte("secret"),
  56. },
  57. {
  58. name: "get secret with error",
  59. client: func() *onepassword.Client {
  60. fc := &fakeClient{
  61. resolveError: errors.New("fobar"),
  62. }
  63. return &onepassword.Client{
  64. SecretsAPI: fc,
  65. VaultsAPI: fc,
  66. }
  67. },
  68. assertError: func(t *testing.T, err error) {
  69. require.ErrorContains(t, err, "fobar")
  70. },
  71. ref: v1.ExternalSecretDataRemoteRef{
  72. Key: "secret",
  73. },
  74. },
  75. {
  76. name: "get secret version not implemented",
  77. client: func() *onepassword.Client {
  78. fc := &fakeClient{
  79. resolveResult: "secret",
  80. }
  81. return &onepassword.Client{
  82. SecretsAPI: fc,
  83. VaultsAPI: fc,
  84. }
  85. },
  86. ref: v1.ExternalSecretDataRemoteRef{
  87. Key: "secret",
  88. Version: "1",
  89. },
  90. assertError: func(t *testing.T, err error) {
  91. require.ErrorContains(t, err, "is not implemented in the 1Password SDK provider")
  92. },
  93. },
  94. }
  95. for _, tt := range tests {
  96. t.Run(tt.name, func(t *testing.T) {
  97. p := &SecretsClient{
  98. client: tt.client(),
  99. vaultPrefix: "op://vault/",
  100. }
  101. got, err := p.GetSecret(t.Context(), tt.ref)
  102. tt.assertError(t, err)
  103. require.Equal(t, string(got), string(tt.want))
  104. })
  105. }
  106. }
  107. func TestProviderGetSecretMap(t *testing.T) {
  108. tests := []struct {
  109. name string
  110. ref v1.ExternalSecretDataRemoteRef
  111. want map[string][]byte
  112. assertError func(t *testing.T, err error)
  113. client func() *onepassword.Client
  114. }{
  115. {
  116. name: "get secret successfully for files",
  117. client: func() *onepassword.Client {
  118. fc := &fakeClient{}
  119. fl := &fakeLister{
  120. listAllResult: []onepassword.ItemOverview{
  121. {
  122. ID: "test-item-id",
  123. Title: "key",
  124. Category: "login",
  125. VaultID: "vault-id",
  126. },
  127. },
  128. getResult: onepassword.Item{
  129. ID: "test-item-id",
  130. Title: "key",
  131. Category: "login",
  132. VaultID: "vault-id",
  133. Files: []onepassword.ItemFile{
  134. {
  135. Attributes: onepassword.FileAttributes{
  136. Name: "name",
  137. ID: "id",
  138. },
  139. FieldID: "field-id",
  140. },
  141. },
  142. },
  143. fileLister: &fakeFileLister{
  144. readContent: []byte("content"),
  145. },
  146. }
  147. return &onepassword.Client{
  148. SecretsAPI: fc,
  149. ItemsAPI: fl,
  150. VaultsAPI: fc,
  151. }
  152. },
  153. assertError: func(t *testing.T, err error) {
  154. require.NoError(t, err)
  155. },
  156. ref: v1.ExternalSecretDataRemoteRef{
  157. Key: "key",
  158. Property: "file/name",
  159. },
  160. want: map[string][]byte{
  161. "name": []byte("content"),
  162. },
  163. },
  164. {
  165. name: "get secret successfully for fields",
  166. client: func() *onepassword.Client {
  167. fc := &fakeClient{}
  168. fl := &fakeLister{
  169. listAllResult: []onepassword.ItemOverview{
  170. {
  171. ID: "test-item-id",
  172. Title: "key",
  173. Category: "login",
  174. VaultID: "vault-id",
  175. },
  176. },
  177. getResult: onepassword.Item{
  178. ID: "test-item-id",
  179. Title: "key",
  180. Category: "login",
  181. VaultID: "vault-id",
  182. Fields: []onepassword.ItemField{
  183. {
  184. ID: "field-id",
  185. Title: "name",
  186. FieldType: onepassword.ItemFieldTypeConcealed,
  187. Value: "value",
  188. },
  189. },
  190. },
  191. fileLister: &fakeFileLister{
  192. readContent: []byte("content"),
  193. },
  194. }
  195. return &onepassword.Client{
  196. SecretsAPI: fc,
  197. ItemsAPI: fl,
  198. VaultsAPI: fc,
  199. }
  200. },
  201. assertError: func(t *testing.T, err error) {
  202. require.NoError(t, err)
  203. },
  204. ref: v1.ExternalSecretDataRemoteRef{
  205. Key: "key",
  206. Property: "field/name",
  207. },
  208. want: map[string][]byte{
  209. "name": []byte("value"),
  210. },
  211. },
  212. {
  213. name: "get secret fails with fields with same title",
  214. client: func() *onepassword.Client {
  215. fc := &fakeClient{}
  216. fl := &fakeLister{
  217. listAllResult: []onepassword.ItemOverview{
  218. {
  219. ID: "test-item-id",
  220. Title: "key",
  221. Category: "login",
  222. VaultID: "vault-id",
  223. },
  224. },
  225. getResult: onepassword.Item{
  226. ID: "test-item-id",
  227. Title: "key",
  228. Category: "login",
  229. VaultID: "vault-id",
  230. Fields: []onepassword.ItemField{
  231. {
  232. ID: "field-id",
  233. Title: "name",
  234. FieldType: onepassword.ItemFieldTypeConcealed,
  235. Value: "value",
  236. },
  237. {
  238. ID: "field-id",
  239. Title: "name",
  240. FieldType: onepassword.ItemFieldTypeConcealed,
  241. Value: "value",
  242. },
  243. },
  244. },
  245. fileLister: &fakeFileLister{
  246. readContent: []byte("content"),
  247. },
  248. }
  249. return &onepassword.Client{
  250. SecretsAPI: fc,
  251. ItemsAPI: fl,
  252. VaultsAPI: fc,
  253. }
  254. },
  255. assertError: func(t *testing.T, err error) {
  256. require.ErrorContains(t, err, "found more than 1 fields with title 'name' in 'key', got 2")
  257. },
  258. ref: v1.ExternalSecretDataRemoteRef{
  259. Key: "key",
  260. Property: "field/name",
  261. },
  262. },
  263. }
  264. for _, tt := range tests {
  265. t.Run(tt.name, func(t *testing.T) {
  266. p := &SecretsClient{
  267. client: tt.client(),
  268. vaultPrefix: "op://vault/",
  269. }
  270. got, err := p.GetSecretMap(t.Context(), tt.ref)
  271. tt.assertError(t, err)
  272. require.Equal(t, tt.want, got)
  273. })
  274. }
  275. }
  276. func TestProviderValidate(t *testing.T) {
  277. tests := []struct {
  278. name string
  279. want v1.ValidationResult
  280. assertError func(t *testing.T, err error)
  281. client func() *onepassword.Client
  282. vaultPrefix string
  283. }{
  284. {
  285. name: "validate successfully",
  286. client: func() *onepassword.Client {
  287. fc := &fakeClient{
  288. listAllResult: []onepassword.VaultOverview{
  289. {
  290. ID: "test",
  291. Title: "test",
  292. },
  293. },
  294. }
  295. return &onepassword.Client{
  296. SecretsAPI: fc,
  297. VaultsAPI: fc,
  298. }
  299. },
  300. want: v1.ValidationResultReady,
  301. assertError: func(t *testing.T, err error) {
  302. require.NoError(t, err)
  303. },
  304. vaultPrefix: "op://vault/",
  305. },
  306. }
  307. for _, tt := range tests {
  308. t.Run(tt.name, func(t *testing.T) {
  309. p := &SecretsClient{
  310. client: tt.client(),
  311. vaultPrefix: tt.vaultPrefix,
  312. }
  313. got, err := p.Validate()
  314. tt.assertError(t, err)
  315. require.Equal(t, got, tt.want)
  316. })
  317. }
  318. }
  319. func TestPushSecret(t *testing.T) {
  320. fc := &fakeClient{
  321. listAllResult: []onepassword.VaultOverview{
  322. {
  323. ID: "test",
  324. Title: "test",
  325. },
  326. },
  327. }
  328. tests := []struct {
  329. name string
  330. ref v1alpha1.PushSecretData
  331. secret *corev1.Secret
  332. assertError func(t *testing.T, err error)
  333. lister func() *fakeLister
  334. assertLister func(t *testing.T, lister *fakeLister)
  335. }{
  336. {
  337. name: "create is called",
  338. lister: func() *fakeLister {
  339. return &fakeLister{
  340. listAllResult: []onepassword.ItemOverview{},
  341. }
  342. },
  343. secret: &corev1.Secret{
  344. Data: map[string][]byte{
  345. "foo": []byte("bar"),
  346. },
  347. ObjectMeta: metav1.ObjectMeta{
  348. Name: "secret",
  349. Namespace: "default",
  350. },
  351. },
  352. ref: v1alpha1.PushSecretData{
  353. Match: v1alpha1.PushSecretMatch{
  354. SecretKey: "foo",
  355. RemoteRef: v1alpha1.PushSecretRemoteRef{
  356. RemoteKey: "key",
  357. },
  358. },
  359. },
  360. assertError: func(t *testing.T, err error) {
  361. require.NoError(t, err)
  362. },
  363. assertLister: func(t *testing.T, lister *fakeLister) {
  364. assert.True(t, lister.createCalled)
  365. },
  366. },
  367. {
  368. name: "update is called",
  369. lister: func() *fakeLister {
  370. return &fakeLister{
  371. listAllResult: []onepassword.ItemOverview{
  372. {
  373. ID: "test-item-id",
  374. Title: "key",
  375. Category: "login",
  376. VaultID: "vault-id",
  377. },
  378. },
  379. }
  380. },
  381. secret: &corev1.Secret{
  382. Data: map[string][]byte{
  383. "foo": []byte("bar"),
  384. },
  385. ObjectMeta: metav1.ObjectMeta{
  386. Name: "secret",
  387. Namespace: "default",
  388. },
  389. },
  390. ref: v1alpha1.PushSecretData{
  391. Match: v1alpha1.PushSecretMatch{
  392. SecretKey: "foo",
  393. RemoteRef: v1alpha1.PushSecretRemoteRef{
  394. RemoteKey: "key",
  395. },
  396. },
  397. },
  398. assertError: func(t *testing.T, err error) {
  399. require.NoError(t, err)
  400. },
  401. assertLister: func(t *testing.T, lister *fakeLister) {
  402. assert.True(t, lister.putCalled)
  403. },
  404. },
  405. }
  406. for _, tt := range tests {
  407. t.Run(tt.name, func(t *testing.T) {
  408. ctx := t.Context()
  409. lister := tt.lister()
  410. p := &SecretsClient{
  411. client: &onepassword.Client{
  412. SecretsAPI: fc,
  413. VaultsAPI: fc,
  414. ItemsAPI: lister,
  415. },
  416. }
  417. err := p.PushSecret(ctx, tt.secret, tt.ref)
  418. tt.assertError(t, err)
  419. tt.assertLister(t, lister)
  420. })
  421. }
  422. }
  423. func TestDeleteItemField(t *testing.T) {
  424. fc := &fakeClient{
  425. listAllResult: []onepassword.VaultOverview{
  426. {
  427. ID: "test",
  428. Title: "test",
  429. },
  430. },
  431. }
  432. testCases := []struct {
  433. name string
  434. lister func() *fakeLister
  435. ref *v1alpha1.PushSecretRemoteRef
  436. assertError func(t *testing.T, err error)
  437. assertLister func(t *testing.T, lister *fakeLister)
  438. }{
  439. {
  440. name: "update is called",
  441. ref: &v1alpha1.PushSecretRemoteRef{
  442. RemoteKey: "key",
  443. Property: "password",
  444. },
  445. assertLister: func(t *testing.T, lister *fakeLister) {
  446. require.True(t, lister.putCalled)
  447. },
  448. lister: func() *fakeLister {
  449. fl := &fakeLister{
  450. listAllResult: []onepassword.ItemOverview{
  451. {
  452. ID: "test-item-id",
  453. Title: "key",
  454. Category: "login",
  455. VaultID: "vault-id",
  456. },
  457. },
  458. getResult: onepassword.Item{
  459. ID: "test-item-id",
  460. Title: "key",
  461. Category: "login",
  462. VaultID: "vault-id",
  463. Fields: []onepassword.ItemField{
  464. {
  465. ID: "field-1",
  466. Title: "password",
  467. FieldType: onepassword.ItemFieldTypeConcealed,
  468. Value: "password",
  469. },
  470. {
  471. ID: "field-2",
  472. Title: "other-field",
  473. FieldType: onepassword.ItemFieldTypeConcealed,
  474. Value: "username",
  475. },
  476. },
  477. },
  478. }
  479. return fl
  480. },
  481. assertError: func(t *testing.T, err error) {
  482. require.NoError(t, err)
  483. },
  484. },
  485. {
  486. name: "delete is called",
  487. ref: &v1alpha1.PushSecretRemoteRef{
  488. RemoteKey: "key",
  489. Property: "password",
  490. },
  491. assertLister: func(t *testing.T, lister *fakeLister) {
  492. require.True(t, lister.deleteCalled, "delete should have been called as the item should have existed")
  493. },
  494. lister: func() *fakeLister {
  495. fl := &fakeLister{
  496. listAllResult: []onepassword.ItemOverview{
  497. {
  498. ID: "test-item-id",
  499. Title: "key",
  500. Category: "login",
  501. VaultID: "vault-id",
  502. },
  503. },
  504. getResult: onepassword.Item{
  505. ID: "test-item-id",
  506. Title: "key",
  507. Category: "login",
  508. VaultID: "vault-id",
  509. Fields: []onepassword.ItemField{
  510. {
  511. ID: "field-1",
  512. Title: "password",
  513. FieldType: onepassword.ItemFieldTypeConcealed,
  514. Value: "password",
  515. },
  516. },
  517. },
  518. }
  519. return fl
  520. },
  521. assertError: func(t *testing.T, err error) {
  522. require.NoError(t, err)
  523. },
  524. },
  525. }
  526. for _, testCase := range testCases {
  527. t.Run(testCase.name, func(t *testing.T) {
  528. ctx := t.Context()
  529. lister := testCase.lister()
  530. p := &SecretsClient{
  531. client: &onepassword.Client{
  532. SecretsAPI: fc,
  533. VaultsAPI: fc,
  534. ItemsAPI: lister,
  535. },
  536. }
  537. testCase.assertError(t, p.DeleteSecret(ctx, testCase.ref))
  538. testCase.assertLister(t, lister)
  539. })
  540. }
  541. }
  542. func TestGetVault(t *testing.T) {
  543. fc := &fakeClient{
  544. listAllResult: []onepassword.VaultOverview{
  545. {
  546. ID: "vault-id",
  547. Title: "vault-title",
  548. },
  549. },
  550. }
  551. p := &SecretsClient{
  552. client: &onepassword.Client{
  553. VaultsAPI: fc,
  554. },
  555. }
  556. titleOrUuids := []string{"vault-title", "vault-id"}
  557. for _, titleOrUuid := range titleOrUuids {
  558. t.Run(titleOrUuid, func(t *testing.T) {
  559. vaultID, err := p.GetVault(t.Context(), titleOrUuid)
  560. require.NoError(t, err)
  561. require.Equal(t, fc.listAllResult[0].ID, vaultID)
  562. })
  563. }
  564. }
  565. type fakeLister struct {
  566. listAllResult []onepassword.ItemOverview
  567. listErr error
  568. createCalled bool
  569. createdFieldType onepassword.ItemFieldType
  570. createdParams onepassword.ItemCreateParams
  571. putCalled bool
  572. putItem onepassword.Item
  573. deleteCalled bool
  574. getResult onepassword.Item
  575. getErr error
  576. fileLister onepassword.ItemsFilesAPI
  577. }
  578. func (f *fakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  579. f.createCalled = true
  580. f.createdParams = params
  581. if len(params.Fields) > 0 {
  582. f.createdFieldType = params.Fields[0].FieldType
  583. }
  584. return onepassword.Item{}, nil
  585. }
  586. func (f *fakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  587. return f.getResult, f.getErr
  588. }
  589. func (f *fakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  590. f.putCalled = true
  591. f.putItem = item
  592. return onepassword.Item{}, nil
  593. }
  594. func (f *fakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  595. f.deleteCalled = true
  596. return nil
  597. }
  598. func (f *fakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  599. return nil
  600. }
  601. func (f *fakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  602. return f.listAllResult, f.listErr
  603. }
  604. func (f *fakeLister) Shares() onepassword.ItemsSharesAPI {
  605. return nil
  606. }
  607. func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
  608. return f.fileLister
  609. }
  610. type fakeFileLister struct {
  611. readContent []byte
  612. }
  613. func (f *fakeFileLister) Attach(ctx context.Context, item onepassword.Item, fileParams onepassword.FileCreateParams) (onepassword.Item, error) {
  614. return onepassword.Item{}, nil
  615. }
  616. func (f *fakeFileLister) Read(ctx context.Context, vaultID, itemID string, attr onepassword.FileAttributes) ([]byte, error) {
  617. return f.readContent, nil
  618. }
  619. func (f *fakeFileLister) Delete(ctx context.Context, item onepassword.Item, sectionID, fieldID string) (onepassword.Item, error) {
  620. return onepassword.Item{}, nil
  621. }
  622. func (f *fakeFileLister) ReplaceDocument(ctx context.Context, item onepassword.Item, docParams onepassword.DocumentCreateParams) (onepassword.Item, error) {
  623. return onepassword.Item{}, nil
  624. }
  625. var _ onepassword.ItemsFilesAPI = (*fakeFileLister)(nil)
  626. type statefulFakeLister struct {
  627. listAllResult []onepassword.ItemOverview
  628. items map[string]onepassword.Item
  629. deletedItems map[string]bool
  630. createCalled bool
  631. putCalled bool
  632. deleteCalled bool
  633. fileLister onepassword.ItemsFilesAPI
  634. }
  635. func (f *statefulFakeLister) Create(ctx context.Context, params onepassword.ItemCreateParams) (onepassword.Item, error) {
  636. f.createCalled = true
  637. return onepassword.Item{}, nil
  638. }
  639. func (f *statefulFakeLister) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  640. if f.deletedItems != nil && f.deletedItems[itemID] {
  641. return onepassword.Item{}, fmt.Errorf("item not found")
  642. }
  643. if item, ok := f.items[itemID]; ok {
  644. return item, nil
  645. }
  646. return onepassword.Item{}, fmt.Errorf("item not found")
  647. }
  648. func (f *statefulFakeLister) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  649. f.putCalled = true
  650. if f.items == nil {
  651. f.items = make(map[string]onepassword.Item)
  652. }
  653. f.items[item.ID] = item
  654. return item, nil
  655. }
  656. func (f *statefulFakeLister) Delete(ctx context.Context, vaultID, itemID string) error {
  657. f.deleteCalled = true
  658. if f.deletedItems == nil {
  659. f.deletedItems = make(map[string]bool)
  660. }
  661. f.deletedItems[itemID] = true
  662. delete(f.items, itemID)
  663. f.listAllResult = nil
  664. return nil
  665. }
  666. func (f *statefulFakeLister) Archive(ctx context.Context, vaultID, itemID string) error {
  667. return nil
  668. }
  669. func (f *statefulFakeLister) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  670. return f.listAllResult, nil
  671. }
  672. func (f *statefulFakeLister) Shares() onepassword.ItemsSharesAPI {
  673. return nil
  674. }
  675. func (f *statefulFakeLister) Files() onepassword.ItemsFilesAPI {
  676. return f.fileLister
  677. }
  678. var _ onepassword.ItemsAPI = (*statefulFakeLister)(nil)
  679. type fakeClient struct {
  680. resolveResult string
  681. resolveError error
  682. resolveAll onepassword.ResolveAllResponse
  683. resolveAllError error
  684. listAllResult []onepassword.VaultOverview
  685. listAllError error
  686. }
  687. func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
  688. return f.listAllResult, f.listAllError
  689. }
  690. func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
  691. return f.resolveResult, f.resolveError
  692. }
  693. func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string) (onepassword.ResolveAllResponse, error) {
  694. return f.resolveAll, f.resolveAllError
  695. }
  696. func TestDeleteMultipleFieldsFromSameItem(t *testing.T) {
  697. fc := &fakeClient{
  698. listAllResult: []onepassword.VaultOverview{
  699. {
  700. ID: "test",
  701. Title: "test",
  702. },
  703. },
  704. }
  705. t.Run("deleting second field after item was deleted should not error", func(t *testing.T) {
  706. fl := &statefulFakeLister{
  707. listAllResult: []onepassword.ItemOverview{
  708. {
  709. ID: "test-item-id",
  710. Title: "key",
  711. Category: "login",
  712. VaultID: "vault-id",
  713. },
  714. },
  715. items: map[string]onepassword.Item{
  716. "test-item-id": {
  717. ID: "test-item-id",
  718. Title: "key",
  719. Category: "login",
  720. VaultID: "vault-id",
  721. Fields: []onepassword.ItemField{
  722. {
  723. ID: "field-1",
  724. Title: "username",
  725. FieldType: onepassword.ItemFieldTypeConcealed,
  726. Value: "testuser",
  727. },
  728. {
  729. ID: "field-2",
  730. Title: "password",
  731. FieldType: onepassword.ItemFieldTypeConcealed,
  732. Value: "testpass",
  733. },
  734. },
  735. },
  736. },
  737. }
  738. p := &SecretsClient{
  739. client: &onepassword.Client{
  740. SecretsAPI: fc,
  741. VaultsAPI: fc,
  742. ItemsAPI: fl,
  743. },
  744. }
  745. ctx := t.Context()
  746. err := p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  747. RemoteKey: "key",
  748. Property: "username",
  749. })
  750. require.NoError(t, err, "first field deletion should succeed")
  751. assert.True(t, fl.putCalled, "Put should have been called to update the item")
  752. assert.False(t, fl.deleteCalled, "Delete should not have been called yet")
  753. fl.putCalled = false
  754. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  755. RemoteKey: "key",
  756. Property: "password",
  757. })
  758. require.NoError(t, err, "second field deletion should succeed")
  759. assert.True(t, fl.deleteCalled, "Delete should have been called to remove the item")
  760. fl.listAllResult = nil
  761. err = p.DeleteSecret(ctx, &v1alpha1.PushSecretRemoteRef{
  762. RemoteKey: "key",
  763. Property: "some-other-field",
  764. })
  765. require.NoError(t, err, "deleting a field from an already-deleted item should not error (this is the bug!)")
  766. })
  767. }
  768. func TestCachingGetSecret(t *testing.T) {
  769. t.Run("cache hit returns cached value", func(t *testing.T) {
  770. fcWithCounter := &fakeClientWithCounter{
  771. fakeClient: &fakeClient{
  772. resolveResult: "secret-value",
  773. },
  774. }
  775. p := &SecretsClient{
  776. client: &onepassword.Client{
  777. SecretsAPI: fcWithCounter,
  778. VaultsAPI: fcWithCounter.fakeClient,
  779. },
  780. vaultPrefix: "op://vault/",
  781. }
  782. // Initialize cache
  783. p.cache = expirable.NewLRU[string, []byte](100, nil, time.Minute)
  784. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  785. // First call - cache miss
  786. val1, err := p.GetSecret(t.Context(), ref)
  787. require.NoError(t, err)
  788. assert.Equal(t, []byte("secret-value"), val1)
  789. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  790. // Second call - cache hit, should not call API
  791. val2, err := p.GetSecret(t.Context(), ref)
  792. require.NoError(t, err)
  793. assert.Equal(t, []byte("secret-value"), val2)
  794. assert.Equal(t, 1, fcWithCounter.resolveCallCount, "API should not be called on cache hit")
  795. })
  796. t.Run("cache disabled works normally", func(t *testing.T) {
  797. fcWithCounter := &fakeClientWithCounter{
  798. fakeClient: &fakeClient{
  799. resolveResult: "secret-value",
  800. },
  801. }
  802. p := &SecretsClient{
  803. client: &onepassword.Client{
  804. SecretsAPI: fcWithCounter,
  805. VaultsAPI: fcWithCounter.fakeClient,
  806. },
  807. vaultPrefix: "op://vault/",
  808. cache: nil, // Cache disabled
  809. }
  810. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
  811. // Multiple calls should always hit API
  812. _, err := p.GetSecret(t.Context(), ref)
  813. require.NoError(t, err)
  814. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  815. _, err = p.GetSecret(t.Context(), ref)
  816. require.NoError(t, err)
  817. assert.Equal(t, 2, fcWithCounter.resolveCallCount)
  818. })
  819. }
  820. func TestCachingGetSecretMap(t *testing.T) {
  821. t.Run("cache hit returns cached map", func(t *testing.T) {
  822. fc := &fakeClient{}
  823. flWithCounter := &fakeListerWithCounter{
  824. fakeLister: &fakeLister{
  825. listAllResult: []onepassword.ItemOverview{
  826. {
  827. ID: "item-id",
  828. Title: "item",
  829. Category: "login",
  830. VaultID: "vault-id",
  831. },
  832. },
  833. getResult: onepassword.Item{
  834. ID: "item-id",
  835. Title: "item",
  836. Category: "login",
  837. VaultID: "vault-id",
  838. Fields: []onepassword.ItemField{
  839. {Title: "username", Value: "user1"},
  840. {Title: "password", Value: "pass1"},
  841. },
  842. },
  843. },
  844. }
  845. p := &SecretsClient{
  846. client: &onepassword.Client{
  847. SecretsAPI: fc,
  848. VaultsAPI: fc,
  849. ItemsAPI: flWithCounter,
  850. },
  851. vaultPrefix: "op://vault/",
  852. vaultID: "vault-id",
  853. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  854. }
  855. ref := v1.ExternalSecretDataRemoteRef{Key: "item"}
  856. // First call - cache miss
  857. val1, err := p.GetSecretMap(t.Context(), ref)
  858. require.NoError(t, err)
  859. assert.Equal(t, map[string][]byte{
  860. "username": []byte("user1"),
  861. "password": []byte("pass1"),
  862. }, val1)
  863. assert.Equal(t, 1, flWithCounter.getCallCount)
  864. // Second call - cache hit
  865. val2, err := p.GetSecretMap(t.Context(), ref)
  866. require.NoError(t, err)
  867. assert.Equal(t, val1, val2)
  868. assert.Equal(t, 1, flWithCounter.getCallCount, "API should not be called on cache hit")
  869. })
  870. }
  871. func TestCacheInvalidationPushSecret(t *testing.T) {
  872. t.Run("push secret invalidates cache", func(t *testing.T) {
  873. fcWithCounter := &fakeClientWithCounter{
  874. fakeClient: &fakeClient{
  875. resolveResult: "secret-value",
  876. },
  877. }
  878. fl := &fakeLister{
  879. listAllResult: []onepassword.ItemOverview{
  880. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  881. },
  882. getResult: onepassword.Item{
  883. ID: "item-id",
  884. Title: "item",
  885. VaultID: "vault-id",
  886. Fields: []onepassword.ItemField{{Title: "password", Value: "old"}},
  887. },
  888. }
  889. p := &SecretsClient{
  890. client: &onepassword.Client{
  891. SecretsAPI: fcWithCounter,
  892. VaultsAPI: fcWithCounter.fakeClient,
  893. ItemsAPI: fl,
  894. },
  895. vaultPrefix: "op://vault/",
  896. vaultID: "vault-id",
  897. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  898. }
  899. ref := v1.ExternalSecretDataRemoteRef{Key: "item/password"}
  900. // Populate cache
  901. val1, err := p.GetSecret(t.Context(), ref)
  902. require.NoError(t, err)
  903. assert.Equal(t, []byte("secret-value"), val1)
  904. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  905. // Push new value (should invalidate cache)
  906. pushRef := v1alpha1.PushSecretData{
  907. Match: v1alpha1.PushSecretMatch{
  908. SecretKey: "key",
  909. RemoteRef: v1alpha1.PushSecretRemoteRef{
  910. RemoteKey: "item",
  911. Property: "password",
  912. },
  913. },
  914. }
  915. secret := &corev1.Secret{
  916. Data: map[string][]byte{"key": []byte("new-value")},
  917. }
  918. err = p.PushSecret(t.Context(), secret, pushRef)
  919. require.NoError(t, err)
  920. // Next GetSecret should fetch fresh value (cache was invalidated)
  921. val2, err := p.GetSecret(t.Context(), ref)
  922. require.NoError(t, err)
  923. assert.Equal(t, []byte("secret-value"), val2)
  924. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  925. })
  926. }
  927. func TestCacheInvalidationStaleItemAfterPush(t *testing.T) {
  928. t.Run("push update invalidates the UUID-keyed item cache", func(t *testing.T) {
  929. fc := &fakeClient{
  930. listAllResult: []onepassword.VaultOverview{
  931. {ID: "vault-id", Title: "vault"},
  932. },
  933. }
  934. fl := &statefulFakeListerWithCounter{
  935. statefulFakeLister: &statefulFakeLister{
  936. listAllResult: []onepassword.ItemOverview{
  937. {ID: "item-id", Title: "key", Category: "login", VaultID: "vault-id"},
  938. },
  939. items: map[string]onepassword.Item{
  940. "item-id": {
  941. ID: "item-id",
  942. Title: "key",
  943. Category: "login",
  944. VaultID: "vault-id",
  945. Fields: []onepassword.ItemField{
  946. {ID: "password", Title: "password", FieldType: onepassword.ItemFieldTypeConcealed, Value: "old-value"},
  947. },
  948. },
  949. },
  950. },
  951. }
  952. p := &SecretsClient{
  953. client: &onepassword.Client{
  954. SecretsAPI: fc,
  955. VaultsAPI: fc,
  956. ItemsAPI: fl,
  957. },
  958. vaultPrefix: "op://vault/",
  959. vaultID: "vault-id",
  960. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  961. }
  962. mapRef := v1.ExternalSecretDataRemoteRef{Key: "key", Property: "password"}
  963. got, err := p.GetSecretMap(t.Context(), mapRef)
  964. require.NoError(t, err)
  965. assert.Equal(t, []byte("old-value"), got["password"])
  966. getsAfterWarm := fl.getCallCount
  967. pushRef := v1alpha1.PushSecretData{
  968. Match: v1alpha1.PushSecretMatch{
  969. SecretKey: "key",
  970. RemoteRef: v1alpha1.PushSecretRemoteRef{
  971. RemoteKey: "key",
  972. Property: "password",
  973. },
  974. },
  975. }
  976. secret := &corev1.Secret{
  977. Data: map[string][]byte{"key": []byte("new-value")},
  978. }
  979. require.NoError(t, p.PushSecret(t.Context(), secret, pushRef))
  980. got2, err := p.GetSecretMap(t.Context(), mapRef)
  981. require.NoError(t, err)
  982. assert.Equal(t, []byte("new-value"), got2["password"])
  983. assert.Greater(t, fl.getCallCount, getsAfterWarm, "second read must hit the backend, not the stale UUID cache")
  984. })
  985. }
  986. func TestCacheInvalidationDeleteSecret(t *testing.T) {
  987. t.Run("delete secret invalidates cache", func(t *testing.T) {
  988. fcWithCounter := &fakeClientWithCounter{
  989. fakeClient: &fakeClient{
  990. resolveResult: "cached-value",
  991. },
  992. }
  993. fl := &fakeLister{
  994. listAllResult: []onepassword.ItemOverview{
  995. {ID: "item-id", Title: "item", VaultID: "vault-id"},
  996. },
  997. getResult: onepassword.Item{
  998. ID: "item-id",
  999. Title: "item",
  1000. VaultID: "vault-id",
  1001. Fields: []onepassword.ItemField{
  1002. {Title: "field1", Value: "val1"},
  1003. {Title: "field2", Value: "val2"},
  1004. },
  1005. },
  1006. }
  1007. p := &SecretsClient{
  1008. client: &onepassword.Client{
  1009. SecretsAPI: fcWithCounter,
  1010. VaultsAPI: fcWithCounter.fakeClient,
  1011. ItemsAPI: fl,
  1012. },
  1013. vaultPrefix: "op://vault/",
  1014. vaultID: "vault-id",
  1015. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  1016. }
  1017. ref := v1.ExternalSecretDataRemoteRef{Key: "item/field1"}
  1018. // Populate cache
  1019. _, err := p.GetSecret(t.Context(), ref)
  1020. require.NoError(t, err)
  1021. assert.Equal(t, 1, fcWithCounter.resolveCallCount)
  1022. // Delete field (should invalidate cache)
  1023. deleteRef := v1alpha1.PushSecretRemoteRef{
  1024. RemoteKey: "item",
  1025. Property: "field1",
  1026. }
  1027. err = p.DeleteSecret(t.Context(), deleteRef)
  1028. require.NoError(t, err)
  1029. // Next GetSecret should miss cache
  1030. _, err = p.GetSecret(t.Context(), ref)
  1031. require.NoError(t, err)
  1032. assert.Equal(t, 2, fcWithCounter.resolveCallCount, "Cache should have been invalidated")
  1033. })
  1034. }
  1035. func TestInvalidateCacheByPrefix(t *testing.T) {
  1036. t.Run("invalidates all entries with prefix", func(t *testing.T) {
  1037. p := &SecretsClient{
  1038. vaultPrefix: "op://vault/",
  1039. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  1040. }
  1041. // Add multiple cache entries
  1042. p.cache.Add("op://vault/item1/field1", []byte("val1"))
  1043. p.cache.Add("op://vault/item1/field2", []byte("val2"))
  1044. p.cache.Add("op://vault/item2/field1", []byte("val3"))
  1045. // Invalidate item1 entries
  1046. p.invalidateCacheByPrefix("op://vault/item1")
  1047. // item1 entries should be gone
  1048. _, ok1 := p.cache.Get("op://vault/item1/field1")
  1049. assert.False(t, ok1)
  1050. _, ok2 := p.cache.Get("op://vault/item1/field2")
  1051. assert.False(t, ok2)
  1052. // item2 entry should still exist
  1053. val3, ok3 := p.cache.Get("op://vault/item2/field1")
  1054. assert.True(t, ok3)
  1055. assert.Equal(t, []byte("val3"), val3)
  1056. })
  1057. t.Run("handles nil cache gracefully", func(t *testing.T) {
  1058. p := &SecretsClient{
  1059. vaultPrefix: "op://vault/",
  1060. cache: nil,
  1061. }
  1062. // Should not panic
  1063. p.invalidateCacheByPrefix("op://vault/item1")
  1064. })
  1065. t.Run("does not invalidate entries with similar prefixes", func(t *testing.T) {
  1066. p := &SecretsClient{
  1067. vaultPrefix: "op://vault/",
  1068. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  1069. }
  1070. p.cache.Add("op://vault/item/field1", []byte("val1"))
  1071. p.cache.Add("op://vault/item/field2", []byte("val2"))
  1072. p.cache.Add("op://vault/item|property", []byte("val3"))
  1073. p.cache.Add("op://vault/item-backup/field1", []byte("val4"))
  1074. p.cache.Add("op://vault/prod-db/secret", []byte("val5"))
  1075. p.cache.Add("op://vault/prod-db-replica/secret", []byte("val6"))
  1076. p.cache.Add("op://vault/prod-db-replica/secret|property", []byte("val7"))
  1077. p.invalidateCacheByPrefix("op://vault/item")
  1078. _, ok1 := p.cache.Get("op://vault/item/field1")
  1079. assert.False(t, ok1)
  1080. _, ok2 := p.cache.Get("op://vault/item/field2")
  1081. assert.False(t, ok2)
  1082. _, ok3 := p.cache.Get("op://vault/item|property")
  1083. assert.False(t, ok3)
  1084. val4, ok4 := p.cache.Get("op://vault/item-backup/field1")
  1085. assert.True(t, ok4, "item-backup should not be invalidated")
  1086. assert.Equal(t, []byte("val4"), val4)
  1087. p.invalidateCacheByPrefix("op://vault/prod-db")
  1088. _, ok5 := p.cache.Get("op://vault/prod-db/secret")
  1089. assert.False(t, ok5)
  1090. val6, ok6 := p.cache.Get("op://vault/prod-db-replica/secret")
  1091. assert.True(t, ok6, "prod-db-replica/secret should not be invalidated")
  1092. assert.Equal(t, []byte("val6"), val6)
  1093. val7, ok7 := p.cache.Get("op://vault/prod-db-replica/secret|property")
  1094. assert.True(t, ok7, "prod-db-replica/secret|property should not be invalidated")
  1095. assert.Equal(t, []byte("val7"), val7)
  1096. })
  1097. }
  1098. // fakeClientWithCounter wraps fakeClient and tracks Resolve call count.
  1099. type fakeClientWithCounter struct {
  1100. *fakeClient
  1101. resolveCallCount int
  1102. }
  1103. func (f *fakeClientWithCounter) Resolve(ctx context.Context, secretReference string) (string, error) {
  1104. f.resolveCallCount++
  1105. return f.fakeClient.Resolve(ctx, secretReference)
  1106. }
  1107. // fakeListerWithCounter wraps fakeLister and tracks Get call count.
  1108. type fakeListerWithCounter struct {
  1109. *fakeLister
  1110. getCallCount int
  1111. }
  1112. func (f *fakeListerWithCounter) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  1113. f.getCallCount++
  1114. return f.fakeLister.Get(ctx, vaultID, itemID)
  1115. }
  1116. func (f *fakeListerWithCounter) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  1117. return f.fakeLister.Put(ctx, item)
  1118. }
  1119. func (f *fakeListerWithCounter) Delete(ctx context.Context, vaultID, itemID string) error {
  1120. return f.fakeLister.Delete(ctx, vaultID, itemID)
  1121. }
  1122. func (f *fakeListerWithCounter) Archive(ctx context.Context, vaultID, itemID string) error {
  1123. return f.fakeLister.Archive(ctx, vaultID, itemID)
  1124. }
  1125. func (f *fakeListerWithCounter) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  1126. return f.fakeLister.List(ctx, vaultID, opts...)
  1127. }
  1128. func (f *fakeListerWithCounter) Shares() onepassword.ItemsSharesAPI {
  1129. return f.fakeLister.Shares()
  1130. }
  1131. func (f *fakeListerWithCounter) Files() onepassword.ItemsFilesAPI {
  1132. return f.fakeLister.Files()
  1133. }
  1134. func (f *fakeListerWithCounter) Create(ctx context.Context, item onepassword.ItemCreateParams) (onepassword.Item, error) {
  1135. return f.fakeLister.Create(ctx, item)
  1136. }
  1137. // fakeFileListerWithCounter wraps fakeFileLister and tracks Read call count.
  1138. type fakeFileListerWithCounter struct {
  1139. *fakeFileLister
  1140. readCallCount int
  1141. }
  1142. func (f *fakeFileListerWithCounter) Attach(ctx context.Context, item onepassword.Item, fileParams onepassword.FileCreateParams) (onepassword.Item, error) {
  1143. return onepassword.Item{}, nil
  1144. }
  1145. func (f *fakeFileListerWithCounter) Read(ctx context.Context, vaultID, itemID string, attr onepassword.FileAttributes) ([]byte, error) {
  1146. f.readCallCount++
  1147. return f.readContent, nil
  1148. }
  1149. func (f *fakeFileListerWithCounter) Delete(ctx context.Context, item onepassword.Item, sectionID, fieldID string) (onepassword.Item, error) {
  1150. return onepassword.Item{}, nil
  1151. }
  1152. func (f *fakeFileListerWithCounter) ReplaceDocument(ctx context.Context, item onepassword.Item, docParams onepassword.DocumentCreateParams) (onepassword.Item, error) {
  1153. return onepassword.Item{}, nil
  1154. }
  1155. // statefulFakeListerWithCounter wraps statefulFakeLister and tracks Get call count.
  1156. type statefulFakeListerWithCounter struct {
  1157. *statefulFakeLister
  1158. getCallCount int
  1159. listCallCount int
  1160. }
  1161. func (f *statefulFakeListerWithCounter) Get(ctx context.Context, vaultID, itemID string) (onepassword.Item, error) {
  1162. f.getCallCount++
  1163. return f.statefulFakeLister.Get(ctx, vaultID, itemID)
  1164. }
  1165. func (f *statefulFakeListerWithCounter) Put(ctx context.Context, item onepassword.Item) (onepassword.Item, error) {
  1166. return f.statefulFakeLister.Put(ctx, item)
  1167. }
  1168. func (f *statefulFakeListerWithCounter) Delete(ctx context.Context, vaultID, itemID string) error {
  1169. return f.statefulFakeLister.Delete(ctx, vaultID, itemID)
  1170. }
  1171. func (f *statefulFakeListerWithCounter) Archive(ctx context.Context, vaultID, itemID string) error {
  1172. return f.statefulFakeLister.Archive(ctx, vaultID, itemID)
  1173. }
  1174. func (f *statefulFakeListerWithCounter) List(ctx context.Context, vaultID string, opts ...onepassword.ItemListFilter) ([]onepassword.ItemOverview, error) {
  1175. f.listCallCount++
  1176. return f.statefulFakeLister.List(ctx, vaultID, opts...)
  1177. }
  1178. func (f *statefulFakeListerWithCounter) Shares() onepassword.ItemsSharesAPI {
  1179. return f.statefulFakeLister.Shares()
  1180. }
  1181. func (f *statefulFakeListerWithCounter) Files() onepassword.ItemsFilesAPI {
  1182. return f.statefulFakeLister.Files()
  1183. }
  1184. func (f *statefulFakeListerWithCounter) Create(ctx context.Context, item onepassword.ItemCreateParams) (onepassword.Item, error) {
  1185. return f.statefulFakeLister.Create(ctx, item)
  1186. }
  1187. var _ onepassword.SecretsAPI = &fakeClient{}
  1188. var _ onepassword.VaultsAPI = &fakeClient{}
  1189. var _ onepassword.ItemsAPI = &fakeLister{}
  1190. var _ onepassword.SecretsAPI = &fakeClientWithCounter{}
  1191. var _ onepassword.ItemsAPI = &fakeListerWithCounter{}
  1192. func TestSecretExists(t *testing.T) {
  1193. fc := &fakeClient{
  1194. listAllResult: []onepassword.VaultOverview{
  1195. {ID: "vault-id", Title: "vault"},
  1196. },
  1197. }
  1198. itemWithPassword := &fakeLister{
  1199. listAllResult: []onepassword.ItemOverview{
  1200. {ID: "item-id", Title: "key", VaultID: "vault-id"},
  1201. },
  1202. getResult: onepassword.Item{
  1203. ID: "item-id", Title: "key", VaultID: "vault-id",
  1204. Fields: []onepassword.ItemField{
  1205. {Title: "password", Value: "s3cr3t"},
  1206. },
  1207. },
  1208. }
  1209. tests := []struct {
  1210. name string
  1211. ref v1alpha1.PushSecretRemoteRef
  1212. lister *fakeLister
  1213. wantExists bool
  1214. assertError func(t *testing.T, err error)
  1215. }{
  1216. {
  1217. name: "item does not exist returns false",
  1218. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "missing"},
  1219. lister: &fakeLister{listAllResult: []onepassword.ItemOverview{}},
  1220. wantExists: false,
  1221. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1222. },
  1223. {
  1224. name: "item exists no property returns true",
  1225. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key"},
  1226. lister: itemWithPassword,
  1227. wantExists: true,
  1228. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1229. },
  1230. {
  1231. name: "item exists field present returns true",
  1232. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key", Property: "password"},
  1233. lister: itemWithPassword,
  1234. wantExists: true,
  1235. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1236. },
  1237. {
  1238. name: "item exists field absent returns false",
  1239. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key", Property: "api-token"},
  1240. lister: itemWithPassword,
  1241. wantExists: false,
  1242. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1243. },
  1244. {
  1245. name: "pushAllKeys scenario: item exists with no fields returns true",
  1246. ref: v1alpha1.PushSecretRemoteRef{RemoteKey: "key"},
  1247. lister: &fakeLister{
  1248. listAllResult: []onepassword.ItemOverview{
  1249. {ID: "item-id", Title: "key", VaultID: "vault-id"},
  1250. },
  1251. getResult: onepassword.Item{
  1252. ID: "item-id", Title: "key", VaultID: "vault-id",
  1253. Fields: []onepassword.ItemField{},
  1254. },
  1255. },
  1256. wantExists: true,
  1257. assertError: func(t *testing.T, err error) { require.NoError(t, err) },
  1258. },
  1259. }
  1260. for _, tt := range tests {
  1261. t.Run(tt.name, func(t *testing.T) {
  1262. p := &SecretsClient{
  1263. client: &onepassword.Client{
  1264. SecretsAPI: fc,
  1265. VaultsAPI: fc,
  1266. ItemsAPI: tt.lister,
  1267. },
  1268. vaultID: "vault-id",
  1269. }
  1270. exists, err := p.SecretExists(t.Context(), tt.ref)
  1271. tt.assertError(t, err)
  1272. assert.Equal(t, tt.wantExists, exists)
  1273. })
  1274. }
  1275. }
  1276. func TestResolveFieldType(t *testing.T) {
  1277. tests := []struct {
  1278. input string
  1279. expected onepassword.ItemFieldType
  1280. }{
  1281. {"text", onepassword.ItemFieldTypeText},
  1282. {"Text", onepassword.ItemFieldTypeText},
  1283. {"TEXT", onepassword.ItemFieldTypeText},
  1284. {"concealed", onepassword.ItemFieldTypeConcealed},
  1285. {"Concealed", onepassword.ItemFieldTypeConcealed},
  1286. {"url", onepassword.ItemFieldTypeURL},
  1287. {"URL", onepassword.ItemFieldTypeURL},
  1288. {"email", onepassword.ItemFieldTypeEmail},
  1289. {"Email", onepassword.ItemFieldTypeEmail},
  1290. {"phone", onepassword.ItemFieldTypePhone},
  1291. {"date", onepassword.ItemFieldTypeDate},
  1292. {"monthYear", onepassword.ItemFieldTypeMonthYear},
  1293. {"monthyear", onepassword.ItemFieldTypeMonthYear},
  1294. {"MONTHYEAR", onepassword.ItemFieldTypeMonthYear},
  1295. {"", onepassword.ItemFieldTypeConcealed},
  1296. {"unknown", onepassword.ItemFieldTypeConcealed},
  1297. {"otp", onepassword.ItemFieldTypeConcealed},
  1298. {"file", onepassword.ItemFieldTypeConcealed},
  1299. }
  1300. for _, tt := range tests {
  1301. t.Run(tt.input, func(t *testing.T) {
  1302. got := resolveFieldType(tt.input)
  1303. assert.Equal(t, tt.expected, got)
  1304. })
  1305. }
  1306. }
  1307. func TestPushSecretFieldType(t *testing.T) {
  1308. fc := &fakeClient{
  1309. listAllResult: []onepassword.VaultOverview{
  1310. {ID: "vault-id", Title: "vault"},
  1311. },
  1312. }
  1313. tests := []struct {
  1314. name string
  1315. metadataJSON string
  1316. wantFieldType onepassword.ItemFieldType
  1317. }{
  1318. {
  1319. name: "no metadata defaults to Concealed",
  1320. metadataJSON: "",
  1321. wantFieldType: onepassword.ItemFieldTypeConcealed,
  1322. },
  1323. {
  1324. name: "fieldType text creates Text field",
  1325. metadataJSON: `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"fieldType":"text"}}`,
  1326. wantFieldType: onepassword.ItemFieldTypeText,
  1327. },
  1328. {
  1329. name: "fieldType URL case-insensitive",
  1330. metadataJSON: `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"fieldType":"URL"}}`,
  1331. wantFieldType: onepassword.ItemFieldTypeURL,
  1332. },
  1333. }
  1334. for _, tt := range tests {
  1335. t.Run(tt.name, func(t *testing.T) {
  1336. fl := &fakeLister{
  1337. listAllResult: []onepassword.ItemOverview{},
  1338. }
  1339. p := &SecretsClient{
  1340. client: &onepassword.Client{
  1341. SecretsAPI: fc,
  1342. VaultsAPI: fc,
  1343. ItemsAPI: fl,
  1344. },
  1345. vaultID: "vault-id",
  1346. }
  1347. ref := v1alpha1.PushSecretData{
  1348. Match: v1alpha1.PushSecretMatch{
  1349. SecretKey: "key",
  1350. RemoteRef: v1alpha1.PushSecretRemoteRef{RemoteKey: "item", Property: "field"},
  1351. },
  1352. }
  1353. if tt.metadataJSON != "" {
  1354. raw := apiextensionsv1.JSON{Raw: []byte(tt.metadataJSON)}
  1355. ref.Metadata = &raw
  1356. }
  1357. secret := &corev1.Secret{
  1358. Data: map[string][]byte{"key": []byte("value")},
  1359. }
  1360. err := p.PushSecret(t.Context(), secret, ref)
  1361. require.NoError(t, err)
  1362. require.True(t, fl.createCalled, "Create should have been called")
  1363. assert.Equal(t, tt.wantFieldType, fl.createdFieldType)
  1364. })
  1365. }
  1366. }
  1367. func TestUpdateFieldValueChangesFieldType(t *testing.T) {
  1368. // Regression test: updateFieldValue must update FieldType when spec.fieldType changes,
  1369. // not only when Value changes.
  1370. fields := []onepassword.ItemField{
  1371. {Title: "myfield", Value: "secret", FieldType: onepassword.ItemFieldTypeConcealed},
  1372. }
  1373. updated, err := updateFieldValue(fields, "myfield", "secret", onepassword.ItemFieldTypeText)
  1374. require.NoError(t, err)
  1375. require.Len(t, updated, 1)
  1376. assert.Equal(t, onepassword.ItemFieldTypeText, updated[0].FieldType, "FieldType should be updated even when Value is unchanged")
  1377. assert.Equal(t, "secret", updated[0].Value)
  1378. }
  1379. func TestGenerateNewItemFieldHasNonEmptyID(t *testing.T) {
  1380. // Regression test: fields created without an ID cause "duplicate field ids" errors
  1381. // when two PushSecret data entries target the same 1Password item.
  1382. // See: generateNewItemField must always produce a non-empty ID.
  1383. tests := []struct {
  1384. title string
  1385. fieldType onepassword.ItemFieldType
  1386. }{
  1387. {"password", onepassword.ItemFieldTypeConcealed},
  1388. {"api-endpoint", onepassword.ItemFieldTypeURL},
  1389. {"username", onepassword.ItemFieldTypeText},
  1390. }
  1391. for _, tt := range tests {
  1392. field := generateNewItemField(tt.title, "value", tt.fieldType)
  1393. assert.NotEmpty(t, field.ID, "field ID must be non-empty to avoid duplicate ID errors on Put")
  1394. assert.Equal(t, tt.title, field.Title)
  1395. assert.Equal(t, "value", field.Value)
  1396. }
  1397. }
  1398. func TestNormalizeItemFields(t *testing.T) {
  1399. // Regression test: fields fetched from 1Password can have SectionID pointing to ""
  1400. // instead of nil. The SDK rejects Put when a field references a section ID that
  1401. // doesn't exist in item.Sections — even an empty-string pointer triggers this.
  1402. emptyStr := ""
  1403. realSection := "extra"
  1404. fields := []onepassword.ItemField{
  1405. {ID: "a", Title: "a", SectionID: &emptyStr},
  1406. {ID: "b", Title: "b", SectionID: nil},
  1407. {ID: "c", Title: "c", SectionID: &realSection},
  1408. }
  1409. got := normalizeItemFields(fields)
  1410. assert.Nil(t, got[0].SectionID, "empty-string SectionID should be normalized to nil")
  1411. assert.Nil(t, got[1].SectionID, "nil SectionID should remain nil")
  1412. assert.Equal(t, &realSection, got[2].SectionID, "non-empty SectionID should be unchanged")
  1413. }
  1414. func TestIsNativeID(t *testing.T) {
  1415. tests := []struct {
  1416. name string
  1417. input string
  1418. expected bool
  1419. }{
  1420. {"valid native ID", "gdpvdudxrico74msloimk7qjna", true},
  1421. {"valid native ID all letters", "abcdefghijklmnopqrstuvwxyz", true},
  1422. {"valid native ID with digits", "abcdefghij0123456789abcdef", true},
  1423. {"too short", "gdpvdudxrico74msloimk7qjn", false},
  1424. {"too long", "gdpvdudxrico74msloimk7qjnaa", false},
  1425. {"empty string", "", false},
  1426. {"contains uppercase", "Gdpvdudxrico74msloimk7qjna", false},
  1427. {"contains special char", "gdpvdudxrico7-msloimk7qjna", false},
  1428. {"RFC 4122 UUID", "687adbe7-e6d2-4059-9a62-dbb95d291143", false},
  1429. {"item title", "My App (Production)", false},
  1430. }
  1431. for _, tt := range tests {
  1432. t.Run(tt.name, func(t *testing.T) {
  1433. got := isNativeID(tt.input)
  1434. if got != tt.expected {
  1435. t.Errorf("isNativeID(%q) = %v, want %v", tt.input, got, tt.expected)
  1436. }
  1437. })
  1438. }
  1439. }
  1440. func TestGetAllSecrets(t *testing.T) {
  1441. item1 := onepassword.Item{
  1442. ID: "item-1",
  1443. Title: "key1",
  1444. Category: "login",
  1445. VaultID: "vault-id",
  1446. Tags: []string{"tag1"},
  1447. Fields: []onepassword.ItemField{
  1448. {
  1449. ID: "field-1",
  1450. Title: "username",
  1451. FieldType: onepassword.ItemFieldTypeConcealed,
  1452. Value: "testuser",
  1453. },
  1454. {
  1455. ID: "field-2",
  1456. Title: "password",
  1457. FieldType: onepassword.ItemFieldTypeConcealed,
  1458. Value: "testpass",
  1459. },
  1460. },
  1461. }
  1462. item2 := onepassword.Item{
  1463. ID: "item-2",
  1464. Title: "key2",
  1465. Category: "login",
  1466. VaultID: "vault-id",
  1467. Tags: []string{"tag2"},
  1468. Fields: []onepassword.ItemField{
  1469. {
  1470. ID: "field-3",
  1471. Title: "db-host",
  1472. FieldType: onepassword.ItemFieldTypeConcealed,
  1473. Value: "testdb",
  1474. },
  1475. },
  1476. }
  1477. item3 := onepassword.Item{
  1478. ID: "item-3",
  1479. Title: "file1",
  1480. Category: "login",
  1481. VaultID: "vault-id",
  1482. Tags: []string{"tag1"},
  1483. Files: []onepassword.ItemFile{
  1484. {
  1485. Attributes: onepassword.FileAttributes{
  1486. Name: "certfile",
  1487. ID: "file-id",
  1488. },
  1489. FieldID: "field-4",
  1490. },
  1491. },
  1492. }
  1493. createLister := func(items ...onepassword.Item) *statefulFakeLister {
  1494. fl := &statefulFakeLister{
  1495. items: make(map[string]onepassword.Item),
  1496. fileLister: &fakeFileLister{
  1497. readContent: []byte("content"),
  1498. },
  1499. }
  1500. for _, it := range items {
  1501. fl.items[it.ID] = it
  1502. fl.listAllResult = append(fl.listAllResult, onepassword.ItemOverview{
  1503. ID: it.ID,
  1504. Title: it.Title,
  1505. VaultID: it.VaultID,
  1506. Tags: it.Tags,
  1507. })
  1508. }
  1509. return fl
  1510. }
  1511. tests := []struct {
  1512. name string
  1513. ref v1.ExternalSecretFind
  1514. want map[string][]byte
  1515. assertError func(t *testing.T, err error)
  1516. client func() *onepassword.Client
  1517. }{
  1518. {
  1519. name: "returns all fields and files from all items",
  1520. client: func() *onepassword.Client {
  1521. return &onepassword.Client{
  1522. SecretsAPI: &fakeClient{},
  1523. VaultsAPI: &fakeClient{},
  1524. ItemsAPI: createLister(item1, item2, item3),
  1525. }
  1526. },
  1527. assertError: func(t *testing.T, err error) {
  1528. require.NoError(t, err)
  1529. },
  1530. ref: v1.ExternalSecretFind{},
  1531. want: map[string][]byte{
  1532. "username": []byte("testuser"),
  1533. "password": []byte("testpass"),
  1534. "db-host": []byte("testdb"),
  1535. "certfile": []byte("content"),
  1536. },
  1537. },
  1538. {
  1539. name: "filters items by path",
  1540. client: func() *onepassword.Client {
  1541. return &onepassword.Client{
  1542. SecretsAPI: &fakeClient{},
  1543. VaultsAPI: &fakeClient{},
  1544. ItemsAPI: createLister(item1, item2, item3),
  1545. }
  1546. },
  1547. assertError: func(t *testing.T, err error) {
  1548. require.NoError(t, err)
  1549. },
  1550. ref: v1.ExternalSecretFind{
  1551. Path: &item1.Title,
  1552. },
  1553. want: map[string][]byte{
  1554. "username": []byte("testuser"),
  1555. "password": []byte("testpass"),
  1556. },
  1557. },
  1558. {
  1559. name: "filters items by tag",
  1560. client: func() *onepassword.Client {
  1561. return &onepassword.Client{
  1562. SecretsAPI: &fakeClient{},
  1563. VaultsAPI: &fakeClient{},
  1564. ItemsAPI: createLister(item1, item2, item3),
  1565. }
  1566. },
  1567. assertError: func(t *testing.T, err error) {
  1568. require.NoError(t, err)
  1569. },
  1570. ref: v1.ExternalSecretFind{
  1571. Tags: map[string]string{"tag1": "true"},
  1572. },
  1573. want: map[string][]byte{
  1574. "username": []byte("testuser"),
  1575. "password": []byte("testpass"),
  1576. "certfile": []byte("content"),
  1577. },
  1578. },
  1579. {
  1580. name: "filters fields by name regex",
  1581. client: func() *onepassword.Client {
  1582. return &onepassword.Client{
  1583. SecretsAPI: &fakeClient{},
  1584. VaultsAPI: &fakeClient{},
  1585. ItemsAPI: createLister(item1, item2, item3),
  1586. }
  1587. },
  1588. assertError: func(t *testing.T, err error) {
  1589. require.NoError(t, err)
  1590. },
  1591. ref: v1.ExternalSecretFind{
  1592. Name: &v1.FindName{RegExp: "e"},
  1593. },
  1594. want: map[string][]byte{
  1595. "username": []byte("testuser"),
  1596. "certfile": []byte("content"),
  1597. },
  1598. },
  1599. {
  1600. name: "returns error on duplicate field name",
  1601. client: func() *onepassword.Client {
  1602. item2dup := onepassword.Item{
  1603. ID: "item-2-dup",
  1604. Title: "key2-dup",
  1605. Category: "login",
  1606. VaultID: "vault-id",
  1607. Tags: []string{"tag2"},
  1608. Fields: []onepassword.ItemField{
  1609. {
  1610. ID: "field-3-dup",
  1611. Title: "db-host",
  1612. FieldType: onepassword.ItemFieldTypeConcealed,
  1613. Value: "testdb-dup",
  1614. },
  1615. },
  1616. }
  1617. return &onepassword.Client{
  1618. SecretsAPI: &fakeClient{},
  1619. VaultsAPI: &fakeClient{},
  1620. ItemsAPI: createLister(item2, item2dup),
  1621. }
  1622. },
  1623. assertError: func(t *testing.T, err error) {
  1624. require.ErrorContains(t, err, "found multiple labels with the same key")
  1625. },
  1626. ref: v1.ExternalSecretFind{},
  1627. },
  1628. {
  1629. name: "returns error on duplicate file name",
  1630. client: func() *onepassword.Client {
  1631. item3dup := onepassword.Item{
  1632. ID: "item-3",
  1633. Title: "file1-dup",
  1634. Category: "login",
  1635. VaultID: "vault-id",
  1636. Tags: []string{"tag1"},
  1637. Files: []onepassword.ItemFile{
  1638. {
  1639. Attributes: onepassword.FileAttributes{
  1640. Name: "certfile",
  1641. ID: "file-id-dup",
  1642. },
  1643. FieldID: "field-4-dup",
  1644. },
  1645. },
  1646. }
  1647. return &onepassword.Client{
  1648. SecretsAPI: &fakeClient{},
  1649. VaultsAPI: &fakeClient{},
  1650. ItemsAPI: createLister(item3, item3dup),
  1651. }
  1652. },
  1653. assertError: func(t *testing.T, err error) {
  1654. require.ErrorContains(t, err, "found multiple labels with the same key")
  1655. },
  1656. ref: v1.ExternalSecretFind{},
  1657. },
  1658. {
  1659. name: "returns error if field name matches file name",
  1660. client: func() *onepassword.Client {
  1661. item2filedup := onepassword.Item{
  1662. ID: "item-2-dup",
  1663. Title: "key2-dup",
  1664. Category: "login",
  1665. VaultID: "vault-id",
  1666. Tags: []string{"tag1"},
  1667. Files: []onepassword.ItemFile{
  1668. {
  1669. Attributes: onepassword.FileAttributes{
  1670. Name: "db-host",
  1671. ID: "file-id-dup",
  1672. },
  1673. FieldID: "field-3-dup",
  1674. },
  1675. },
  1676. }
  1677. return &onepassword.Client{
  1678. SecretsAPI: &fakeClient{},
  1679. VaultsAPI: &fakeClient{},
  1680. ItemsAPI: createLister(item2, item2filedup),
  1681. }
  1682. },
  1683. assertError: func(t *testing.T, err error) {
  1684. require.ErrorContains(t, err, "found multiple labels with the same key")
  1685. },
  1686. ref: v1.ExternalSecretFind{},
  1687. },
  1688. {
  1689. name: "returns error when list fails",
  1690. client: func() *onepassword.Client {
  1691. fl := &fakeLister{listAllResult: nil}
  1692. fl.listErr = errors.New("list error")
  1693. return &onepassword.Client{
  1694. SecretsAPI: &fakeClient{},
  1695. VaultsAPI: &fakeClient{},
  1696. ItemsAPI: fl,
  1697. }
  1698. },
  1699. assertError: func(t *testing.T, err error) {
  1700. require.ErrorContains(t, err, "list error")
  1701. },
  1702. ref: v1.ExternalSecretFind{},
  1703. },
  1704. {
  1705. name: "returns error when get fails",
  1706. client: func() *onepassword.Client {
  1707. fl := &fakeLister{
  1708. listAllResult: []onepassword.ItemOverview{{ID: "item-1", Title: "app-secrets", VaultID: "vault-id"}},
  1709. getErr: errors.New("get error"),
  1710. }
  1711. return &onepassword.Client{
  1712. SecretsAPI: &fakeClient{},
  1713. VaultsAPI: &fakeClient{},
  1714. ItemsAPI: fl,
  1715. }
  1716. },
  1717. assertError: func(t *testing.T, err error) {
  1718. require.ErrorContains(t, err, "get error")
  1719. },
  1720. ref: v1.ExternalSecretFind{},
  1721. },
  1722. }
  1723. for _, tt := range tests {
  1724. t.Run(tt.name, func(t *testing.T) {
  1725. p := &SecretsClient{
  1726. client: tt.client(),
  1727. vaultPrefix: "op://vault/",
  1728. }
  1729. got, err := p.GetAllSecrets(t.Context(), tt.ref)
  1730. tt.assertError(t, err)
  1731. require.Equal(t, tt.want, got)
  1732. })
  1733. }
  1734. }
  1735. func TestCachingGetAllSecrets(t *testing.T) {
  1736. item1 := onepassword.Item{
  1737. ID: "item1aaaaaaaaaaaaaaaaaaaaa",
  1738. Title: "key1",
  1739. Category: "login",
  1740. VaultID: "vault-id",
  1741. Tags: []string{"tag1"},
  1742. Fields: []onepassword.ItemField{
  1743. {
  1744. ID: "field1aaaaaaaaaaaaaaaaaaaa",
  1745. Title: "username",
  1746. FieldType: onepassword.ItemFieldTypeConcealed,
  1747. Value: "testuser",
  1748. },
  1749. {
  1750. ID: "field2aaaaaaaaaaaaaaaaaaaa",
  1751. Title: "password",
  1752. FieldType: onepassword.ItemFieldTypeConcealed,
  1753. Value: "testpass",
  1754. },
  1755. },
  1756. }
  1757. item2 := onepassword.Item{
  1758. ID: "item2bbbbbbbbbbbbbbbbbbbbb",
  1759. Title: "file1",
  1760. Category: "login",
  1761. VaultID: "vault-id",
  1762. Tags: []string{"tag2"},
  1763. Files: []onepassword.ItemFile{
  1764. {
  1765. Attributes: onepassword.FileAttributes{
  1766. Name: "certfile",
  1767. ID: "item2filebbbbbbbbbbbbbbbbb",
  1768. },
  1769. FieldID: "field3bbbbbbbbbbbbbbbbbbbb",
  1770. },
  1771. },
  1772. }
  1773. createLister := func(items ...onepassword.Item) *statefulFakeListerWithCounter {
  1774. fl := &statefulFakeListerWithCounter{
  1775. statefulFakeLister: &statefulFakeLister{
  1776. items: make(map[string]onepassword.Item),
  1777. fileLister: &fakeFileListerWithCounter{
  1778. fakeFileLister: &fakeFileLister{
  1779. readContent: []byte("content"),
  1780. },
  1781. },
  1782. },
  1783. }
  1784. for _, it := range items {
  1785. fl.items[it.ID] = it
  1786. fl.listAllResult = append(fl.listAllResult, onepassword.ItemOverview{
  1787. ID: it.ID,
  1788. Title: it.Title,
  1789. VaultID: it.VaultID,
  1790. Tags: it.Tags,
  1791. })
  1792. }
  1793. return fl
  1794. }
  1795. newCachedClient := func(fl *statefulFakeListerWithCounter) *SecretsClient {
  1796. fc := &fakeClientWithCounter{
  1797. fakeClient: &fakeClient{
  1798. listAllResult: []onepassword.VaultOverview{{ID: "vault-id", Title: "vault"}},
  1799. resolveResult: "testpass",
  1800. },
  1801. }
  1802. return &SecretsClient{
  1803. client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl},
  1804. vaultID: "vault-id",
  1805. cache: expirable.NewLRU[string, []byte](100, nil, time.Minute),
  1806. }
  1807. }
  1808. t.Run("second GetAllSecrets call does not re-fetch item list from API", func(t *testing.T) {
  1809. fl := createLister(item1, item2)
  1810. p := newCachedClient(fl)
  1811. find := v1.ExternalSecretFind{}
  1812. _, err := p.GetAllSecrets(t.Context(), find)
  1813. require.NoError(t, err)
  1814. // There can be quite a few list calls depending on the items, so we'll just make sure
  1815. // at least one was made, and that the count doesn't increase on the second call.
  1816. assert.NotZero(t, fl.listCallCount, "first call should list from API")
  1817. previousCallCount := fl.listCallCount
  1818. _, err = p.GetAllSecrets(t.Context(), find)
  1819. require.NoError(t, err)
  1820. assert.Equal(t, previousCallCount, fl.listCallCount, "second call should use item cache, not re-fetch list from API")
  1821. })
  1822. t.Run("second GetAllSecrets call does not re-fetch items from API", func(t *testing.T) {
  1823. fl := createLister(item1)
  1824. p := newCachedClient(fl)
  1825. find := v1.ExternalSecretFind{}
  1826. _, err := p.GetAllSecrets(t.Context(), find)
  1827. require.NoError(t, err)
  1828. assert.Equal(t, 1, fl.getCallCount, "first call should fetch from API")
  1829. _, err = p.GetAllSecrets(t.Context(), find)
  1830. require.NoError(t, err)
  1831. assert.Equal(t, 1, fl.getCallCount, "second call should use item cache, not re-fetch")
  1832. })
  1833. t.Run("second GetAllSecrets call does not re-read files from API", func(t *testing.T) {
  1834. fl := createLister(item2)
  1835. ffl := fl.fileLister.(*fakeFileListerWithCounter)
  1836. p := newCachedClient(fl)
  1837. find := v1.ExternalSecretFind{}
  1838. _, err := p.GetAllSecrets(t.Context(), find)
  1839. require.NoError(t, err)
  1840. assert.Equal(t, 1, ffl.readCallCount, "first call should read file from API")
  1841. _, err = p.GetAllSecrets(t.Context(), find)
  1842. require.NoError(t, err)
  1843. assert.Equal(t, 1, ffl.readCallCount, "second call should use file cache, not re-read")
  1844. })
  1845. t.Run("item fetched by GetAllSecrets is reused by GetSecretMap", func(t *testing.T) {
  1846. fl := createLister(item1)
  1847. p := newCachedClient(fl)
  1848. want := map[string][]byte{"username": []byte("testuser"), "password": []byte("testpass")}
  1849. got, err := p.GetAllSecrets(t.Context(), v1.ExternalSecretFind{})
  1850. require.NoError(t, err)
  1851. assert.Equal(t, 1, fl.getCallCount, "GetAllSecrets fetches item once")
  1852. assert.Equal(t, want, got, "GetAllSecrets should return expected secrets")
  1853. got, err = p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Key: item1.Title})
  1854. require.NoError(t, err)
  1855. assert.Equal(t, 1, fl.getCallCount, "GetSecretMap should use cached item from GetAllSecrets")
  1856. assert.Equal(t, want, got, "GetSecretMap should return expected secrets")
  1857. want = map[string][]byte{"username": []byte("testuser")}
  1858. got, err = p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Key: item1.Title, Property: "username"})
  1859. require.NoError(t, err)
  1860. assert.Equal(t, 1, fl.getCallCount, "GetSecretMap with Property should use cached item from GetAllSecrets")
  1861. assert.Equal(t, want, got, "GetSecretMap with Property should return expected secrets")
  1862. })
  1863. t.Run("item fetched by GetAllSecrets is reused by GetSecret", func(t *testing.T) {
  1864. fl := createLister(item1)
  1865. p := newCachedClient(fl)
  1866. fc := p.client.SecretsAPI.(*fakeClientWithCounter)
  1867. wantAllSecrets := map[string][]byte{"username": []byte("testuser"), "password": []byte("testpass")}
  1868. gotAllSecrets, err := p.GetAllSecrets(t.Context(), v1.ExternalSecretFind{})
  1869. require.NoError(t, err)
  1870. assert.Equal(t, 1, fl.getCallCount, "GetAllSecrets fetches item once")
  1871. assert.Equal(t, 0, fc.resolveCallCount, "GetAllSecrets should not call Resolve")
  1872. assert.Equal(t, wantAllSecrets, gotAllSecrets, "GetAllSecrets should return expected secrets")
  1873. wantSecret := []byte("testpass")
  1874. gotSecret, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: fmt.Sprintf("%s/password", item1.Title)})
  1875. require.NoError(t, err)
  1876. assert.Equal(t, 1, fl.getCallCount, "GetSecret should use cached item from GetAllSecrets")
  1877. assert.Equal(t, 0, fc.resolveCallCount, "GetSecret should not call Resolve due to cached value")
  1878. assert.Equal(t, wantSecret, gotSecret, "GetSecret should return expected secrets")
  1879. })
  1880. }
  1881. func TestPushAllKeys(t *testing.T) {
  1882. const (
  1883. testExistingItem = "existing-item"
  1884. testOldKey = "old-key"
  1885. )
  1886. fc := &fakeClient{listAllResult: []onepassword.VaultOverview{{ID: "vault-id", Title: "vault"}}}
  1887. existingItem := onepassword.Item{
  1888. ID: "item-id", Title: testExistingItem, VaultID: "vault-id",
  1889. Fields: []onepassword.ItemField{
  1890. {ID: testOldKey, Title: testOldKey, Value: "old-val", FieldType: onepassword.ItemFieldTypeConcealed},
  1891. },
  1892. }
  1893. newLister := func(existing ...onepassword.Item) *fakeLister {
  1894. fl := &fakeLister{listAllResult: []onepassword.ItemOverview{}}
  1895. if len(existing) > 0 {
  1896. fl.getResult = existing[0]
  1897. fl.listAllResult = []onepassword.ItemOverview{{ID: existing[0].ID, Title: existing[0].Title, VaultID: existing[0].VaultID}}
  1898. }
  1899. return fl
  1900. }
  1901. fieldsMap := func(fields []onepassword.ItemField) map[string]onepassword.ItemField {
  1902. m := make(map[string]onepassword.ItemField, len(fields))
  1903. for _, f := range fields {
  1904. m[f.Title] = f
  1905. }
  1906. return m
  1907. }
  1908. ref := func(key, remoteKey string, meta ...string) v1alpha1.PushSecretData {
  1909. d := v1alpha1.PushSecretData{Match: v1alpha1.PushSecretMatch{SecretKey: key, RemoteRef: v1alpha1.PushSecretRemoteRef{RemoteKey: remoteKey}}}
  1910. if len(meta) > 0 {
  1911. raw := apiextensionsv1.JSON{Raw: []byte(meta[0])}
  1912. d.Metadata = &raw
  1913. }
  1914. return d
  1915. }
  1916. secret := func(kv ...string) *corev1.Secret {
  1917. s := &corev1.Secret{Data: map[string][]byte{}}
  1918. for i := 0; i+1 < len(kv); i += 2 {
  1919. s.Data[kv[i]] = []byte(kv[i+1])
  1920. }
  1921. return s
  1922. }
  1923. t.Run("creates new item with all secret keys as concealed fields", func(t *testing.T) {
  1924. fl := newLister()
  1925. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1926. require.NoError(t, p.PushSecret(t.Context(), secret("alpha", "val-alpha", "beta", "val-beta"), ref("", "my-item")))
  1927. require.True(t, fl.createCalled)
  1928. assert.False(t, fl.putCalled)
  1929. fm := fieldsMap(fl.createdParams.Fields)
  1930. assert.Equal(t, "val-alpha", fm["alpha"].Value)
  1931. assert.Equal(t, onepassword.ItemFieldTypeConcealed, fm["alpha"].FieldType)
  1932. assert.Equal(t, "val-beta", fm["beta"].Value)
  1933. })
  1934. t.Run("updates existing item with all secret keys", func(t *testing.T) {
  1935. fl := newLister(onepassword.Item{ID: "item-id", Title: testExistingItem, VaultID: "vault-id"})
  1936. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1937. require.NoError(t, p.PushSecret(t.Context(), secret("key1", "value1", "key2", "value2"), ref("", testExistingItem)))
  1938. assert.False(t, fl.createCalled)
  1939. require.True(t, fl.putCalled)
  1940. fm := fieldsMap(fl.putItem.Fields)
  1941. assert.Equal(t, "value1", fm["key1"].Value)
  1942. assert.Equal(t, "value2", fm["key2"].Value)
  1943. })
  1944. t.Run("applies tags from metadata on create", func(t *testing.T) {
  1945. fl := newLister()
  1946. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1947. meta := `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"tags":["env:prod","team:backend"]}}`
  1948. require.NoError(t, p.PushSecret(t.Context(), secret("k", "v"), ref("", "tagged-item", meta)))
  1949. require.True(t, fl.createCalled)
  1950. assert.Equal(t, []string{"env:prod", "team:backend"}, fl.createdParams.Tags)
  1951. })
  1952. t.Run("removes fields deleted from the secret", func(t *testing.T) {
  1953. fl := newLister(existingItem) // existingItem has field testOldKey
  1954. p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
  1955. // secret no longer contains testOldKey, only "new-key"
  1956. require.NoError(t, p.PushSecret(t.Context(), secret("new-key", "new-val"), ref("", testExistingItem)))
  1957. require.True(t, fl.putCalled)
  1958. fm := fieldsMap(fl.putItem.Fields)
  1959. assert.Equal(t, "new-val", fm["new-key"].Value, "new field must be added")
  1960. _, stillThere := fm[testOldKey]
  1961. assert.False(t, stillThere, "deleted key must be removed from the 1Password item")
  1962. })
  1963. }