beyondtrustworkloadcredentials_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  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 beyondtrustworkloadcredentialsdynamic
  14. import (
  15. "context"
  16. "errors"
  17. "testing"
  18. "github.com/google/go-cmp/cmp"
  19. corev1 "k8s.io/api/core/v1"
  20. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  21. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  22. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  23. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  24. "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/fake"
  25. btwcutil "github.com/external-secrets/external-secrets/providers/v1/beyondtrustworkloadcredentials/util"
  26. )
  27. type args struct {
  28. jsonSpec *apiextensions.JSON
  29. kube kclient.Client
  30. btsClientFn func(server, token string) (btwcutil.Client, error)
  31. generateMock func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error)
  32. }
  33. type want struct {
  34. val map[string][]byte
  35. partialVal map[string][]byte
  36. err error
  37. }
  38. type testCase struct {
  39. reason string
  40. args args
  41. want want
  42. }
  43. func TestBeyondtrustWorkloadCredentialsDynamicSecretGenerator(t *testing.T) {
  44. namespace := "test-namespace"
  45. cases := map[string]testCase{
  46. "NilSpec": {
  47. reason: "Raise an error with empty spec.",
  48. args: args{
  49. jsonSpec: nil,
  50. },
  51. want: want{
  52. err: errors.New("no config spec provided"),
  53. },
  54. },
  55. "InvalidSpec": {
  56. reason: "Raise an error with invalid spec.",
  57. args: args{
  58. jsonSpec: &apiextensions.JSON{
  59. Raw: []byte(``),
  60. },
  61. },
  62. want: want{
  63. err: errors.New("no beyondtrustworkloadcredentials provider config in spec"),
  64. },
  65. },
  66. "MissingFolderPath": {
  67. reason: "Raise error if path is not provided.",
  68. args: args{
  69. jsonSpec: &apiextensions.JSON{
  70. Raw: []byte(specMissingFolderPath),
  71. },
  72. kube: clientfake.NewClientBuilder().Build(),
  73. },
  74. want: want{
  75. err: errors.New("path is required in spec"),
  76. },
  77. },
  78. "EmptySecretName": {
  79. reason: "Raise error if path ends with slash resulting in empty secret name.",
  80. args: args{
  81. jsonSpec: &apiextensions.JSON{
  82. Raw: []byte(specEmptySecretName),
  83. },
  84. kube: clientfake.NewClientBuilder().Build(),
  85. },
  86. want: want{
  87. err: errors.New("invalid path: missing secret name in \"test/folder/\""),
  88. },
  89. },
  90. "MissingAuth": {
  91. reason: "Raise error if auth is not provided.",
  92. args: args{
  93. jsonSpec: &apiextensions.JSON{
  94. Raw: []byte(specMissingAuth),
  95. },
  96. kube: clientfake.NewClientBuilder().Build(),
  97. },
  98. want: want{
  99. err: errors.New(
  100. "failed to create BeyondtrustWorkloadCredentials client: failed to load credentials: missing or invalid BeyondtrustWorkloadCredentials API Token in BeyondtrustWorkloadCredentials SecretStore",
  101. ),
  102. },
  103. },
  104. "SecretNotFound": {
  105. reason: "Raise error if secret containing api token does not exist.",
  106. args: args{
  107. jsonSpec: &apiextensions.JSON{
  108. Raw: []byte(specSecretNotFound),
  109. },
  110. kube: clientfake.NewClientBuilder().Build(),
  111. },
  112. want: want{
  113. err: errors.New(
  114. "failed to create BeyondtrustWorkloadCredentials client: failed to load credentials: " +
  115. "cannot get Kubernetes secret \"nonexistent-secret\" from namespace \"test-namespace\": " +
  116. "secrets \"nonexistent-secret\" not found",
  117. ),
  118. },
  119. },
  120. "SuccessfulGeneration": {
  121. reason: "Successfully generate dynamic secret.",
  122. args: args{
  123. jsonSpec: &apiextensions.JSON{
  124. Raw: []byte(validDynamicSecretSpec),
  125. },
  126. kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
  127. ObjectMeta: metav1.ObjectMeta{
  128. Name: "beyondtrustworkloadcredentials-token",
  129. Namespace: namespace,
  130. },
  131. Data: map[string][]byte{
  132. "token": []byte("test-token"),
  133. },
  134. }).Build(),
  135. btsClientFn: func(server, token string) (btwcutil.Client, error) {
  136. client := &fake.BeyondtrustWorkloadCredentialsClient{}
  137. client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
  138. return client, nil
  139. },
  140. generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
  141. return &btwcutil.GeneratedSecret{
  142. AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
  143. SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  144. SessionToken: "FwoGZXIvYXdzEBYaD...",
  145. Expiration: "2025-12-08T12:00:00Z",
  146. LeaseID: "aws/creds/example/abc123",
  147. }, nil
  148. },
  149. },
  150. want: want{
  151. val: map[string][]byte{
  152. "accessKeyId": []byte("AKIAIOSFODNN7EXAMPLE"),
  153. "secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
  154. "sessionToken": []byte("FwoGZXIvYXdzEBYaD..."),
  155. "expiration": []byte("2025-12-08T12:00:00Z"),
  156. "leaseId": []byte("aws/creds/example/abc123"),
  157. },
  158. },
  159. },
  160. "SuccessfulGenerationAtRootFolder": {
  161. reason: "Successfully generate dynamic secret at root folder path.",
  162. args: args{
  163. jsonSpec: &apiextensions.JSON{
  164. Raw: []byte(validDynamicSecretSpecNoFolder),
  165. },
  166. kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
  167. ObjectMeta: metav1.ObjectMeta{
  168. Name: "beyondtrustworkloadcredentials-token",
  169. Namespace: namespace,
  170. },
  171. Data: map[string][]byte{
  172. "token": []byte("test-token"),
  173. },
  174. }).Build(),
  175. btsClientFn: func(server, token string) (btwcutil.Client, error) {
  176. client := &fake.BeyondtrustWorkloadCredentialsClient{}
  177. client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
  178. return client, nil
  179. },
  180. generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
  181. if folderPath != nil {
  182. return nil, errors.New("expected nil folder path")
  183. }
  184. // For generic secrets, we'll just return empty fields
  185. return &btwcutil.GeneratedSecret{
  186. AccessKeyID: "admin",
  187. SecretAccessKey: "secret123",
  188. }, nil
  189. },
  190. },
  191. want: want{
  192. partialVal: map[string][]byte{
  193. "accessKeyId": []byte("admin"),
  194. "secretAccessKey": []byte("secret123"),
  195. },
  196. },
  197. },
  198. "GenerationError": {
  199. reason: "Raise error when generation fails.",
  200. args: args{
  201. jsonSpec: &apiextensions.JSON{
  202. Raw: []byte(validDynamicSecretSpecWithFolder),
  203. },
  204. kube: clientfake.NewClientBuilder().WithObjects(&corev1.Secret{
  205. ObjectMeta: metav1.ObjectMeta{
  206. Name: "beyondtrustworkloadcredentials-token",
  207. Namespace: namespace,
  208. },
  209. Data: map[string][]byte{
  210. "token": []byte("test-token"),
  211. },
  212. }).Build(),
  213. btsClientFn: func(server, token string) (btwcutil.Client, error) {
  214. client := &fake.BeyondtrustWorkloadCredentialsClient{}
  215. client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
  216. return client, nil
  217. },
  218. generateMock: func(ctx context.Context, name string, folderPath *string) (*btwcutil.GeneratedSecret, error) {
  219. return nil, errors.New("API error: insufficient permissions")
  220. },
  221. },
  222. want: want{
  223. err: errors.New("unable to generate dynamic secret: API error: insufficient permissions"),
  224. },
  225. },
  226. }
  227. for name, tc := range cases {
  228. t.Run(name, func(t *testing.T) {
  229. // Create client factory function with mock setup
  230. newClientFn := func(server, token string) (btwcutil.Client, error) {
  231. client := &fake.BeyondtrustWorkloadCredentialsClient{}
  232. client.WithValues(context.Background(), nil, nil, nil, nil, nil, nil)
  233. // Set up the generate mock if provided
  234. if tc.args.generateMock != nil {
  235. client.WithGenerateDynamicSecret(tc.args.generateMock)
  236. }
  237. return client, nil
  238. }
  239. if tc.args.btsClientFn != nil {
  240. // If a custom client function is provided, wrap it to add the generate mock
  241. customFn := tc.args.btsClientFn
  242. newClientFn = func(server, token string) (btwcutil.Client, error) {
  243. client, err := customFn(server, token)
  244. if err != nil {
  245. return nil, err
  246. }
  247. // If it's a fake client, set up the generate mock
  248. if fakeClient, ok := client.(*fake.BeyondtrustWorkloadCredentialsClient); ok && tc.args.generateMock != nil {
  249. fakeClient.WithGenerateDynamicSecret(tc.args.generateMock)
  250. }
  251. return client, nil
  252. }
  253. }
  254. // Create generator with injected client factory
  255. gen := &Generator{
  256. NewBeyondtrustWorkloadCredentialsClient: newClientFn,
  257. }
  258. // Call gen.Generate() for all test cases
  259. val, _, err := gen.Generate(context.Background(), tc.args.jsonSpec, tc.args.kube, namespace)
  260. // Assertions
  261. if tc.want.err != nil {
  262. if err != nil {
  263. if diff := cmp.Diff(tc.want.err.Error(), err.Error()); diff != "" {
  264. t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want error, +got error:\n%s", tc.reason, diff)
  265. }
  266. } else {
  267. t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want error, +got val:\n%v", tc.reason, val)
  268. }
  269. } else if tc.want.partialVal != nil {
  270. // Success case: expect no error
  271. if err != nil {
  272. t.Fatalf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): unexpected error: expected nil, got %v", tc.reason, err)
  273. }
  274. for k, v := range tc.want.partialVal {
  275. if diff := cmp.Diff(v, val[k]); diff != "" {
  276. t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...) -> %s: -want partial, +got partial:\n%s", tc.reason, k, diff)
  277. }
  278. }
  279. } else {
  280. // Success case: expect no error
  281. if err != nil {
  282. t.Fatalf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): unexpected error: expected nil, got %v", tc.reason, err)
  283. }
  284. if diff := cmp.Diff(tc.want.val, val); diff != "" {
  285. t.Errorf("\n%s\nbeyondtrustworkloadcredentials.Generate(...): -want val, +got val:\n%s", tc.reason, diff)
  286. }
  287. }
  288. })
  289. }
  290. }
  291. func TestPathParsing(t *testing.T) {
  292. tests := []struct {
  293. name string
  294. input string
  295. wantFolder *string
  296. wantSecretName string
  297. }{
  298. {
  299. name: "Path with folder",
  300. input: "test/subfolder/secret-name",
  301. wantFolder: new("test/subfolder"),
  302. wantSecretName: "secret-name",
  303. },
  304. {
  305. name: "Path without folder",
  306. input: "secret-name",
  307. wantFolder: nil,
  308. wantSecretName: "secret-name",
  309. },
  310. {
  311. name: "Path with single folder",
  312. input: "folder/secret",
  313. wantFolder: new("folder"),
  314. wantSecretName: "secret",
  315. },
  316. }
  317. for _, tt := range tests {
  318. t.Run(tt.name, func(t *testing.T) {
  319. gotFolder, gotSecretName := parsePath(tt.input)
  320. if tt.wantFolder == nil && gotFolder != nil {
  321. t.Errorf("parsePath() gotFolder = %v, want nil", *gotFolder)
  322. } else if tt.wantFolder != nil && gotFolder == nil {
  323. t.Errorf("parsePath() gotFolder = nil, want %v", *tt.wantFolder)
  324. } else if tt.wantFolder != nil && gotFolder != nil && *gotFolder != *tt.wantFolder {
  325. t.Errorf("parsePath() gotFolder = %v, want %v", *gotFolder, *tt.wantFolder)
  326. }
  327. if gotSecretName != tt.wantSecretName {
  328. t.Errorf("parsePath() gotSecretName = %v, want %v", gotSecretName, tt.wantSecretName)
  329. }
  330. })
  331. }
  332. }
  333. func TestConvertToByteMap(t *testing.T) {
  334. tests := []struct {
  335. name string
  336. input *btwcutil.GeneratedSecret
  337. want map[string][]byte
  338. }{
  339. {
  340. name: "Valid string values",
  341. input: &btwcutil.GeneratedSecret{
  342. AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
  343. SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  344. SessionToken: "FwoGZXIvYXdzEBYaD...",
  345. LeaseID: "aws/creds/example/abc123",
  346. Expiration: "2025-12-08T12:00:00Z",
  347. },
  348. want: map[string][]byte{
  349. "accessKeyId": []byte("AKIAIOSFODNN7EXAMPLE"),
  350. "secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
  351. "sessionToken": []byte("FwoGZXIvYXdzEBYaD..."),
  352. "leaseId": []byte("aws/creds/example/abc123"),
  353. "expiration": []byte("2025-12-08T12:00:00Z"),
  354. },
  355. },
  356. {
  357. name: "Empty session token",
  358. input: &btwcutil.GeneratedSecret{
  359. AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
  360. SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
  361. LeaseID: "aws/creds/example/abc123",
  362. Expiration: "2025-12-08T12:00:00Z",
  363. },
  364. want: map[string][]byte{
  365. "accessKeyId": []byte("AKIAIOSFODNN7EXAMPLE"),
  366. "secretAccessKey": []byte("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"),
  367. "leaseId": []byte("aws/creds/example/abc123"),
  368. "expiration": []byte("2025-12-08T12:00:00Z"),
  369. },
  370. },
  371. }
  372. for _, tt := range tests {
  373. t.Run(tt.name, func(t *testing.T) {
  374. got := convertToByteMap(tt.input)
  375. if diff := cmp.Diff(tt.want, got); diff != "" {
  376. t.Errorf("convertToByteMap() mismatch (-want +got):\n%s", diff)
  377. }
  378. })
  379. }
  380. }