client_test.go 71 KB

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