chef.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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 chef
  13. import (
  14. "context"
  15. "encoding/json"
  16. "fmt"
  17. "net/url"
  18. "strings"
  19. "time"
  20. "github.com/go-chef/chef"
  21. "github.com/go-logr/logr"
  22. "github.com/tidwall/gjson"
  23. corev1 "k8s.io/api/core/v1"
  24. "k8s.io/apimachinery/pkg/types"
  25. ctrl "sigs.k8s.io/controller-runtime"
  26. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  27. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  28. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  29. "github.com/external-secrets/external-secrets/pkg/metrics"
  30. "github.com/external-secrets/external-secrets/pkg/utils"
  31. )
  32. const (
  33. errChefStore = "received invalid Chef SecretStore resource: %w"
  34. errMissingStore = "missing store"
  35. errMissingStoreSpec = "missing store spec"
  36. errMissingProvider = "missing provider"
  37. errMissingChefProvider = "missing chef provider"
  38. errMissingUserName = "missing username"
  39. errMissingServerURL = "missing serverurl"
  40. errMissingAuth = "cannot initialize Chef Client: no valid authType was specified"
  41. errMissingSecretKey = "missing Secret Key"
  42. errInvalidClusterStoreMissingPKNamespace = "invalid ClusterSecretStore: missing privateKeySecretRef.Namespace"
  43. errFetchK8sSecret = "could not fetch SecretKey Secret: %w"
  44. errInvalidURL = "invalid serverurl: %w"
  45. errChefClient = "unable to create chef client: %w"
  46. errChefProvider = "missing or invalid spec: %w"
  47. errUninitalizedChefProvider = "chef provider is not initialized"
  48. errNoDatabagItemFound = "data bag item %s not found in data bag %s"
  49. errNoDatabagItemPropertyFound = "property %s not found in data bag item"
  50. errCannotListDataBagItems = "unable to list items in data bag %s, may be given data bag doesn't exists or it is empty"
  51. errUnableToConvertToJSON = "unable to convert databagItem into JSON"
  52. errInvalidFormat = "invalid key format in data section. Expected value 'databagName/databagItemName'"
  53. errStoreValidateFailed = "unable to validate provided store. Check if username, serverUrl and privateKey are correct"
  54. errServerURLNoEndSlash = "serverurl does not end with slash(/)"
  55. errInvalidDataform = "invalid key format in dataForm section. Expected only 'databagName'"
  56. errNotImplemented = "not implemented"
  57. ProviderChef = "Chef"
  58. CallChefGetDataBagItem = "GetDataBagItem"
  59. CallChefListDataBagItems = "ListDataBagItems"
  60. CallChefGetUser = "GetUser"
  61. )
  62. var contextTimeout = time.Second * 25
  63. type DatabagFetcher interface {
  64. GetItem(databagName string, databagItem string) (item chef.DataBagItem, err error)
  65. ListItems(name string) (data *chef.DataBagListResult, err error)
  66. }
  67. type UserInterface interface {
  68. Get(name string) (user chef.User, err error)
  69. }
  70. type Providerchef struct {
  71. clientName string
  72. databagService DatabagFetcher
  73. userService UserInterface
  74. log logr.Logger
  75. }
  76. var _ v1beta1.SecretsClient = &Providerchef{}
  77. var _ v1beta1.Provider = &Providerchef{}
  78. func init() {
  79. v1beta1.Register(&Providerchef{}, &v1beta1.SecretStoreProvider{
  80. Chef: &v1beta1.ChefProvider{},
  81. })
  82. }
  83. func (providerchef *Providerchef) NewClient(ctx context.Context, store v1beta1.GenericStore, kube kclient.Client, namespace string) (v1beta1.SecretsClient, error) {
  84. chefProvider, err := getChefProvider(store)
  85. if err != nil {
  86. return nil, fmt.Errorf(errChefProvider, err)
  87. }
  88. credentialsSecret := &corev1.Secret{}
  89. objectKey := types.NamespacedName{
  90. Name: chefProvider.Auth.SecretRef.SecretKey.Name,
  91. Namespace: namespace,
  92. }
  93. if store.GetObjectKind().GroupVersionKind().Kind == v1beta1.ClusterSecretStoreKind {
  94. if chefProvider.Auth.SecretRef.SecretKey.Namespace == nil {
  95. return nil, fmt.Errorf(errInvalidClusterStoreMissingPKNamespace)
  96. }
  97. objectKey.Namespace = *chefProvider.Auth.SecretRef.SecretKey.Namespace
  98. }
  99. if err := kube.Get(ctx, objectKey, credentialsSecret); err != nil {
  100. return nil, fmt.Errorf(errFetchK8sSecret, err)
  101. }
  102. secretKey := credentialsSecret.Data[chefProvider.Auth.SecretRef.SecretKey.Key]
  103. if len(secretKey) == 0 {
  104. return nil, fmt.Errorf(errMissingSecretKey)
  105. }
  106. client, err := chef.NewClient(&chef.Config{
  107. Name: chefProvider.UserName,
  108. Key: string(secretKey),
  109. BaseURL: chefProvider.ServerURL,
  110. })
  111. if err != nil {
  112. return nil, fmt.Errorf(errChefClient, err)
  113. }
  114. providerchef.clientName = chefProvider.UserName
  115. providerchef.databagService = client.DataBags
  116. providerchef.userService = client.Users
  117. providerchef.log = ctrl.Log.WithName("provider").WithName("chef").WithName("secretsmanager")
  118. return providerchef, nil
  119. }
  120. // Close closes the client connection.
  121. func (providerchef *Providerchef) Close(_ context.Context) error {
  122. return nil
  123. }
  124. // Validate checks if the client is configured correctly
  125. // to be able to retrieve secrets from the provider.
  126. func (providerchef *Providerchef) Validate() (v1beta1.ValidationResult, error) {
  127. _, err := providerchef.userService.Get(providerchef.clientName)
  128. metrics.ObserveAPICall(ProviderChef, CallChefGetUser, err)
  129. if err != nil {
  130. return v1beta1.ValidationResultError, fmt.Errorf(errStoreValidateFailed)
  131. }
  132. return v1beta1.ValidationResultReady, nil
  133. }
  134. // GetAllSecrets Retrieves a map[string][]byte with the Databag names as key and the Databag's Items as secrets.
  135. func (providerchef *Providerchef) GetAllSecrets(_ context.Context, _ v1beta1.ExternalSecretFind) (map[string][]byte, error) {
  136. return nil, fmt.Errorf("dataFrom.find not suppported")
  137. }
  138. // GetSecret returns a databagItem present in the databag. format example: databagName/databagItemName.
  139. func (providerchef *Providerchef) GetSecret(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  140. if utils.IsNil(providerchef.databagService) {
  141. return nil, fmt.Errorf(errUninitalizedChefProvider)
  142. }
  143. key := ref.Key
  144. databagName := ""
  145. databagItem := ""
  146. nameSplitted := strings.Split(key, "/")
  147. if len(nameSplitted) > 1 {
  148. databagName = nameSplitted[0]
  149. databagItem = nameSplitted[1]
  150. }
  151. providerchef.log.Info("fetching secret value", "databag Name:", databagName, "databag Item:", databagItem)
  152. if databagName != "" && databagItem != "" {
  153. return getSingleDatabagItemWithContext(ctx, providerchef, databagName, databagItem, ref.Property)
  154. }
  155. return nil, fmt.Errorf(errInvalidFormat)
  156. }
  157. func getSingleDatabagItemWithContext(ctx context.Context, providerchef *Providerchef, dataBagName, databagItemName, propertyName string) ([]byte, error) {
  158. ctxWithTimeout, cancel := context.WithTimeout(ctx, contextTimeout)
  159. defer cancel()
  160. type result = struct {
  161. values []byte
  162. err error
  163. }
  164. getWithTimeout := func() chan result {
  165. resultChan := make(chan result, 1)
  166. go func() {
  167. defer close(resultChan)
  168. ditem, err := providerchef.databagService.GetItem(dataBagName, databagItemName)
  169. metrics.ObserveAPICall(ProviderChef, CallChefGetDataBagItem, err)
  170. if err != nil {
  171. resultChan <- result{err: fmt.Errorf(errNoDatabagItemFound, databagItemName, dataBagName)}
  172. return
  173. }
  174. jsonByte, err := json.Marshal(ditem)
  175. if err != nil {
  176. resultChan <- result{err: fmt.Errorf(errUnableToConvertToJSON)}
  177. return
  178. }
  179. if propertyName != "" {
  180. propertyValue, err := getPropertyFromDatabagItem(jsonByte, propertyName)
  181. if err != nil {
  182. resultChan <- result{err: err}
  183. return
  184. }
  185. resultChan <- result{values: propertyValue}
  186. } else {
  187. resultChan <- result{values: jsonByte}
  188. }
  189. }()
  190. return resultChan
  191. }
  192. select {
  193. case <-ctxWithTimeout.Done():
  194. return nil, ctxWithTimeout.Err()
  195. case r := <-getWithTimeout():
  196. if r.err != nil {
  197. return nil, r.err
  198. }
  199. return r.values, nil
  200. }
  201. }
  202. /*
  203. A path is a series of keys separated by a dot.
  204. A key may contain special wildcard characters '*' and '?'.
  205. To access an array value use the index as the key.
  206. To get the number of elements in an array or to access a child path, use the '#' character.
  207. The dot and wildcard characters can be escaped with '\'.
  208. refer https://github.com/tidwall/gjson#:~:text=JSON%20byte%20slices.-,Path%20Syntax,-Below%20is%20a
  209. */
  210. func getPropertyFromDatabagItem(jsonByte []byte, propertyName string) ([]byte, error) {
  211. result := gjson.GetBytes(jsonByte, propertyName)
  212. if !result.Exists() {
  213. return nil, fmt.Errorf(errNoDatabagItemPropertyFound, propertyName)
  214. }
  215. return []byte(result.Str), nil
  216. }
  217. // GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.key
  218. // dataFrom.extract.key only accepts dataBagName, example : dataFrom.extract.key: myDatabag
  219. // databagItemName or Property not expected in key.
  220. func (providerchef *Providerchef) GetSecretMap(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  221. if utils.IsNil(providerchef.databagService) {
  222. return nil, fmt.Errorf(errUninitalizedChefProvider)
  223. }
  224. databagName := ref.Key
  225. if strings.Contains(databagName, "/") {
  226. return nil, fmt.Errorf(errInvalidDataform)
  227. }
  228. getAllSecrets := make(map[string][]byte)
  229. providerchef.log.Info("fetching all items from", "databag:", databagName)
  230. dataItems, err := providerchef.databagService.ListItems(databagName)
  231. metrics.ObserveAPICall(ProviderChef, CallChefListDataBagItems, err)
  232. if err != nil {
  233. return nil, fmt.Errorf(errCannotListDataBagItems, databagName)
  234. }
  235. for dataItem := range *dataItems {
  236. dItem, err := getSingleDatabagItemWithContext(ctx, providerchef, databagName, dataItem, "")
  237. if err != nil {
  238. return nil, fmt.Errorf(errNoDatabagItemFound, dataItem, databagName)
  239. }
  240. getAllSecrets[dataItem] = dItem
  241. }
  242. return getAllSecrets, nil
  243. }
  244. // ValidateStore checks if the provided store is valid.
  245. func (providerchef *Providerchef) ValidateStore(store v1beta1.GenericStore) (admission.Warnings, error) {
  246. chefProvider, err := getChefProvider(store)
  247. if err != nil {
  248. return nil, fmt.Errorf(errChefStore, err)
  249. }
  250. // check namespace compared to kind
  251. if err := utils.ValidateSecretSelector(store, chefProvider.Auth.SecretRef.SecretKey); err != nil {
  252. return nil, fmt.Errorf(errChefStore, err)
  253. }
  254. return nil, nil
  255. }
  256. // getChefProvider validates the incoming store and return the chef provider.
  257. func getChefProvider(store v1beta1.GenericStore) (*v1beta1.ChefProvider, error) {
  258. if store == nil {
  259. return nil, fmt.Errorf(errMissingStore)
  260. }
  261. storeSpec := store.GetSpec()
  262. if storeSpec == nil {
  263. return nil, fmt.Errorf(errMissingStoreSpec)
  264. }
  265. provider := storeSpec.Provider
  266. if provider == nil {
  267. return nil, fmt.Errorf(errMissingProvider)
  268. }
  269. chefProvider := storeSpec.Provider.Chef
  270. if chefProvider == nil {
  271. return nil, fmt.Errorf(errMissingChefProvider)
  272. }
  273. if chefProvider.UserName == "" {
  274. return chefProvider, fmt.Errorf(errMissingUserName)
  275. }
  276. if chefProvider.ServerURL == "" {
  277. return chefProvider, fmt.Errorf(errMissingServerURL)
  278. }
  279. if !strings.HasSuffix(chefProvider.ServerURL, "/") {
  280. return chefProvider, fmt.Errorf(errServerURLNoEndSlash)
  281. }
  282. // check valid URL
  283. if _, err := url.ParseRequestURI(chefProvider.ServerURL); err != nil {
  284. return chefProvider, fmt.Errorf(errInvalidURL, err)
  285. }
  286. if chefProvider.Auth == nil {
  287. return chefProvider, fmt.Errorf(errMissingAuth)
  288. }
  289. if chefProvider.Auth.SecretRef.SecretKey.Key == "" {
  290. return chefProvider, fmt.Errorf(errMissingSecretKey)
  291. }
  292. return chefProvider, nil
  293. }
  294. // Not Implemented DeleteSecret.
  295. func (providerchef *Providerchef) DeleteSecret(_ context.Context, _ v1beta1.PushSecretRemoteRef) error {
  296. return fmt.Errorf(errNotImplemented)
  297. }
  298. // Not Implemented PushSecret.
  299. func (providerchef *Providerchef) PushSecret(_ context.Context, _ *corev1.Secret, _ v1beta1.PushSecretData) error {
  300. return fmt.Errorf(errNotImplemented)
  301. }
  302. func (providerchef *Providerchef) SecretExists(_ context.Context, _ v1beta1.PushSecretRemoteRef) (bool, error) {
  303. return false, fmt.Errorf(errNotImplemented)
  304. }
  305. // Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite).
  306. func (providerchef *Providerchef) Capabilities() v1beta1.SecretStoreCapabilities {
  307. return v1beta1.SecretStoreReadOnly
  308. }