provider_test.go 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055
  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 beyondtrust
  14. import (
  15. "context"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "net/http/httptest"
  21. "testing"
  22. "time"
  23. "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication"
  24. "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging"
  25. "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils"
  26. "github.com/cenkalti/backoff/v4"
  27. "github.com/stretchr/testify/assert"
  28. "github.com/stretchr/testify/require"
  29. "go.uber.org/zap"
  30. v1 "k8s.io/api/core/v1"
  31. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  32. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  33. "k8s.io/client-go/tools/clientcmd"
  34. clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
  35. kubeclient "sigs.k8s.io/controller-runtime/pkg/client"
  36. "sigs.k8s.io/controller-runtime/pkg/client/fake"
  37. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  38. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  39. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  40. )
  41. const (
  42. errTestCase = "Test case Failed"
  43. fakeAPIURL = "https://example.com:443/BeyondTrust/api/public/v3/"
  44. apiKey = "fakeapikey00fakeapikeydd0000000000065b010f20fakeapikey0000000008700000a93fb5d74fddc0000000000000000000000000000000000000;runas=test_user"
  45. clientID = "12345678-25fg-4b05-9ced-35e7dd5093ae"
  46. clientSecret = "12345678-25fg-4b05-9ced-35e7dd5093ae"
  47. authConnectTokenPath = "/Auth/connect/token"
  48. authSignAppInPath = "/Auth/SignAppIn"
  49. secretsSafeFoldersPath = "/secrets-safe/folders/"
  50. secretsSafeSecretsPath = "/secrets-safe/secrets"
  51. folderSecretsPath = "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets"
  52. apiVersion32 = "3.2"
  53. fakeClientSecret = "fake_client_secret"
  54. passwordKey = "password"
  55. testCredentialKey = "test-credential"
  56. testName = "test"
  57. )
  58. func createMockPasswordSafeClient(t *testing.T) kubeclient.Client {
  59. server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  60. switch r.URL.Path {
  61. case authSignAppInPath:
  62. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"fake@beyondtrust.com"}`))
  63. if err != nil {
  64. t.Error(errTestCase)
  65. }
  66. case "/Auth/Signout":
  67. _, err := w.Write([]byte(``))
  68. if err != nil {
  69. t.Error(errTestCase)
  70. }
  71. case secretsSafeSecretsPath:
  72. _, err := w.Write([]byte(`[{"SecretType": "FILE", "Password": "credential_in_sub_3_password","Id": "12345678-07d6-4955-175a-08db047219ce","Title": "credential_in_sub_3"}]`))
  73. if err != nil {
  74. t.Error(errTestCase)
  75. }
  76. case "/secrets-safe/secrets/12345678-07d6-4955-175a-08db047219ce/file/download":
  77. _, err := w.Write([]byte(`fake_password`))
  78. if err != nil {
  79. t.Error(errTestCase)
  80. }
  81. default:
  82. http.NotFound(w, r)
  83. }
  84. }))
  85. t.Cleanup(server.Close)
  86. clientConfig := clientcmd.NewDefaultClientConfig(clientcmdapi.Config{
  87. Clusters: map[string]*clientcmdapi.Cluster{
  88. testName: {
  89. Server: server.URL,
  90. },
  91. },
  92. AuthInfos: map[string]*clientcmdapi.AuthInfo{
  93. testName: {
  94. Token: "token",
  95. },
  96. },
  97. Contexts: map[string]*clientcmdapi.Context{
  98. testName: {
  99. Cluster: testName,
  100. AuthInfo: testName,
  101. },
  102. },
  103. CurrentContext: testName,
  104. }, &clientcmd.ConfigOverrides{})
  105. restConfig, err := clientConfig.ClientConfig()
  106. assert.Nil(t, err)
  107. c, err := kubeclient.New(restConfig, kubeclient.Options{})
  108. assert.Nil(t, err)
  109. return c
  110. }
  111. func TestNewClient(t *testing.T) {
  112. type args struct {
  113. store esv1.SecretStore
  114. kube kubeclient.Client
  115. provider esv1.Provider
  116. }
  117. tests := []struct {
  118. name string
  119. nameSpace string
  120. args args
  121. validateErrorNil bool
  122. validateErrorText bool
  123. expectedErrorText string
  124. }{
  125. {
  126. name: "Client ok",
  127. nameSpace: testName,
  128. args: args{
  129. store: esv1.SecretStore{
  130. Spec: esv1.SecretStoreSpec{
  131. Provider: &esv1.SecretStoreProvider{
  132. Beyondtrust: &esv1.BeyondtrustProvider{
  133. Server: &esv1.BeyondtrustServer{
  134. APIURL: fakeAPIURL,
  135. RetrievalType: secretTypeSecret,
  136. },
  137. Auth: &esv1.BeyondtrustAuth{
  138. ClientID: &esv1.BeyondTrustProviderSecretRef{
  139. Value: clientID,
  140. },
  141. ClientSecret: &esv1.BeyondTrustProviderSecretRef{
  142. Value: clientSecret,
  143. },
  144. },
  145. },
  146. },
  147. },
  148. },
  149. kube: createMockPasswordSafeClient(t),
  150. provider: &Provider{},
  151. },
  152. validateErrorNil: true,
  153. validateErrorText: false,
  154. },
  155. {
  156. name: "Bad Client Id",
  157. nameSpace: testName,
  158. args: args{
  159. store: esv1.SecretStore{
  160. Spec: esv1.SecretStoreSpec{
  161. Provider: &esv1.SecretStoreProvider{
  162. Beyondtrust: &esv1.BeyondtrustProvider{
  163. Server: &esv1.BeyondtrustServer{
  164. APIURL: fakeAPIURL,
  165. RetrievalType: secretTypeSecret,
  166. },
  167. Auth: &esv1.BeyondtrustAuth{
  168. ClientID: &esv1.BeyondTrustProviderSecretRef{
  169. Value: "6138d050",
  170. },
  171. ClientSecret: &esv1.BeyondTrustProviderSecretRef{
  172. Value: clientSecret,
  173. },
  174. },
  175. },
  176. },
  177. },
  178. },
  179. kube: createMockPasswordSafeClient(t),
  180. provider: &Provider{},
  181. },
  182. validateErrorNil: false,
  183. validateErrorText: true,
  184. expectedErrorText: "error in Inputs: Error in field ClientId : min / 36.",
  185. },
  186. {
  187. name: "Bad Client Secret",
  188. nameSpace: testName,
  189. args: args{
  190. store: esv1.SecretStore{
  191. Spec: esv1.SecretStoreSpec{
  192. Provider: &esv1.SecretStoreProvider{
  193. Beyondtrust: &esv1.BeyondtrustProvider{
  194. Server: &esv1.BeyondtrustServer{
  195. APIURL: fakeAPIURL,
  196. RetrievalType: secretTypeSecret,
  197. },
  198. Auth: &esv1.BeyondtrustAuth{
  199. ClientSecret: &esv1.BeyondTrustProviderSecretRef{
  200. Value: "8i7U0Yulabon8mTc",
  201. },
  202. ClientID: &esv1.BeyondTrustProviderSecretRef{
  203. Value: clientID,
  204. },
  205. },
  206. },
  207. },
  208. },
  209. },
  210. kube: createMockPasswordSafeClient(t),
  211. provider: &Provider{},
  212. },
  213. validateErrorNil: false,
  214. validateErrorText: true,
  215. expectedErrorText: "error in Inputs: Error in field ClientSecret : min / 36.",
  216. },
  217. {
  218. name: "Bad Separator",
  219. nameSpace: testName,
  220. args: args{
  221. store: esv1.SecretStore{
  222. Spec: esv1.SecretStoreSpec{
  223. Provider: &esv1.SecretStoreProvider{
  224. Beyondtrust: &esv1.BeyondtrustProvider{
  225. Server: &esv1.BeyondtrustServer{
  226. APIURL: fakeAPIURL,
  227. Separator: "//",
  228. RetrievalType: secretTypeSecret,
  229. },
  230. Auth: &esv1.BeyondtrustAuth{
  231. ClientID: &esv1.BeyondTrustProviderSecretRef{
  232. Value: clientID,
  233. },
  234. ClientSecret: &esv1.BeyondTrustProviderSecretRef{
  235. Value: clientSecret,
  236. },
  237. },
  238. },
  239. },
  240. },
  241. },
  242. kube: createMockPasswordSafeClient(t),
  243. provider: &Provider{},
  244. },
  245. validateErrorNil: false,
  246. validateErrorText: true,
  247. expectedErrorText: "error in Inputs: Error in field ClientId : min / 36.",
  248. },
  249. {
  250. name: "Time Out",
  251. nameSpace: testName,
  252. args: args{
  253. store: esv1.SecretStore{
  254. Spec: esv1.SecretStoreSpec{
  255. Provider: &esv1.SecretStoreProvider{
  256. Beyondtrust: &esv1.BeyondtrustProvider{
  257. Server: &esv1.BeyondtrustServer{
  258. APIURL: fakeAPIURL,
  259. Separator: "/",
  260. ClientTimeOutSeconds: 400,
  261. RetrievalType: secretTypeSecret,
  262. },
  263. Auth: &esv1.BeyondtrustAuth{
  264. ClientID: &esv1.BeyondTrustProviderSecretRef{
  265. Value: clientID,
  266. },
  267. ClientSecret: &esv1.BeyondTrustProviderSecretRef{
  268. Value: clientSecret,
  269. },
  270. },
  271. },
  272. },
  273. },
  274. },
  275. kube: createMockPasswordSafeClient(t),
  276. provider: &Provider{},
  277. },
  278. validateErrorNil: false,
  279. validateErrorText: true,
  280. expectedErrorText: "error in Inputs: Error in field ClientTimeOutinSeconds : lte / 300.",
  281. },
  282. {
  283. name: "ApiKey ok",
  284. nameSpace: testName,
  285. args: args{
  286. store: esv1.SecretStore{
  287. Spec: esv1.SecretStoreSpec{
  288. Provider: &esv1.SecretStoreProvider{
  289. Beyondtrust: &esv1.BeyondtrustProvider{
  290. Server: &esv1.BeyondtrustServer{
  291. APIURL: fakeAPIURL,
  292. RetrievalType: secretTypeSecret,
  293. },
  294. Auth: &esv1.BeyondtrustAuth{
  295. APIKey: &esv1.BeyondTrustProviderSecretRef{
  296. Value: apiKey,
  297. },
  298. },
  299. },
  300. },
  301. },
  302. },
  303. kube: createMockPasswordSafeClient(t),
  304. provider: &Provider{},
  305. },
  306. validateErrorNil: true,
  307. validateErrorText: false,
  308. },
  309. {
  310. name: "Bad ApiKey",
  311. nameSpace: testName,
  312. args: args{
  313. store: esv1.SecretStore{
  314. Spec: esv1.SecretStoreSpec{
  315. Provider: &esv1.SecretStoreProvider{
  316. Beyondtrust: &esv1.BeyondtrustProvider{
  317. Server: &esv1.BeyondtrustServer{
  318. APIURL: fakeAPIURL,
  319. RetrievalType: secretTypeSecret,
  320. },
  321. Auth: &esv1.BeyondtrustAuth{
  322. APIKey: &esv1.BeyondTrustProviderSecretRef{
  323. Value: "bad_api_key",
  324. },
  325. },
  326. },
  327. },
  328. },
  329. },
  330. kube: createMockPasswordSafeClient(t),
  331. provider: &Provider{},
  332. },
  333. validateErrorNil: false,
  334. validateErrorText: true,
  335. expectedErrorText: "error in Inputs: Error in field ApiKey : min / 128.",
  336. },
  337. }
  338. for _, tt := range tests {
  339. t.Run(tt.name, func(t *testing.T) {
  340. _, err := tt.args.provider.NewClient(context.Background(), &tt.args.store, tt.args.kube, tt.nameSpace)
  341. if err != nil && tt.validateErrorNil {
  342. t.Errorf("ProviderBeyondtrust.NewClient() error = %v", err)
  343. }
  344. if err != nil && tt.validateErrorText {
  345. assert.Equal(t, err.Error(), tt.expectedErrorText)
  346. }
  347. })
  348. }
  349. }
  350. func TestLoadConfigSecret_NamespacedStoreCannotCrossNamespace(t *testing.T) {
  351. kube := fake.NewClientBuilder().WithObjects(&v1.Secret{
  352. ObjectMeta: metav1.ObjectMeta{
  353. Namespace: "foo",
  354. Name: "creds",
  355. },
  356. Data: map[string][]byte{
  357. "key": []byte("value"),
  358. },
  359. }).Build()
  360. ref := &esv1.BeyondTrustProviderSecretRef{
  361. SecretRef: &esmeta.SecretKeySelector{
  362. Namespace: new("foo"),
  363. Name: "creds",
  364. Key: "key",
  365. },
  366. }
  367. // For a namespaced SecretStore, attempting to read from another namespace must fail.
  368. _, err := loadConfigSecret(t.Context(), ref, kube, "ns2", esv1.SecretStoreKind)
  369. if err == nil {
  370. t.Fatalf("expected error when accessing secret across namespaces with SecretStore, got nil")
  371. }
  372. // For a namespaced SecretStore, attempting to read from the right namespace must not fail.
  373. val, err := loadConfigSecret(t.Context(), ref, kube, "foo", esv1.SecretStoreKind)
  374. if err != nil {
  375. t.Fatalf("expected error when accessing secret across namespaces with SecretStore, got nil")
  376. }
  377. if val != "value" {
  378. t.Fatalf("expected value, got %q", val)
  379. }
  380. }
  381. func TestPushSecret(t *testing.T) {
  382. type testCase struct {
  383. name string
  384. serverHandler http.HandlerFunc
  385. metadata apiextensionsv1.JSON
  386. expectedError bool
  387. // apiVersion lets a case target a specific Password Safe API version.
  388. // Empty string keeps the historical default of 3.1.
  389. apiVersion string
  390. }
  391. tests := []testCase{
  392. {
  393. name: "successfully pushes credential secret",
  394. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  395. switch r.URL.Path {
  396. case authConnectTokenPath:
  397. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  398. if err != nil {
  399. t.Error(err)
  400. }
  401. case authSignAppInPath:
  402. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  403. if err != nil {
  404. t.Error(err)
  405. }
  406. case secretsSafeFoldersPath:
  407. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  408. if err != nil {
  409. t.Error(err)
  410. }
  411. case folderSecretsPath:
  412. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  413. if err != nil {
  414. t.Error(err)
  415. }
  416. default:
  417. http.Error(w, "not found", http.StatusNotFound)
  418. }
  419. },
  420. expectedError: false,
  421. metadata: apiextensionsv1.JSON{
  422. Raw: []byte(`{
  423. "title": "Test Credential",
  424. "username": "admin",
  425. "description": "Test Credential Secret description",
  426. "secret_type": "CREDENTIAL",
  427. "folder_name": "folder1"
  428. }`),
  429. },
  430. },
  431. {
  432. name: "successfully pushes file secret",
  433. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  434. switch r.URL.Path {
  435. case authConnectTokenPath:
  436. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  437. if err != nil {
  438. t.Error(err)
  439. }
  440. case authSignAppInPath:
  441. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  442. if err != nil {
  443. t.Error(err)
  444. }
  445. case secretsSafeFoldersPath:
  446. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  447. if err != nil {
  448. t.Error(err)
  449. }
  450. case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/file":
  451. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  452. if err != nil {
  453. t.Error(err)
  454. }
  455. default:
  456. http.Error(w, "not found", http.StatusNotFound)
  457. }
  458. },
  459. expectedError: false,
  460. metadata: apiextensionsv1.JSON{
  461. Raw: []byte(`{
  462. "title": "Test File Secret",
  463. "username": "admin",
  464. "description": "Test File Secret description",
  465. "secret_type": "FILE",
  466. "folder_name": "folder1",
  467. "file_name": "credentials.txt"
  468. }`),
  469. },
  470. },
  471. {
  472. name: "successfully pushes text secret",
  473. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  474. switch r.URL.Path {
  475. case authConnectTokenPath:
  476. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  477. if err != nil {
  478. t.Error(err)
  479. }
  480. case authSignAppInPath:
  481. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  482. if err != nil {
  483. t.Error(err)
  484. }
  485. case secretsSafeFoldersPath:
  486. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  487. if err != nil {
  488. t.Error(err)
  489. }
  490. case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/text":
  491. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  492. if err != nil {
  493. t.Error(err)
  494. }
  495. default:
  496. http.Error(w, "not found", http.StatusNotFound)
  497. }
  498. },
  499. expectedError: false,
  500. metadata: apiextensionsv1.JSON{
  501. Raw: []byte(`{
  502. "title": "Test Text Secret",
  503. "username": "admin",
  504. "description": "Test File Secret description",
  505. "secret_type": "TEXT",
  506. "folder_name": "folder1"
  507. }`),
  508. },
  509. },
  510. {
  511. name: "successfully pushes credential secret v3.2",
  512. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  513. switch r.URL.Path {
  514. case authConnectTokenPath:
  515. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  516. if err != nil {
  517. t.Error(err)
  518. }
  519. case authSignAppInPath:
  520. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  521. if err != nil {
  522. t.Error(err)
  523. }
  524. case secretsSafeFoldersPath:
  525. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  526. if err != nil {
  527. t.Error(err)
  528. }
  529. case folderSecretsPath:
  530. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  531. if err != nil {
  532. t.Error(err)
  533. }
  534. default:
  535. http.Error(w, "not found", http.StatusNotFound)
  536. }
  537. },
  538. expectedError: false,
  539. apiVersion: apiVersion32,
  540. metadata: apiextensionsv1.JSON{
  541. Raw: []byte(`{
  542. "title": "Test Credential v3.2",
  543. "username": "admin",
  544. "description": "Test Credential Secret description",
  545. "secret_type": "CREDENTIAL",
  546. "folder_name": "folder1"
  547. }`),
  548. },
  549. },
  550. {
  551. name: "successfully pushes file secret v3.2",
  552. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  553. switch r.URL.Path {
  554. case authConnectTokenPath:
  555. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  556. if err != nil {
  557. t.Error(err)
  558. }
  559. case authSignAppInPath:
  560. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  561. if err != nil {
  562. t.Error(err)
  563. }
  564. case secretsSafeFoldersPath:
  565. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  566. if err != nil {
  567. t.Error(err)
  568. }
  569. case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/file":
  570. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  571. if err != nil {
  572. t.Error(err)
  573. }
  574. default:
  575. http.Error(w, "not found", http.StatusNotFound)
  576. }
  577. },
  578. expectedError: false,
  579. apiVersion: apiVersion32,
  580. metadata: apiextensionsv1.JSON{
  581. Raw: []byte(`{
  582. "title": "Test File Secret v3.2",
  583. "username": "admin",
  584. "description": "Test File Secret description",
  585. "secret_type": "FILE",
  586. "folder_name": "folder1",
  587. "file_name": "credentials.txt"
  588. }`),
  589. },
  590. },
  591. {
  592. name: "successfully pushes text secret v3.2",
  593. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  594. switch r.URL.Path {
  595. case authConnectTokenPath:
  596. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  597. if err != nil {
  598. t.Error(err)
  599. }
  600. case authSignAppInPath:
  601. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  602. if err != nil {
  603. t.Error(err)
  604. }
  605. case secretsSafeFoldersPath:
  606. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  607. if err != nil {
  608. t.Error(err)
  609. }
  610. case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/text":
  611. _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`))
  612. if err != nil {
  613. t.Error(err)
  614. }
  615. default:
  616. http.Error(w, "not found", http.StatusNotFound)
  617. }
  618. },
  619. expectedError: false,
  620. apiVersion: apiVersion32,
  621. metadata: apiextensionsv1.JSON{
  622. Raw: []byte(`{
  623. "title": "Test Text Secret v3.2",
  624. "username": "admin",
  625. "description": "Test Text Secret description",
  626. "secret_type": "TEXT",
  627. "folder_name": "folder1"
  628. }`),
  629. },
  630. },
  631. {
  632. name: "successfully pushes text secret - 404 error",
  633. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  634. switch r.URL.Path {
  635. case authConnectTokenPath:
  636. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  637. if err != nil {
  638. t.Error(err)
  639. }
  640. case authSignAppInPath:
  641. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  642. if err != nil {
  643. t.Error(err)
  644. }
  645. case secretsSafeFoldersPath:
  646. _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`))
  647. if err != nil {
  648. t.Error(err)
  649. }
  650. default:
  651. http.Error(w, "not found", http.StatusNotFound)
  652. }
  653. },
  654. expectedError: true,
  655. metadata: apiextensionsv1.JSON{
  656. Raw: []byte(`{
  657. "title": "Test Text Secret",
  658. "username": "admin",
  659. "description": "Test File Secret description",
  660. "secret_type": "TEXT",
  661. "folder_name": "folder1"
  662. }`),
  663. },
  664. },
  665. {
  666. name: "fails authentication",
  667. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  668. http.Error(w, "unauthorized", http.StatusUnauthorized)
  669. },
  670. expectedError: true,
  671. },
  672. }
  673. for _, tt := range tests {
  674. t.Run(tt.name, func(t *testing.T) {
  675. fakeServer := httptest.NewServer(tt.serverHandler)
  676. defer fakeServer.Close()
  677. logger, err := zap.NewDevelopment()
  678. if err != nil {
  679. t.Error(err)
  680. }
  681. zapLogger := logging.NewZapLogger(logger)
  682. clientTimeout := 30
  683. verifyCa := true
  684. retryMaxElapsedTimeMinutes := 2
  685. backoffDefinition := backoff.NewExponentialBackOff()
  686. backoffDefinition.InitialInterval = 1 * time.Second
  687. backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Second
  688. backoffDefinition.RandomizationFactor = 0.5
  689. httpClientObj, err := utils.GetHttpClient(clientTimeout, verifyCa, "", "", zapLogger)
  690. if err != nil {
  691. t.Error(err)
  692. }
  693. apiVersion := tt.apiVersion
  694. if apiVersion == "" {
  695. apiVersion = "3.1"
  696. }
  697. params := authentication.AuthenticationParametersObj{
  698. HTTPClient: *httpClientObj,
  699. BackoffDefinition: backoffDefinition,
  700. EndpointURL: fakeServer.URL,
  701. APIVersion: apiVersion,
  702. ClientID: "fake_clinet_id",
  703. ClientSecret: fakeClientSecret,
  704. Logger: zapLogger,
  705. RetryMaxElapsedTimeSeconds: 30,
  706. }
  707. authObj, err := authentication.Authenticate(params)
  708. require.NoError(t, err)
  709. p := &Provider{authenticate: *authObj}
  710. secret := &v1.Secret{
  711. Data: map[string][]byte{passwordKey: []byte("supersecret")},
  712. }
  713. metadataJSON := &tt.metadata
  714. psd := v1alpha1.PushSecretData{
  715. Match: v1alpha1.PushSecretMatch{
  716. SecretKey: passwordKey,
  717. RemoteRef: v1alpha1.PushSecretRemoteRef{
  718. RemoteKey: testCredentialKey,
  719. },
  720. },
  721. Metadata: metadataJSON,
  722. }
  723. err = p.PushSecret(context.Background(), secret, psd)
  724. if tt.expectedError {
  725. require.Error(t, err)
  726. } else {
  727. require.NoError(t, err)
  728. }
  729. })
  730. }
  731. }
  732. func TestSecretExists(t *testing.T) {
  733. type testCase struct {
  734. name string
  735. serverHandler http.HandlerFunc
  736. expectedExisting bool
  737. }
  738. tests := []testCase{
  739. {
  740. name: "Secret Exists",
  741. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  742. switch r.URL.Path {
  743. case authConnectTokenPath:
  744. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  745. if err != nil {
  746. t.Error(err)
  747. }
  748. case authSignAppInPath:
  749. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  750. if err != nil {
  751. t.Error(err)
  752. }
  753. case secretsSafeSecretsPath:
  754. _, err := w.Write([]byte(`[{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}]`))
  755. if err != nil {
  756. t.Error(err)
  757. }
  758. default:
  759. http.Error(w, "not found", http.StatusNotFound)
  760. }
  761. },
  762. expectedExisting: true,
  763. },
  764. {
  765. name: "Secret does not Exist",
  766. serverHandler: func(w http.ResponseWriter, r *http.Request) {
  767. switch r.URL.Path {
  768. case authConnectTokenPath:
  769. _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`))
  770. if err != nil {
  771. t.Error(err)
  772. }
  773. case authSignAppInPath:
  774. _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`))
  775. if err != nil {
  776. t.Error(err)
  777. }
  778. case secretsSafeSecretsPath:
  779. http.Error(w, "secret was not found", http.StatusNotFound)
  780. default:
  781. http.Error(w, "not found", http.StatusNotFound)
  782. }
  783. },
  784. expectedExisting: false,
  785. },
  786. }
  787. for _, tt := range tests {
  788. t.Run(tt.name, func(t *testing.T) {
  789. fakeServer := httptest.NewServer(tt.serverHandler)
  790. defer fakeServer.Close()
  791. logger, err := zap.NewDevelopment()
  792. if err != nil {
  793. t.Error(err)
  794. }
  795. zapLogger := logging.NewZapLogger(logger)
  796. clientTimeout := 30
  797. verifyCa := true
  798. retryMaxElapsedTimeMinutes := 2
  799. backoffDefinition := backoff.NewExponentialBackOff()
  800. backoffDefinition.InitialInterval = 1 * time.Second
  801. backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeMinutes) * time.Second
  802. backoffDefinition.RandomizationFactor = 0.5
  803. httpClientObj, err := utils.GetHttpClient(clientTimeout, verifyCa, "", "", zapLogger)
  804. if err != nil {
  805. t.Error(err)
  806. }
  807. params := authentication.AuthenticationParametersObj{
  808. HTTPClient: *httpClientObj,
  809. BackoffDefinition: backoffDefinition,
  810. EndpointURL: fakeServer.URL,
  811. APIVersion: "3.1",
  812. ClientID: "fake_clinet_id",
  813. ClientSecret: fakeClientSecret,
  814. Logger: zapLogger,
  815. RetryMaxElapsedTimeSeconds: 30,
  816. }
  817. authObj, err := authentication.Authenticate(params)
  818. require.NoError(t, err)
  819. p := &Provider{authenticate: *authObj}
  820. remoteRef := v1alpha1.PushSecretRemoteRef{
  821. RemoteKey: testCredentialKey,
  822. }
  823. exists, err := p.SecretExists(context.Background(), remoteRef)
  824. if err != nil {
  825. t.Error(err)
  826. }
  827. if tt.expectedExisting {
  828. assert.True(t, exists)
  829. } else {
  830. assert.False(t, exists)
  831. }
  832. })
  833. }
  834. }
  835. // writeBody is a small helper for the mock servers below. It fails the test
  836. // if the response cannot be written, instead of nesting an err-check inside
  837. // every switch case in the handler.
  838. func writeBody(t *testing.T, w http.ResponseWriter, body string) {
  839. t.Helper()
  840. if _, err := w.Write([]byte(body)); err != nil {
  841. t.Error(err)
  842. }
  843. }
  844. // newOwnerFieldsMockServer returns an httptest.Server that satisfies the
  845. // auth + folder lookup + create-secret request flow. The body of the
  846. // create-secret POST is captured into *captured for assertion.
  847. func newOwnerFieldsMockServer(t *testing.T, secretsPath string, captured map[string]any) *httptest.Server {
  848. t.Helper()
  849. return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  850. switch r.URL.Path {
  851. case authConnectTokenPath:
  852. writeBody(t, w, `{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer"}`)
  853. case authSignAppInPath:
  854. writeBody(t, w, `{"UserId":1, "EmailAddress":"test@beyondtrust.com"}`)
  855. case secretsSafeFoldersPath:
  856. writeBody(t, w, `[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}]`)
  857. case secretsPath:
  858. bodyBytes, err := io.ReadAll(r.Body)
  859. require.NoError(t, err)
  860. require.NoError(t, json.Unmarshal(bodyBytes, &captured))
  861. writeBody(t, w, `{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title"}`)
  862. default:
  863. http.Error(w, "not found", http.StatusNotFound)
  864. }
  865. }))
  866. }
  867. // newAuthenticatedProvider builds a Provider authenticated against serverURL
  868. // with the requested API version. Used by tests that need a ready-to-call
  869. // Provider without inlining the full auth/HTTP-client wiring.
  870. func newAuthenticatedProvider(t *testing.T, serverURL, apiVersion string) *Provider {
  871. t.Helper()
  872. logger, err := zap.NewDevelopment()
  873. require.NoError(t, err)
  874. zapLogger := logging.NewZapLogger(logger)
  875. backoffDefinition := backoff.NewExponentialBackOff()
  876. backoffDefinition.InitialInterval = 1 * time.Second
  877. backoffDefinition.MaxElapsedTime = 2 * time.Second
  878. backoffDefinition.RandomizationFactor = 0.5
  879. httpClientObj, err := utils.GetHttpClient(30, true, "", "", zapLogger)
  880. require.NoError(t, err)
  881. authObj, err := authentication.Authenticate(authentication.AuthenticationParametersObj{
  882. HTTPClient: *httpClientObj,
  883. BackoffDefinition: backoffDefinition,
  884. EndpointURL: serverURL,
  885. APIVersion: apiVersion,
  886. ClientID: "fake_client_id",
  887. ClientSecret: fakeClientSecret,
  888. Logger: zapLogger,
  889. RetryMaxElapsedTimeSeconds: 30,
  890. })
  891. require.NoError(t, err)
  892. return &Provider{authenticate: *authObj}
  893. }
  894. func ownerFieldsMetadata(secretType string) apiextensionsv1.JSON {
  895. return apiextensionsv1.JSON{
  896. Raw: fmt.Appendf(nil, `{
  897. "title": "Owner Fields Test",
  898. "username": "admin",
  899. "secret_type": %q,
  900. "folder_name": "folder1",
  901. "owner_type": "User",
  902. "owner_id": 7,
  903. "group_id": 42
  904. }`, secretType),
  905. }
  906. }
  907. // TestPushSecret_OwnerFieldsArePropagated guards the dual-field migration in CreateSecret
  908. // where every secret-type input carries both OwnersByOwnerId and OwnersByGroupId. The SDK
  909. // then narrows down to one shape based on API version: v3.0 → OwnerDetailsOwnerId,
  910. // v3.1/v3.2 → OwnerDetailsGroupId. We assert the request body the SDK actually emitted.
  911. func TestPushSecret_OwnerFieldsArePropagated(t *testing.T) {
  912. type testCase struct {
  913. name string
  914. apiVersion string
  915. secretType string
  916. secretsPath string
  917. assertBody func(t *testing.T, body map[string]any)
  918. }
  919. tests := []testCase{
  920. {
  921. name: "v3.0 propagates OwnersByOwnerId and top-level OwnerId/OwnerType for CREDENTIAL",
  922. apiVersion: "3.0",
  923. secretType: secretTypeCredential,
  924. secretsPath: folderSecretsPath,
  925. assertBody: func(t *testing.T, body map[string]any) {
  926. assert.Equal(t, float64(7), body["OwnerId"], "top-level OwnerId should come from metadata.owner_id")
  927. assert.Equal(t, "User", body["OwnerType"], "top-level OwnerType should come from metadata.owner_type")
  928. owners, ok := body["Owners"].([]any)
  929. require.True(t, ok, "Owners array missing or wrong type")
  930. require.NotEmpty(t, owners, "Owners array empty")
  931. first := owners[0].(map[string]any)
  932. assert.Equal(t, float64(1), first["OwnerId"], "main owner OwnerId should be UserId=1 from sign-in")
  933. assert.Equal(t, "test@beyondtrust.com", first["Email"])
  934. },
  935. },
  936. {
  937. name: "v3.2 propagates OwnersByGroupId for CREDENTIAL",
  938. apiVersion: apiVersion32,
  939. secretType: secretTypeCredential,
  940. secretsPath: folderSecretsPath,
  941. assertBody: func(t *testing.T, body map[string]any) {
  942. _, hasOwnerId := body["OwnerId"]
  943. _, hasOwnerType := body["OwnerType"]
  944. assert.False(t, hasOwnerId, "v3.2 Config32 should not emit top-level OwnerId")
  945. assert.False(t, hasOwnerType, "v3.2 Config32 should not emit top-level OwnerType")
  946. owners, ok := body["Owners"].([]any)
  947. require.True(t, ok, "Owners array missing or wrong type")
  948. require.NotEmpty(t, owners, "Owners array empty")
  949. first := owners[0].(map[string]any)
  950. assert.Equal(t, float64(42), first["GroupId"], "main owner GroupId should come from metadata.group_id")
  951. assert.Equal(t, float64(1), first["UserId"], "main owner UserId should be UserId=1 from sign-in")
  952. assert.Equal(t, "test@beyondtrust.com", first["Email"])
  953. },
  954. },
  955. }
  956. for _, tt := range tests {
  957. t.Run(tt.name, func(t *testing.T) {
  958. capturedBody := make(map[string]any)
  959. fakeServer := newOwnerFieldsMockServer(t, tt.secretsPath, capturedBody)
  960. defer fakeServer.Close()
  961. p := newAuthenticatedProvider(t, fakeServer.URL, tt.apiVersion)
  962. metadata := ownerFieldsMetadata(tt.secretType)
  963. psd := v1alpha1.PushSecretData{
  964. Match: v1alpha1.PushSecretMatch{
  965. SecretKey: passwordKey,
  966. RemoteRef: v1alpha1.PushSecretRemoteRef{RemoteKey: testCredentialKey},
  967. },
  968. Metadata: &metadata,
  969. }
  970. secret := &v1.Secret{Data: map[string][]byte{passwordKey: []byte("supersecret")}}
  971. require.NoError(t, p.PushSecret(context.Background(), secret, psd))
  972. require.NotNil(t, capturedBody, "create-secret endpoint was never hit")
  973. tt.assertBody(t, capturedBody)
  974. })
  975. }
  976. }