client_push_test.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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 conjur
  14. import (
  15. "context"
  16. "testing"
  17. "github.com/cyberark/conjur-api-go/conjurapi"
  18. "github.com/doodlesbykumbi/conjur-policy-go/pkg/conjurpolicy"
  19. "github.com/stretchr/testify/assert"
  20. "gopkg.in/yaml.v3"
  21. corev1 "k8s.io/api/core/v1"
  22. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  23. clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
  24. "github.com/external-secrets/external-secrets/providers/v1/conjur/fake"
  25. "github.com/external-secrets/external-secrets/runtime/esutils"
  26. )
  27. func TestDefaultPolicy(t *testing.T) {
  28. policy, err := conjurPolicy("secret1", []string{"foo", "bar", "baz"})
  29. assert.NoError(t, err)
  30. expected := `
  31. - !policy
  32. id: secret1
  33. body:
  34. - !group
  35. id: delegation/consumers
  36. annotations:
  37. managed-by: "external-secrets"
  38. editable: "true"
  39. - !variable
  40. id: foo
  41. annotations:
  42. managed-by: "external-secrets"
  43. - !variable
  44. id: bar
  45. annotations:
  46. managed-by: "external-secrets"
  47. - !variable
  48. id: baz
  49. annotations:
  50. managed-by: "external-secrets"
  51. - !permit
  52. resource: !variable foo
  53. role: !group delegation/consumers
  54. privileges: [ read, execute ]
  55. - !permit
  56. resource: !variable bar
  57. role: !group delegation/consumers
  58. privileges: [ read, execute ]
  59. - !permit
  60. resource: !variable baz
  61. role: !group delegation/consumers
  62. privileges: [ read, execute ]`
  63. // roundtrip the expected output through a unmarshal/marshal to remove any formatting related issues
  64. p := conjurpolicy.PolicyStatements{}
  65. err = yaml.Unmarshal([]byte(expected), &p)
  66. assert.NoError(t, err)
  67. exp, err := yaml.Marshal(p)
  68. assert.NoError(t, err)
  69. assert.Equal(t, exp, policy)
  70. }
  71. type RemoteRef struct {
  72. RemoteKey string
  73. Property string
  74. SecretKey string
  75. }
  76. func (r RemoteRef) GetRemoteKey() string {
  77. return r.RemoteKey
  78. }
  79. func (r RemoteRef) GetProperty() string {
  80. return r.Property
  81. }
  82. func (r RemoteRef) GetMetadata() *apiextensionsv1.JSON {
  83. return nil
  84. }
  85. func (r RemoteRef) GetSecretKey() string {
  86. return r.SecretKey
  87. }
  88. func TestPushSecret(t *testing.T) {
  89. tests := []struct {
  90. name string
  91. secretValue []byte
  92. remoteRef RemoteRef
  93. expectedPolicy string
  94. expectedVar string
  95. expectedVal string
  96. }{
  97. {
  98. name: "Push specified value to property",
  99. secretValue: []byte("password123"),
  100. remoteRef: RemoteRef{
  101. SecretKey: "password",
  102. RemoteKey: "data/vault/eso/db",
  103. Property: "password",
  104. },
  105. expectedVar: "data/vault/eso/db/password",
  106. expectedVal: "password123",
  107. expectedPolicy: `- !policy
  108. id: db
  109. body:
  110. - !group
  111. id: delegation/consumers
  112. annotations:
  113. editable: "true"
  114. managed-by: external-secrets
  115. - !variable
  116. id: password
  117. annotations:
  118. managed-by: external-secrets
  119. - !permit
  120. role: !group delegation/consumers
  121. privileges: [read, execute]
  122. resource: !variable password
  123. `,
  124. },
  125. {
  126. name: "Push all values to a single property",
  127. secretValue: []byte("password123"),
  128. remoteRef: RemoteRef{
  129. RemoteKey: "data/vault/eso/db",
  130. Property: "password",
  131. },
  132. expectedVar: "data/vault/eso/db/password",
  133. expectedVal: `{"password":"password123"}`,
  134. },
  135. {
  136. name: "Push all values, unspecified property",
  137. secretValue: []byte("password123"),
  138. remoteRef: RemoteRef{
  139. RemoteKey: "data/vault/eso/db",
  140. },
  141. expectedVar: "data/vault/eso/db/password",
  142. expectedVal: "password123",
  143. },
  144. }
  145. for _, tt := range tests {
  146. t.Run(tt.name, func(t *testing.T) {
  147. mockClient := &fake.ConjurMockClient{}
  148. provider := &Client{
  149. store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
  150. kube: clientfake.NewClientBuilder().
  151. WithObjects(makeFakeAPIKeySecrets()...).Build(),
  152. namespace: "default",
  153. client: mockClient,
  154. }
  155. kubeSecret := &corev1.Secret{
  156. Data: map[string][]byte{
  157. "password": tt.secretValue,
  158. },
  159. }
  160. err := provider.PushSecret(context.Background(), kubeSecret, tt.remoteRef)
  161. if err != nil {
  162. t.Fatalf("PushSecret failed: %v", err)
  163. }
  164. if len(mockClient.AddSecretCalls) != 1 {
  165. t.Errorf("expected 1 AddSecret call, got %d", len(mockClient.AddSecretCalls))
  166. } else {
  167. call := mockClient.AddSecretCalls[0]
  168. if call.Variable != tt.expectedVar {
  169. t.Errorf("expected var %s, got %s", tt.expectedVar, call.Variable)
  170. }
  171. if call.Value != tt.expectedVal {
  172. t.Errorf("expected value %s, got %s", tt.expectedVal, call.Value)
  173. }
  174. }
  175. if tt.expectedPolicy != "" {
  176. if len(mockClient.LoadPolicyCalls) == 0 {
  177. t.Error("expected a LoadPolicy call but none occurred")
  178. } else {
  179. policy := mockClient.LoadPolicyCalls[0].Policy
  180. assert.Equal(t, tt.expectedPolicy, policy)
  181. }
  182. }
  183. })
  184. }
  185. }
  186. func TestCheckSecrets(t *testing.T) {
  187. tests := []struct {
  188. name string
  189. conjurSecretName string
  190. conjurVars []string
  191. secretData map[string]string
  192. property string
  193. key string
  194. setupMock func(m *fake.ConjurMockClient)
  195. expectedUpdates []string
  196. }{
  197. {
  198. name: "Unmanaged secret (should skip despite drift)",
  199. conjurSecretName: "db",
  200. conjurVars: []string{"pass"},
  201. secretData: map[string]string{"pass": "new-value"},
  202. setupMock: func(m *fake.ConjurMockClient) {
  203. m.SecretDetails = map[string]*conjurapi.StaticSecretResponse{
  204. "db/pass": {
  205. StaticSecret: conjurapi.StaticSecret{
  206. Annotations: map[string]string{"owner": "manual"},
  207. },
  208. },
  209. }
  210. m.SecretValues = map[string][]byte{"db/pass": []byte("old-value")}
  211. },
  212. expectedUpdates: []string{},
  213. },
  214. {
  215. name: "Managed secret with drift (should update)",
  216. conjurSecretName: "db",
  217. conjurVars: []string{"pass"},
  218. secretData: map[string]string{"pass": "new-value"},
  219. setupMock: func(m *fake.ConjurMockClient) {
  220. m.SecretValues = map[string][]byte{"db/pass": []byte("old-value")}
  221. },
  222. expectedUpdates: []string{"pass"},
  223. },
  224. {
  225. name: "Managed secret, no drift (should skip)",
  226. conjurSecretName: "db",
  227. conjurVars: []string{"pass"},
  228. secretData: map[string]string{"pass": "same-value"},
  229. setupMock: func(m *fake.ConjurMockClient) {
  230. m.SecretValues = map[string][]byte{"db/pass": []byte("same-value")}
  231. },
  232. expectedUpdates: []string{},
  233. },
  234. {
  235. name: "Property + Key: Value differs (should update)",
  236. conjurSecretName: "api",
  237. conjurVars: []string{"token"},
  238. property: "credential",
  239. key: "apikey",
  240. secretData: map[string]string{"apikey": "new-val"},
  241. setupMock: func(m *fake.ConjurMockClient) {
  242. m.SecretValues = map[string][]byte{"api/token": []byte("old-val")}
  243. },
  244. expectedUpdates: []string{"token"},
  245. },
  246. {
  247. name: "Property + Key: Value matches (should skip)",
  248. conjurSecretName: "api",
  249. conjurVars: []string{"token"},
  250. property: "credential",
  251. key: "apikey",
  252. secretData: map[string]string{"apikey": "same-val"},
  253. setupMock: func(m *fake.ConjurMockClient) {
  254. m.SecretValues = map[string][]byte{"api/token": []byte("same-val")}
  255. },
  256. expectedUpdates: []string{},
  257. },
  258. {
  259. name: "Property only (JSON): Matches marshaled data (should skip)",
  260. conjurSecretName: "app",
  261. conjurVars: []string{"config"},
  262. property: "json-blob",
  263. key: "",
  264. secretData: map[string]string{"user": "admin", "port": "8080"},
  265. setupMock: func(m *fake.ConjurMockClient) {
  266. marshaled, _ := esutils.JSONMarshal(map[string]string{"user": "admin", "port": "8080"})
  267. m.SecretValues = map[string][]byte{"app/config": marshaled}
  268. },
  269. expectedUpdates: []string{},
  270. },
  271. {
  272. name: "Property only (JSON): Data differs (should update)",
  273. conjurSecretName: "app",
  274. conjurVars: []string{"config"},
  275. property: "json-blob",
  276. key: "",
  277. secretData: map[string]string{"user": "admin"},
  278. setupMock: func(m *fake.ConjurMockClient) {
  279. m.SecretValues = map[string][]byte{"app/config": []byte(`{"user":"old"}`)}
  280. },
  281. expectedUpdates: []string{"config"},
  282. },
  283. {
  284. name: "Key-only (Flat): Value matches (should skip)",
  285. conjurSecretName: "db",
  286. conjurVars: []string{"username"},
  287. property: "",
  288. secretData: map[string]string{"username": "admin"},
  289. setupMock: func(m *fake.ConjurMockClient) {
  290. m.SecretValues = map[string][]byte{"db/username": []byte("admin")}
  291. },
  292. expectedUpdates: []string{},
  293. },
  294. {
  295. name: "Key-only (Flat): Value differs (should update)",
  296. conjurSecretName: "db",
  297. conjurVars: []string{"username"},
  298. property: "",
  299. secretData: map[string]string{"username": "new-admin"},
  300. setupMock: func(m *fake.ConjurMockClient) {
  301. m.SecretValues = map[string][]byte{"db/username": []byte("old-admin")}
  302. },
  303. expectedUpdates: []string{"username"},
  304. },
  305. }
  306. for _, tt := range tests {
  307. t.Run(tt.name, func(t *testing.T) {
  308. mock := &fake.ConjurMockClient{
  309. SecretDetails: make(map[string]*conjurapi.StaticSecretResponse),
  310. SecretValues: make(map[string][]byte),
  311. }
  312. tt.setupMock(mock)
  313. updates, err := checkSecrets(mock, tt.conjurSecretName, tt.conjurVars, tt.secretData, tt.property, tt.key)
  314. assert.NoError(t, err)
  315. assert.Equal(t, tt.expectedUpdates, updates)
  316. })
  317. }
  318. }