client.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  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 infisical implements a provider for retrieving secrets from Infisical.
  14. package infisical
  15. import (
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "net/http"
  21. "path"
  22. "strings"
  23. infisical "github.com/infisical/go-sdk"
  24. sdkErrors "github.com/infisical/go-sdk/packages/errors"
  25. "github.com/tidwall/gjson"
  26. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  27. "github.com/external-secrets/external-secrets/providers/v1/infisical/constants"
  28. "github.com/external-secrets/external-secrets/runtime/find"
  29. "github.com/external-secrets/external-secrets/runtime/metrics"
  30. )
  31. var (
  32. errPropertyNotFound = "property %s does not exist in secret %s"
  33. errTagsNotImplemented = errors.New("find by tags not supported")
  34. )
  35. const (
  36. getSecretsV3 = "GetSecretsV3"
  37. getSecretByKeyV3 = "GetSecretByKeyV3"
  38. )
  39. // isNotFoundError reports whether err is an Infisical API error with HTTP 404.
  40. // The go-sdk wraps transport failures in *sdkErrors.APIError, which carries the
  41. // upstream StatusCode.
  42. func isNotFoundError(err error) bool {
  43. var apiErr *sdkErrors.APIError
  44. return errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotFound
  45. }
  46. func getPropertyValue(jsonData, propertyName, keyName string) ([]byte, error) {
  47. result := gjson.Get(jsonData, propertyName)
  48. if !result.Exists() {
  49. return nil, fmt.Errorf(errPropertyNotFound, propertyName, keyName)
  50. }
  51. return []byte(result.Str), nil
  52. }
  53. // formatSecretKey returns the secret key, optionally prefixed with the relative
  54. // path when includeSecretPath is enabled. Secrets at the root path (/) are
  55. // never prefixed.
  56. //
  57. // Example (basePath="/", includeSecretPath=true):
  58. //
  59. // ("/", "FOO") -> "FOO"
  60. // ("/sub", "FOO") -> "sub/FOO"
  61. // ("/a/b", "FOO") -> "a/b/FOO"
  62. //
  63. // Example (basePath="/path", includeSecretPath=true):
  64. //
  65. // ("/path", "FOO") -> "FOO"
  66. // ("/path/sub", "FOO") -> "sub/FOO"
  67. func formatSecretKey(secretKey, secretPath, basePath string, includeSecretPath bool) string {
  68. if !includeSecretPath {
  69. return secretKey
  70. }
  71. rel := strings.TrimPrefix(secretPath, basePath)
  72. rel = strings.TrimPrefix(rel, "/")
  73. if rel == "" {
  74. return secretKey
  75. }
  76. return rel + "/" + secretKey
  77. }
  78. // getSecretAddress returns the (folder, name) pair to look up in Infisical for the given key.
  79. //
  80. // Resolution rules:
  81. // - No slash in key: treat key as a bare secret name in defaultPath.
  82. // ("foo" + defaultPath="/scope") -> ("/scope", "foo")
  83. // - Key starts with `/`: treat key as an absolute path; defaultPath is ignored.
  84. // ("/a/b/foo" + defaultPath="/scope") -> ("/a/b", "foo")
  85. // - Otherwise (slash present, no leading `/`): treat key as a folder path relative to defaultPath.
  86. // ("sub/foo" + defaultPath="/scope") -> ("/scope/sub", "foo")
  87. func getSecretAddress(defaultPath, key string) (string, string) {
  88. if !strings.Contains(key, "/") {
  89. return defaultPath, key
  90. }
  91. lastIndex := strings.LastIndex(key, "/")
  92. folder, name := key[:lastIndex], key[lastIndex+1:]
  93. if strings.HasPrefix(key, "/") {
  94. return folder, name
  95. }
  96. return path.Join(defaultPath, folder), name
  97. }
  98. // GetSecret retrieves a secret value from Infisical.
  99. // If this returns an error with type NoSecretError then the secret entry will be deleted depending on the
  100. // deletionPolicy.
  101. func (p *Provider) GetSecret(_ context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  102. path, key := getSecretAddress(p.apiScope.SecretPath, ref.Key)
  103. secret, err := p.sdkClient.Secrets().Retrieve(infisical.RetrieveSecretOptions{
  104. Environment: p.apiScope.EnvironmentSlug,
  105. ProjectSlug: p.apiScope.ProjectSlug,
  106. SecretKey: key,
  107. SecretPath: path,
  108. IncludeImports: true,
  109. ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
  110. })
  111. metrics.ObserveAPICall(constants.ProviderName, getSecretByKeyV3, err)
  112. if err != nil {
  113. // Translate a 404 into the NoSecret sentinel so deletionPolicy: Delete
  114. // can prune the entry and a missing key reports as not-found rather
  115. // than a generic sync error.
  116. if isNotFoundError(err) {
  117. return nil, esv1.NoSecretErr
  118. }
  119. return nil, err
  120. }
  121. if ref.Property != "" {
  122. propertyValue, err := getPropertyValue(secret.SecretValue, ref.Property, ref.Key)
  123. if err != nil {
  124. return nil, err
  125. }
  126. return propertyValue, nil
  127. }
  128. return []byte(secret.SecretValue), nil
  129. }
  130. // GetSecretMap returns multiple k/v pairs from the provider.
  131. func (p *Provider) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  132. secret, err := p.GetSecret(ctx, ref)
  133. if err != nil {
  134. return nil, err
  135. }
  136. kv := make(map[string]json.RawMessage)
  137. err = json.Unmarshal(secret, &kv)
  138. if err != nil {
  139. return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
  140. }
  141. secretData := make(map[string][]byte)
  142. for k, v := range kv {
  143. var strVal string
  144. err = json.Unmarshal(v, &strVal)
  145. if err == nil {
  146. secretData[k] = []byte(strVal)
  147. } else {
  148. secretData[k] = v
  149. }
  150. }
  151. return secretData, nil
  152. }
  153. // GetAllSecrets retrieves all secrets matching the given criteria from Infisical.
  154. func (p *Provider) GetAllSecrets(_ context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  155. if ref.Tags != nil {
  156. return nil, errTagsNotImplemented
  157. }
  158. // A find path says where to look, not how far: whether subfolders are read
  159. // stays with the store's recursive setting. An empty one is not a path, and
  160. // passing it on would ask the SDK for the whole project.
  161. secretPath := p.apiScope.SecretPath
  162. if ref.Path != nil && *ref.Path != "" {
  163. secretPath = *ref.Path
  164. }
  165. secrets, err := p.sdkClient.Secrets().List(infisical.ListSecretsOptions{
  166. Environment: p.apiScope.EnvironmentSlug,
  167. ProjectSlug: p.apiScope.ProjectSlug,
  168. SecretPath: secretPath,
  169. Recursive: p.apiScope.Recursive,
  170. ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
  171. IncludeImports: true,
  172. SkipUniqueValidation: p.apiScope.IncludeSecretPath,
  173. })
  174. metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
  175. if err != nil {
  176. return nil, err
  177. }
  178. if ref.Name == nil {
  179. secretMap := make(map[string][]byte, len(secrets))
  180. for _, secret := range secrets {
  181. key := formatSecretKey(secret.SecretKey, secret.SecretPath, secretPath, p.apiScope.IncludeSecretPath)
  182. secretMap[key] = []byte(secret.SecretValue)
  183. }
  184. return secretMap, nil
  185. }
  186. matcher, err := find.New(*ref.Name)
  187. if err != nil {
  188. return nil, err
  189. }
  190. selected := map[string][]byte{}
  191. for _, secret := range secrets {
  192. if matcher.MatchName(secret.SecretKey) {
  193. key := formatSecretKey(secret.SecretKey, secret.SecretPath, secretPath, p.apiScope.IncludeSecretPath)
  194. selected[key] = []byte(secret.SecretValue)
  195. }
  196. }
  197. return selected, nil
  198. }
  199. // Validate checks if the client is configured correctly.
  200. // and is able to retrieve secrets from the provider.
  201. // If the validation result is unknown it will be ignored.
  202. func (p *Provider) Validate() (esv1.ValidationResult, error) {
  203. // try to fetch the secrets to ensure provided credentials has access to read secrets
  204. _, err := p.sdkClient.Secrets().List(infisical.ListSecretsOptions{
  205. Environment: p.apiScope.EnvironmentSlug,
  206. ProjectSlug: p.apiScope.ProjectSlug,
  207. Recursive: p.apiScope.Recursive,
  208. SecretPath: p.apiScope.SecretPath,
  209. ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
  210. })
  211. metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
  212. if err != nil {
  213. return esv1.ValidationResultError, fmt.Errorf(
  214. "cannot read secrets with provided project scope project:%s environment:%s secret-path:%s recursive:%t, %w",
  215. p.apiScope.ProjectSlug,
  216. p.apiScope.EnvironmentSlug,
  217. p.apiScope.SecretPath,
  218. p.apiScope.Recursive,
  219. err,
  220. )
  221. }
  222. return esv1.ValidationResultReady, nil
  223. }