client_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  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. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  22. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  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 esv1.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. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  66. return generateRecords()[:1], 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 secret with multiple matches by Name",
  81. fields: fields{
  82. ksmClient: &fake.MockKeeperClient{
  83. DeleteSecretsFn: func(recrecordUids []string) (map[string]string, error) {
  84. return map[string]string{
  85. record0: record0,
  86. }, nil
  87. },
  88. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  89. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  90. },
  91. },
  92. folderID: folderID,
  93. },
  94. args: args{
  95. context.Background(),
  96. &v1alpha1.PushSecretRemoteRef{
  97. RemoteKey: validExistingRecord,
  98. },
  99. },
  100. wantErr: true,
  101. },
  102. {
  103. name: "Delete non existing secret",
  104. fields: fields{
  105. ksmClient: &fake.MockKeeperClient{
  106. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  107. return nil, errors.New("failed")
  108. },
  109. },
  110. folderID: folderID,
  111. },
  112. args: args{
  113. context.Background(),
  114. &v1alpha1.PushSecretRemoteRef{
  115. RemoteKey: invalidRecord,
  116. },
  117. },
  118. wantErr: true,
  119. },
  120. }
  121. for _, tt := range tests {
  122. t.Run(tt.name, func(t *testing.T) {
  123. c := &Client{
  124. ksmClient: tt.fields.ksmClient,
  125. folderID: tt.fields.folderID,
  126. }
  127. if err := c.DeleteSecret(tt.args.ctx, tt.args.remoteRef); (err != nil) != tt.wantErr {
  128. t.Errorf("DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
  129. }
  130. })
  131. }
  132. }
  133. func TestClientGetAllSecrets(t *testing.T) {
  134. type fields struct {
  135. ksmClient SecurityClient
  136. folderID string
  137. }
  138. type args struct {
  139. ctx context.Context
  140. ref esv1.ExternalSecretFind
  141. }
  142. var path = "path_to_fail"
  143. tests := []struct {
  144. name string
  145. fields fields
  146. args args
  147. want map[string][]byte
  148. wantErr bool
  149. }{
  150. {
  151. name: "Tags not Implemented",
  152. fields: fields{
  153. ksmClient: &fake.MockKeeperClient{},
  154. folderID: folderID,
  155. },
  156. args: args{
  157. ctx: context.Background(),
  158. ref: esv1.ExternalSecretFind{
  159. Tags: map[string]string{
  160. "xxx": "yyy",
  161. },
  162. },
  163. },
  164. wantErr: true,
  165. },
  166. {
  167. name: "Path not Implemented",
  168. fields: fields{
  169. ksmClient: &fake.MockKeeperClient{},
  170. folderID: folderID,
  171. },
  172. args: args{
  173. ctx: context.Background(),
  174. ref: esv1.ExternalSecretFind{
  175. Path: &path,
  176. },
  177. },
  178. wantErr: true,
  179. },
  180. {
  181. name: "Get secrets with matching regex",
  182. fields: fields{
  183. ksmClient: &fake.MockKeeperClient{
  184. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  185. return generateRecords(), nil
  186. },
  187. },
  188. folderID: folderID,
  189. },
  190. args: args{
  191. ctx: context.Background(),
  192. ref: esv1.ExternalSecretFind{
  193. Name: &esv1.FindName{
  194. RegExp: "record",
  195. },
  196. },
  197. },
  198. want: map[string][]byte{
  199. record0: []byte(outputRecord0),
  200. record1: []byte(outputRecord1),
  201. record2: []byte(outputRecord2),
  202. },
  203. wantErr: false,
  204. },
  205. {
  206. name: "Get 1 secret with matching regex",
  207. fields: fields{
  208. ksmClient: &fake.MockKeeperClient{
  209. GetSecretsFn: func(strings []string) ([]*ksm.Record, error) {
  210. return generateRecords(), nil
  211. },
  212. },
  213. folderID: folderID,
  214. },
  215. args: args{
  216. ctx: context.Background(),
  217. ref: esv1.ExternalSecretFind{
  218. Name: &esv1.FindName{
  219. RegExp: record0,
  220. },
  221. },
  222. },
  223. want: map[string][]byte{
  224. record0: []byte(outputRecord0),
  225. },
  226. wantErr: false,
  227. },
  228. }
  229. for _, tt := range tests {
  230. t.Run(tt.name, func(t *testing.T) {
  231. c := &Client{
  232. ksmClient: tt.fields.ksmClient,
  233. folderID: tt.fields.folderID,
  234. }
  235. got, err := c.GetAllSecrets(tt.args.ctx, tt.args.ref)
  236. if (err != nil) != tt.wantErr {
  237. t.Errorf("GetAllSecrets() error = %v, wantErr %v", err, tt.wantErr)
  238. return
  239. }
  240. if !reflect.DeepEqual(got, tt.want) {
  241. t.Errorf("GetAllSecrets() got = %v, want %v", got, tt.want)
  242. }
  243. })
  244. }
  245. }
  246. func TestClientGetSecret(t *testing.T) {
  247. type fields struct {
  248. ksmClient SecurityClient
  249. folderID string
  250. }
  251. type args struct {
  252. ctx context.Context
  253. ref esv1.ExternalSecretDataRemoteRef
  254. }
  255. tests := []struct {
  256. name string
  257. fields fields
  258. args args
  259. want []byte
  260. wantErr bool
  261. }{
  262. {
  263. name: "Get Secret with a property",
  264. fields: fields{
  265. ksmClient: &fake.MockKeeperClient{
  266. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  267. return []*ksm.Record{generateRecords()[0]}, nil
  268. },
  269. },
  270. folderID: folderID,
  271. },
  272. args: args{
  273. ctx: context.Background(),
  274. ref: esv1.ExternalSecretDataRemoteRef{
  275. Key: record0,
  276. Property: LoginKey,
  277. },
  278. },
  279. want: []byte("foo"),
  280. wantErr: false,
  281. },
  282. {
  283. name: "Get Secret without property",
  284. fields: fields{
  285. ksmClient: &fake.MockKeeperClient{
  286. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  287. return []*ksm.Record{generateRecords()[0]}, nil
  288. },
  289. },
  290. folderID: folderID,
  291. },
  292. args: args{
  293. ctx: context.Background(),
  294. ref: esv1.ExternalSecretDataRemoteRef{
  295. Key: record0,
  296. },
  297. },
  298. want: []byte(outputRecord0),
  299. wantErr: false,
  300. },
  301. {
  302. name: "Get secret with multiple matches by ID",
  303. fields: fields{
  304. ksmClient: &fake.MockKeeperClient{
  305. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  306. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  307. },
  308. },
  309. folderID: folderID,
  310. },
  311. args: args{
  312. ctx: context.Background(),
  313. ref: esv1.ExternalSecretDataRemoteRef{
  314. Key: record0,
  315. },
  316. },
  317. want: []byte(outputRecord0),
  318. wantErr: false,
  319. },
  320. {
  321. name: "Get non existing secret",
  322. fields: fields{
  323. ksmClient: &fake.MockKeeperClient{
  324. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  325. return nil, errors.New("not found")
  326. },
  327. },
  328. folderID: folderID,
  329. },
  330. args: args{
  331. ctx: context.Background(),
  332. ref: esv1.ExternalSecretDataRemoteRef{
  333. Key: "record5",
  334. },
  335. },
  336. wantErr: true,
  337. },
  338. {
  339. name: "Get valid secret with non existing property",
  340. fields: fields{
  341. ksmClient: &fake.MockKeeperClient{
  342. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  343. return []*ksm.Record{generateRecords()[0]}, nil
  344. },
  345. },
  346. folderID: folderID,
  347. },
  348. args: args{
  349. ctx: context.Background(),
  350. ref: esv1.ExternalSecretDataRemoteRef{
  351. Key: record0,
  352. Property: "invalid",
  353. },
  354. },
  355. wantErr: true,
  356. },
  357. }
  358. for _, tt := range tests {
  359. t.Run(tt.name, func(t *testing.T) {
  360. c := &Client{
  361. ksmClient: tt.fields.ksmClient,
  362. folderID: tt.fields.folderID,
  363. }
  364. got, err := c.GetSecret(tt.args.ctx, tt.args.ref)
  365. if (err != nil) != tt.wantErr {
  366. t.Errorf("GetSecret() error = %v, wantErr %v", err, tt.wantErr)
  367. return
  368. }
  369. if !reflect.DeepEqual(got, tt.want) {
  370. t.Errorf("GetSecret() got = %v, want %v", got, tt.want)
  371. }
  372. })
  373. }
  374. }
  375. func TestClientGetSecretMap(t *testing.T) {
  376. type fields struct {
  377. ksmClient SecurityClient
  378. folderID string
  379. }
  380. type args struct {
  381. ctx context.Context
  382. ref esv1.ExternalSecretDataRemoteRef
  383. }
  384. tests := []struct {
  385. name string
  386. fields fields
  387. args args
  388. want map[string][]byte
  389. wantErr bool
  390. }{
  391. {
  392. name: "Get Secret with valid property",
  393. fields: fields{
  394. ksmClient: &fake.MockKeeperClient{
  395. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  396. return []*ksm.Record{generateRecords()[0]}, nil
  397. },
  398. },
  399. folderID: folderID,
  400. },
  401. args: args{
  402. ctx: context.Background(),
  403. ref: esv1.ExternalSecretDataRemoteRef{
  404. Key: record0,
  405. Property: LoginKey,
  406. },
  407. },
  408. want: map[string][]byte{
  409. LoginKey: []byte("foo"),
  410. },
  411. wantErr: false,
  412. },
  413. {
  414. name: "Get Secret without property",
  415. fields: fields{
  416. ksmClient: &fake.MockKeeperClient{
  417. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  418. return []*ksm.Record{generateRecords()[0]}, nil
  419. },
  420. },
  421. folderID: folderID,
  422. },
  423. args: args{
  424. ctx: context.Background(),
  425. ref: esv1.ExternalSecretDataRemoteRef{
  426. Key: record0,
  427. },
  428. },
  429. want: map[string][]byte{
  430. LoginKey: []byte("foo"),
  431. PasswordKey: []byte("bar"),
  432. fmt.Sprintf(HostKeyFormat, 0): []byte("{\"hostName\":\"mysql\",\"port\":\"3306\"}"),
  433. },
  434. wantErr: false,
  435. },
  436. {
  437. name: "Get non existing secret",
  438. fields: fields{
  439. ksmClient: &fake.MockKeeperClient{
  440. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  441. return nil, errors.New("not found")
  442. },
  443. },
  444. folderID: folderID,
  445. },
  446. args: args{
  447. ctx: context.Background(),
  448. ref: esv1.ExternalSecretDataRemoteRef{
  449. Key: "record5",
  450. },
  451. },
  452. wantErr: true,
  453. },
  454. {
  455. name: "Get Secret with invalid property",
  456. fields: fields{
  457. ksmClient: &fake.MockKeeperClient{
  458. GetSecretsFn: func(filter []string) ([]*ksm.Record, error) {
  459. return []*ksm.Record{generateRecords()[0]}, nil
  460. },
  461. },
  462. folderID: folderID,
  463. },
  464. args: args{
  465. ctx: context.Background(),
  466. ref: esv1.ExternalSecretDataRemoteRef{
  467. Key: record0,
  468. Property: "invalid",
  469. },
  470. },
  471. wantErr: true,
  472. },
  473. }
  474. for _, tt := range tests {
  475. t.Run(tt.name, func(t *testing.T) {
  476. c := &Client{
  477. ksmClient: tt.fields.ksmClient,
  478. folderID: tt.fields.folderID,
  479. }
  480. got, err := c.GetSecretMap(tt.args.ctx, tt.args.ref)
  481. if (err != nil) != tt.wantErr {
  482. t.Errorf("GetSecretMap() error = %v, wantErr %v", err, tt.wantErr)
  483. return
  484. }
  485. if !reflect.DeepEqual(got, tt.want) {
  486. t.Errorf("GetSecretMap() got = %v, want %v", got, tt.want)
  487. }
  488. })
  489. }
  490. }
  491. func TestClientPushSecret(t *testing.T) {
  492. secretKey := "secret-key"
  493. type fields struct {
  494. ksmClient SecurityClient
  495. folderID string
  496. }
  497. type args struct {
  498. value []byte
  499. data testingfake.PushSecretData
  500. }
  501. tests := []struct {
  502. name string
  503. fields fields
  504. args args
  505. wantErr bool
  506. }{
  507. {
  508. name: "Invalid remote ref",
  509. fields: fields{
  510. ksmClient: &fake.MockKeeperClient{},
  511. folderID: folderID,
  512. },
  513. args: args{
  514. data: testingfake.PushSecretData{
  515. SecretKey: secretKey,
  516. RemoteKey: record0,
  517. },
  518. value: []byte("foo"),
  519. },
  520. wantErr: true,
  521. },
  522. {
  523. name: "Push new valid secret",
  524. fields: fields{
  525. ksmClient: &fake.MockKeeperClient{
  526. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  527. return generateRecords()[0:0], nil
  528. },
  529. CreateSecretWithRecordDataFn: func(recUID, folderUid string, recordData *ksm.RecordCreate) (string, error) {
  530. return "record5", nil
  531. },
  532. },
  533. folderID: folderID,
  534. },
  535. args: args{
  536. data: testingfake.PushSecretData{
  537. SecretKey: secretKey,
  538. RemoteKey: invalidRecord,
  539. },
  540. value: []byte("foo"),
  541. },
  542. wantErr: false,
  543. },
  544. {
  545. name: "Push existing valid secret",
  546. fields: fields{
  547. ksmClient: &fake.MockKeeperClient{
  548. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  549. return generateRecords()[0:1], nil
  550. },
  551. SaveFn: func(record *ksm.Record) error {
  552. return nil
  553. },
  554. },
  555. folderID: folderID,
  556. },
  557. args: args{
  558. data: testingfake.PushSecretData{
  559. SecretKey: secretKey,
  560. RemoteKey: validExistingRecord,
  561. },
  562. value: []byte("foo2"),
  563. },
  564. wantErr: false,
  565. },
  566. {
  567. name: "Unable to push new valid secret with multiple matches by Name",
  568. fields: fields{
  569. ksmClient: &fake.MockKeeperClient{
  570. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  571. return []*ksm.Record{generateRecords()[0], generateRecords()[0]}, nil
  572. },
  573. },
  574. folderID: folderID,
  575. },
  576. args: args{
  577. data: testingfake.PushSecretData{
  578. SecretKey: secretKey,
  579. RemoteKey: validExistingRecord,
  580. },
  581. value: []byte("foo"),
  582. },
  583. wantErr: true,
  584. },
  585. {
  586. name: "Unable to push new valid secret",
  587. fields: fields{
  588. ksmClient: &fake.MockKeeperClient{
  589. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  590. return nil, errors.New("NotFound")
  591. },
  592. CreateSecretWithRecordDataFn: func(recUID, folderUID string, recordData *ksm.RecordCreate) (string, error) {
  593. return "", errors.New("Unable to push")
  594. },
  595. },
  596. folderID: folderID,
  597. },
  598. args: args{
  599. data: testingfake.PushSecretData{
  600. SecretKey: secretKey,
  601. RemoteKey: invalidRecord,
  602. },
  603. value: []byte("foo"),
  604. },
  605. wantErr: true,
  606. },
  607. {
  608. name: "Unable to save existing valid secret",
  609. fields: fields{
  610. ksmClient: &fake.MockKeeperClient{
  611. GetSecretByTitleFn: func(recordTitle string) (*ksm.Record, error) {
  612. return generateRecords()[0], nil
  613. },
  614. GetSecretsByTitleFn: func(recordTitle string) (records []*ksm.Record, err error) {
  615. return generateRecords()[0:1], nil
  616. },
  617. SaveFn: func(record *ksm.Record) error {
  618. return errors.New("Unable to save")
  619. },
  620. },
  621. folderID: folderID,
  622. },
  623. args: args{
  624. data: testingfake.PushSecretData{
  625. SecretKey: secretKey,
  626. RemoteKey: validExistingRecord,
  627. },
  628. value: []byte("foo2"),
  629. },
  630. wantErr: true,
  631. },
  632. }
  633. for _, tt := range tests {
  634. t.Run(tt.name, func(t *testing.T) {
  635. c := &Client{
  636. ksmClient: tt.fields.ksmClient,
  637. folderID: tt.fields.folderID,
  638. }
  639. s := &corev1.Secret{Data: map[string][]byte{secretKey: tt.args.value}}
  640. if err := c.PushSecret(context.Background(), s, tt.args.data); (err != nil) != tt.wantErr {
  641. t.Errorf("PushSecret() error = %v, wantErr %v", err, tt.wantErr)
  642. }
  643. })
  644. }
  645. }
  646. func generateRecords() []*ksm.Record {
  647. var records []*ksm.Record
  648. for i := 0; i < 3; i++ {
  649. var record ksm.Record
  650. if i == 0 {
  651. record = ksm.Record{
  652. Uid: fmt.Sprintf(RecordNameFormat, i),
  653. RecordDict: map[string]any{
  654. "type": externalSecretType,
  655. "folderUID": folderID,
  656. },
  657. }
  658. } else {
  659. record = ksm.Record{
  660. Uid: fmt.Sprintf(RecordNameFormat, i),
  661. RecordDict: map[string]any{
  662. "type": LoginType,
  663. "folderUID": folderID,
  664. },
  665. }
  666. }
  667. 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)
  668. record.SetTitle(fmt.Sprintf(RecordNameFormat, i))
  669. record.SetStandardFieldValue(LoginKey, "foo")
  670. record.SetStandardFieldValue(PasswordKey, "bar")
  671. record.RawJson = sec
  672. records = append(records, &record)
  673. }
  674. return records
  675. }