client_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package keepersecurity
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "reflect"
  18. "testing"
  19. ksm "github.com/keeper-security/secrets-manager-go/core"
  20. corev1 "k8s.io/api/core/v1"
  21. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  22. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  23. "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity/fake"
  24. testingfake "github.com/external-secrets/external-secrets/pkg/provider/testing/fake"
  25. )
  26. const (
  27. folderID = "a8ekf031k"
  28. validExistingRecord = "record0/login"
  29. invalidRecord = "record5/login"
  30. outputRecord0 = "{\"title\":\"record0\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host0\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  31. outputRecord1 = "{\"title\":\"record1\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host1\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  32. outputRecord2 = "{\"title\":\"record2\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host2\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}],\"files\":null}"
  33. record0 = "record0"
  34. record1 = "record1"
  35. record2 = "record2"
  36. LoginKey = "login"
  37. PasswordKey = "password"
  38. HostKeyFormat = "host%d"
  39. RecordNameFormat = "record%d"
  40. )
  41. func TestClientDeleteSecret(t *testing.T) {
  42. type fields struct {
  43. ksmClient SecurityClient
  44. folderID string
  45. }
  46. type args struct {
  47. ctx context.Context
  48. remoteRef v1beta1.PushSecretRemoteRef
  49. }
  50. tests := []struct {
  51. name string
  52. fields fields
  53. args args
  54. wantErr bool
  55. }{
  56. {
  57. name: "Delete valid secret",
  58. fields: fields{
  59. ksmClient: &fake.MockKeeperClient{
  60. DeleteSecretsFn: func(recrecordUids []string) (map[string]string, error) {
  61. return map[string]string{
  62. record0: record0,
  63. }, nil
  64. },
  65. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  66. return generateRecords()[0], nil
  67. },
  68. },
  69. folderID: folderID,
  70. },
  71. args: args{
  72. context.Background(),
  73. &v1alpha1.PushSecretRemoteRef{
  74. RemoteKey: validExistingRecord,
  75. },
  76. },
  77. wantErr: false,
  78. },
  79. {
  80. name: "Delete invalid secret type",
  81. fields: fields{
  82. ksmClient: &fake.MockKeeperClient{
  83. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  84. return generateRecords()[1], nil
  85. },
  86. },
  87. folderID: folderID,
  88. },
  89. args: args{
  90. context.Background(),
  91. &v1alpha1.PushSecretRemoteRef{
  92. RemoteKey: validExistingRecord,
  93. },
  94. },
  95. wantErr: true,
  96. },
  97. {
  98. name: "Delete non existing secret",
  99. fields: fields{
  100. ksmClient: &fake.MockKeeperClient{
  101. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  102. return nil, errors.New("failed")
  103. },
  104. },
  105. folderID: folderID,
  106. },
  107. args: args{
  108. context.Background(),
  109. &v1alpha1.PushSecretRemoteRef{
  110. RemoteKey: invalidRecord,
  111. },
  112. },
  113. wantErr: true,
  114. },
  115. }
  116. for _, tt := range tests {
  117. t.Run(tt.name, func(t *testing.T) {
  118. c := &Client{
  119. ksmClient: tt.fields.ksmClient,
  120. folderID: tt.fields.folderID,
  121. }
  122. if err := c.DeleteSecret(tt.args.ctx, tt.args.remoteRef); (err != nil) != tt.wantErr {
  123. t.Errorf("DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
  124. }
  125. })
  126. }
  127. }
  128. func TestClientGetAllSecrets(t *testing.T) {
  129. type fields struct {
  130. ksmClient SecurityClient
  131. folderID string
  132. }
  133. type args struct {
  134. ctx context.Context
  135. ref v1beta1.ExternalSecretFind
  136. }
  137. var path = "path_to_fail"
  138. tests := []struct {
  139. name string
  140. fields fields
  141. args args
  142. want map[string][]byte
  143. wantErr bool
  144. }{
  145. {
  146. name: "Tags not Implemented",
  147. fields: fields{
  148. ksmClient: &fake.MockKeeperClient{},
  149. folderID: folderID,
  150. },
  151. args: args{
  152. ctx: context.Background(),
  153. ref: v1beta1.ExternalSecretFind{
  154. Tags: map[string]string{
  155. "xxx": "yyy",
  156. },
  157. },
  158. },
  159. wantErr: true,
  160. },
  161. {
  162. name: "Path not Implemented",
  163. fields: fields{
  164. ksmClient: &fake.MockKeeperClient{},
  165. folderID: folderID,
  166. },
  167. args: args{
  168. ctx: context.Background(),
  169. ref: v1beta1.ExternalSecretFind{
  170. Path: &path,
  171. },
  172. },
  173. wantErr: true,
  174. },
  175. {
  176. name: "Get secrets with matching regex",
  177. fields: fields{
  178. ksmClient: &fake.MockKeeperClient{
  179. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  180. return generateRecords(), nil
  181. },
  182. },
  183. folderID: folderID,
  184. },
  185. args: args{
  186. ctx: context.Background(),
  187. ref: v1beta1.ExternalSecretFind{
  188. Name: &v1beta1.FindName{
  189. RegExp: "record",
  190. },
  191. },
  192. },
  193. want: map[string][]byte{
  194. record0: []byte(outputRecord0),
  195. record1: []byte(outputRecord1),
  196. record2: []byte(outputRecord2),
  197. },
  198. wantErr: false,
  199. },
  200. {
  201. name: "Get 1 secret with matching regex",
  202. fields: fields{
  203. ksmClient: &fake.MockKeeperClient{
  204. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  205. return generateRecords(), nil
  206. },
  207. },
  208. folderID: folderID,
  209. },
  210. args: args{
  211. ctx: context.Background(),
  212. ref: v1beta1.ExternalSecretFind{
  213. Name: &v1beta1.FindName{
  214. RegExp: record0,
  215. },
  216. },
  217. },
  218. want: map[string][]byte{
  219. record0: []byte(outputRecord0),
  220. },
  221. wantErr: false,
  222. },
  223. }
  224. for _, tt := range tests {
  225. t.Run(tt.name, func(t *testing.T) {
  226. c := &Client{
  227. ksmClient: tt.fields.ksmClient,
  228. folderID: tt.fields.folderID,
  229. }
  230. got, err := c.GetAllSecrets(tt.args.ctx, tt.args.ref)
  231. if (err != nil) != tt.wantErr {
  232. t.Errorf("GetAllSecrets() error = %v, wantErr %v", err, tt.wantErr)
  233. return
  234. }
  235. if !reflect.DeepEqual(got, tt.want) {
  236. t.Errorf("GetAllSecrets() got = %v, want %v", got, tt.want)
  237. }
  238. })
  239. }
  240. }
  241. func TestClientGetSecret(t *testing.T) {
  242. type fields struct {
  243. ksmClient SecurityClient
  244. folderID string
  245. }
  246. type args struct {
  247. ctx context.Context
  248. ref v1beta1.ExternalSecretDataRemoteRef
  249. }
  250. tests := []struct {
  251. name string
  252. fields fields
  253. args args
  254. want []byte
  255. wantErr bool
  256. }{
  257. {
  258. name: "Get Secret with a property",
  259. fields: fields{
  260. ksmClient: &fake.MockKeeperClient{
  261. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  262. return []*ksm.Record{generateRecords()[0]}, nil
  263. },
  264. },
  265. folderID: folderID,
  266. },
  267. args: args{
  268. ctx: context.Background(),
  269. ref: v1beta1.ExternalSecretDataRemoteRef{
  270. Key: record0,
  271. Property: LoginKey,
  272. },
  273. },
  274. want: []byte("foo"),
  275. wantErr: false,
  276. },
  277. {
  278. name: "Get Secret without property",
  279. fields: fields{
  280. ksmClient: &fake.MockKeeperClient{
  281. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  282. return []*ksm.Record{generateRecords()[0]}, nil
  283. },
  284. },
  285. folderID: folderID,
  286. },
  287. args: args{
  288. ctx: context.Background(),
  289. ref: v1beta1.ExternalSecretDataRemoteRef{
  290. Key: record0,
  291. },
  292. },
  293. want: []byte(outputRecord0),
  294. wantErr: false,
  295. },
  296. {
  297. name: "Get non existing secret",
  298. fields: fields{
  299. ksmClient: &fake.MockKeeperClient{
  300. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  301. return nil, errors.New("not found")
  302. },
  303. },
  304. folderID: folderID,
  305. },
  306. args: args{
  307. ctx: context.Background(),
  308. ref: v1beta1.ExternalSecretDataRemoteRef{
  309. Key: "record5",
  310. },
  311. },
  312. wantErr: true,
  313. },
  314. {
  315. name: "Get valid secret with non existing property",
  316. fields: fields{
  317. ksmClient: &fake.MockKeeperClient{
  318. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  319. return []*ksm.Record{generateRecords()[0]}, nil
  320. },
  321. },
  322. folderID: folderID,
  323. },
  324. args: args{
  325. ctx: context.Background(),
  326. ref: v1beta1.ExternalSecretDataRemoteRef{
  327. Key: record0,
  328. Property: "invalid",
  329. },
  330. },
  331. wantErr: true,
  332. },
  333. }
  334. for _, tt := range tests {
  335. t.Run(tt.name, func(t *testing.T) {
  336. c := &Client{
  337. ksmClient: tt.fields.ksmClient,
  338. folderID: tt.fields.folderID,
  339. }
  340. got, err := c.GetSecret(tt.args.ctx, tt.args.ref)
  341. if (err != nil) != tt.wantErr {
  342. t.Errorf("GetSecret() error = %v, wantErr %v", err, tt.wantErr)
  343. return
  344. }
  345. if !reflect.DeepEqual(got, tt.want) {
  346. t.Errorf("GetSecret() got = %v, want %v", got, tt.want)
  347. }
  348. })
  349. }
  350. }
  351. func TestClientGetSecretMap(t *testing.T) {
  352. type fields struct {
  353. ksmClient SecurityClient
  354. folderID string
  355. }
  356. type args struct {
  357. ctx context.Context
  358. ref v1beta1.ExternalSecretDataRemoteRef
  359. }
  360. tests := []struct {
  361. name string
  362. fields fields
  363. args args
  364. want map[string][]byte
  365. wantErr bool
  366. }{
  367. {
  368. name: "Get Secret with valid property",
  369. fields: fields{
  370. ksmClient: &fake.MockKeeperClient{
  371. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  372. return []*ksm.Record{generateRecords()[0]}, nil
  373. },
  374. },
  375. folderID: folderID,
  376. },
  377. args: args{
  378. ctx: context.Background(),
  379. ref: v1beta1.ExternalSecretDataRemoteRef{
  380. Key: record0,
  381. Property: LoginKey,
  382. },
  383. },
  384. want: map[string][]byte{
  385. LoginKey: []byte("foo"),
  386. },
  387. wantErr: false,
  388. },
  389. {
  390. name: "Get Secret without property",
  391. fields: fields{
  392. ksmClient: &fake.MockKeeperClient{
  393. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  394. return []*ksm.Record{generateRecords()[0]}, nil
  395. },
  396. },
  397. folderID: folderID,
  398. },
  399. args: args{
  400. ctx: context.Background(),
  401. ref: v1beta1.ExternalSecretDataRemoteRef{
  402. Key: record0,
  403. },
  404. },
  405. want: map[string][]byte{
  406. LoginKey: []byte("foo"),
  407. PasswordKey: []byte("bar"),
  408. fmt.Sprintf(HostKeyFormat, 0): []byte("{\"hostName\":\"mysql\",\"port\":\"3306\"}"),
  409. },
  410. wantErr: false,
  411. },
  412. {
  413. name: "Get non existing secret",
  414. fields: fields{
  415. ksmClient: &fake.MockKeeperClient{
  416. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  417. return nil, errors.New("not found")
  418. },
  419. },
  420. folderID: folderID,
  421. },
  422. args: args{
  423. ctx: context.Background(),
  424. ref: v1beta1.ExternalSecretDataRemoteRef{
  425. Key: "record5",
  426. },
  427. },
  428. wantErr: true,
  429. },
  430. {
  431. name: "Get Secret with invalid property",
  432. fields: fields{
  433. ksmClient: &fake.MockKeeperClient{
  434. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  435. return []*ksm.Record{generateRecords()[0]}, nil
  436. },
  437. },
  438. folderID: folderID,
  439. },
  440. args: args{
  441. ctx: context.Background(),
  442. ref: v1beta1.ExternalSecretDataRemoteRef{
  443. Key: record0,
  444. Property: "invalid",
  445. },
  446. },
  447. wantErr: true,
  448. },
  449. }
  450. for _, tt := range tests {
  451. t.Run(tt.name, func(t *testing.T) {
  452. c := &Client{
  453. ksmClient: tt.fields.ksmClient,
  454. folderID: tt.fields.folderID,
  455. }
  456. got, err := c.GetSecretMap(tt.args.ctx, tt.args.ref)
  457. if (err != nil) != tt.wantErr {
  458. t.Errorf("GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
  459. return
  460. }
  461. if !reflect.DeepEqual(got, tt.want) {
  462. t.Errorf("GetSecretMap() got = %v, want %v", got, tt.want)
  463. }
  464. })
  465. }
  466. }
  467. func TestClientPushSecret(t *testing.T) {
  468. secretKey := "secret-key"
  469. type fields struct {
  470. ksmClient SecurityClient
  471. folderID string
  472. }
  473. type args struct {
  474. value []byte
  475. data testingfake.PushSecretData
  476. }
  477. tests := []struct {
  478. name string
  479. fields fields
  480. args args
  481. wantErr bool
  482. }{
  483. {
  484. name: "Invalid remote ref",
  485. fields: fields{
  486. ksmClient: &fake.MockKeeperClient{},
  487. folderID: folderID,
  488. },
  489. args: args{
  490. data: testingfake.PushSecretData{
  491. SecretKey: secretKey,
  492. RemoteKey: record0,
  493. },
  494. value: []byte("foo"),
  495. },
  496. wantErr: true,
  497. },
  498. {
  499. name: "Push new valid secret",
  500. fields: fields{
  501. ksmClient: &fake.MockKeeperClient{
  502. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  503. return nil, errors.New("NotFound")
  504. },
  505. CreateSecretWithRecordDataFn: func(recUID, folderUid string, recordData *ksm.RecordCreate) (string, error) {
  506. return "record5", nil
  507. },
  508. },
  509. folderID: folderID,
  510. },
  511. args: args{
  512. data: testingfake.PushSecretData{
  513. SecretKey: secretKey,
  514. RemoteKey: invalidRecord,
  515. },
  516. value: []byte("foo"),
  517. },
  518. wantErr: false,
  519. },
  520. {
  521. name: "Push existing valid secret",
  522. fields: fields{
  523. ksmClient: &fake.MockKeeperClient{
  524. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  525. return generateRecords()[0], nil
  526. },
  527. SaveFn: func(record *ksm.Record) error {
  528. return nil
  529. },
  530. },
  531. folderID: folderID,
  532. },
  533. args: args{
  534. data: testingfake.PushSecretData{
  535. SecretKey: secretKey,
  536. RemoteKey: validExistingRecord,
  537. },
  538. value: []byte("foo2"),
  539. },
  540. wantErr: false,
  541. },
  542. {
  543. name: "Push existing invalid secret",
  544. fields: fields{
  545. ksmClient: &fake.MockKeeperClient{
  546. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  547. return generateRecords()[1], nil
  548. },
  549. SaveFn: func(record *ksm.Record) error {
  550. return nil
  551. },
  552. },
  553. folderID: folderID,
  554. },
  555. args: args{
  556. data: testingfake.PushSecretData{
  557. SecretKey: secretKey,
  558. RemoteKey: validExistingRecord,
  559. },
  560. value: []byte("foo2"),
  561. },
  562. wantErr: true,
  563. },
  564. {
  565. name: "Unable to push new valid secret",
  566. fields: fields{
  567. ksmClient: &fake.MockKeeperClient{
  568. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  569. return nil, errors.New("NotFound")
  570. },
  571. CreateSecretWithRecordDataFn: func(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error) {
  572. return "", errors.New("Unable to push")
  573. },
  574. },
  575. folderID: folderID,
  576. },
  577. args: args{
  578. data: testingfake.PushSecretData{
  579. SecretKey: secretKey,
  580. RemoteKey: invalidRecord,
  581. },
  582. value: []byte("foo"),
  583. },
  584. wantErr: true,
  585. },
  586. {
  587. name: "Unable to save existing valid secret",
  588. fields: fields{
  589. ksmClient: &fake.MockKeeperClient{
  590. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  591. return generateRecords()[0], nil
  592. },
  593. SaveFn: func(record *ksm.Record) error {
  594. return errors.New("Unable to save")
  595. },
  596. },
  597. folderID: folderID,
  598. },
  599. args: args{
  600. data: testingfake.PushSecretData{
  601. SecretKey: secretKey,
  602. RemoteKey: validExistingRecord,
  603. },
  604. value: []byte("foo2"),
  605. },
  606. wantErr: true,
  607. },
  608. }
  609. for _, tt := range tests {
  610. t.Run(tt.name, func(t *testing.T) {
  611. c := &Client{
  612. ksmClient: tt.fields.ksmClient,
  613. folderID: tt.fields.folderID,
  614. }
  615. s := &corev1.Secret{Data: map[string][]byte{secretKey: tt.args.value}}
  616. if err := c.PushSecret(context.Background(), s, tt.args.data); (err != nil) != tt.wantErr {
  617. t.Errorf("PushSecret() error = %v, wantErr %v", err, tt.wantErr)
  618. }
  619. })
  620. }
  621. }
  622. func generateRecords() []*ksm.Record {
  623. var records []*ksm.Record
  624. for i := 0; i < 3; i++ {
  625. var record ksm.Record
  626. if i == 0 {
  627. record = ksm.Record{
  628. Uid: fmt.Sprintf(RecordNameFormat, i),
  629. RecordDict: map[string]interface{}{
  630. "type": externalSecretType,
  631. "folderUID": folderID,
  632. },
  633. }
  634. } else {
  635. record = ksm.Record{
  636. Uid: fmt.Sprintf(RecordNameFormat, i),
  637. RecordDict: map[string]interface{}{
  638. "type": LoginType,
  639. "folderUID": folderID,
  640. },
  641. }
  642. }
  643. sec := fmt.Sprintf("{\"title\":\"record%d\",\"type\":\"login\",\"fields\":[{\"type\":\"login\",\"value\":[\"foo\"]},{\"type\":\"password\",\"value\":[\"bar\"]}],\"custom\":[{\"type\":\"host\",\"label\":\"host%d\",\"value\":[{\"hostName\":\"mysql\",\"port\":\"3306\"}]}]}", i, i)
  644. record.SetTitle(fmt.Sprintf(RecordNameFormat, i))
  645. record.SetStandardFieldValue(LoginKey, "foo")
  646. record.SetStandardFieldValue(PasswordKey, "bar")
  647. record.RawJson = sec
  648. records = append(records, &record)
  649. }
  650. return records
  651. }