secretsmanager.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  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 secretsmanager
  13. import (
  14. "context"
  15. "encoding/json"
  16. "errors"
  17. "fmt"
  18. "strings"
  19. "github.com/aws/aws-sdk-go/aws"
  20. "github.com/aws/aws-sdk-go/aws/request"
  21. "github.com/aws/aws-sdk-go/aws/session"
  22. awssm "github.com/aws/aws-sdk-go/service/secretsmanager"
  23. "github.com/tidwall/gjson"
  24. utilpointer "k8s.io/utils/pointer"
  25. ctrl "sigs.k8s.io/controller-runtime"
  26. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  27. "github.com/external-secrets/external-secrets/pkg/find"
  28. "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  29. )
  30. // https://github.com/external-secrets/external-secrets/issues/644
  31. var _ esv1beta1.SecretsClient = &SecretsManager{}
  32. // SecretsManager is a provider for AWS SecretsManager.
  33. type SecretsManager struct {
  34. sess *session.Session
  35. client SMInterface
  36. cache map[string]*awssm.GetSecretValueOutput
  37. }
  38. // SMInterface is a subset of the smiface api.
  39. // see: https://docs.aws.amazon.com/sdk-for-go/api/service/secretsmanager/secretsmanageriface/
  40. type SMInterface interface {
  41. GetSecretValue(*awssm.GetSecretValueInput) (*awssm.GetSecretValueOutput, error)
  42. ListSecrets(*awssm.ListSecretsInput) (*awssm.ListSecretsOutput, error)
  43. CreateSecretWithContext(aws.Context, *awssm.CreateSecretInput, ...request.Option) (*awssm.CreateSecretOutput, error)
  44. }
  45. const (
  46. errUnexpectedFindOperator = "unexpected find operator"
  47. )
  48. var log = ctrl.Log.WithName("provider").WithName("aws").WithName("secretsmanager")
  49. // New creates a new SecretsManager client.
  50. func New(sess *session.Session, cfg *aws.Config) (*SecretsManager, error) {
  51. return &SecretsManager{
  52. sess: sess,
  53. client: awssm.New(sess, cfg),
  54. cache: make(map[string]*awssm.GetSecretValueOutput),
  55. }, nil
  56. }
  57. func (sm *SecretsManager) fetch(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (*awssm.GetSecretValueOutput, error) {
  58. ver := "AWSCURRENT"
  59. if ref.Version != "" {
  60. ver = ref.Version
  61. }
  62. log.Info("fetching secret value", "key", ref.Key, "version", ver)
  63. cacheKey := fmt.Sprintf("%s#%s", ref.Key, ver)
  64. if secretOut, found := sm.cache[cacheKey]; found {
  65. log.Info("found secret in cache", "key", ref.Key, "version", ver)
  66. return secretOut, nil
  67. }
  68. var getSecretValueInput *awssm.GetSecretValueInput
  69. if strings.HasPrefix(ver, "uuid/") {
  70. versionID := strings.TrimPrefix(ver, "uuid/")
  71. getSecretValueInput = &awssm.GetSecretValueInput{
  72. SecretId: &ref.Key,
  73. VersionId: &versionID,
  74. }
  75. } else {
  76. getSecretValueInput = &awssm.GetSecretValueInput{
  77. SecretId: &ref.Key,
  78. VersionStage: &ver,
  79. }
  80. }
  81. secretOut, err := sm.client.GetSecretValue(getSecretValueInput)
  82. var nf *awssm.ResourceNotFoundException
  83. if errors.As(err, &nf) {
  84. return nil, esv1beta1.NoSecretErr
  85. }
  86. if err != nil {
  87. return nil, err
  88. }
  89. sm.cache[cacheKey] = secretOut
  90. return secretOut, nil
  91. }
  92. func (sm *SecretsManager) SetSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
  93. secretName := remoteRef.GetRemoteKey()
  94. secretRequest := awssm.CreateSecretInput{
  95. Name: &secretName,
  96. SecretBinary: value,
  97. }
  98. _, err := sm.client.CreateSecretWithContext(ctx, &secretRequest)
  99. if err != nil {
  100. return err
  101. }
  102. return nil
  103. }
  104. // GetAllSecrets syncs multiple secrets from aws provider into a single Kubernetes Secret.
  105. func (sm *SecretsManager) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  106. if ref.Name != nil {
  107. return sm.findByName(ctx, ref)
  108. }
  109. if len(ref.Tags) > 0 {
  110. return sm.findByTags(ctx, ref)
  111. }
  112. return nil, errors.New(errUnexpectedFindOperator)
  113. }
  114. func (sm *SecretsManager) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  115. matcher, err := find.New(*ref.Name)
  116. if err != nil {
  117. return nil, err
  118. }
  119. filters := make([]*awssm.Filter, 0)
  120. if ref.Path != nil {
  121. filters = append(filters, &awssm.Filter{
  122. Key: utilpointer.StringPtr(awssm.FilterNameStringTypeName),
  123. Values: []*string{
  124. ref.Path,
  125. },
  126. })
  127. }
  128. data := make(map[string][]byte)
  129. var nextToken *string
  130. for {
  131. it, err := sm.client.ListSecrets(&awssm.ListSecretsInput{
  132. Filters: filters,
  133. NextToken: nextToken,
  134. })
  135. if err != nil {
  136. return nil, err
  137. }
  138. log.V(1).Info("aws sm findByName found", "secrets", len(it.SecretList))
  139. for _, secret := range it.SecretList {
  140. if !matcher.MatchName(*secret.Name) {
  141. continue
  142. }
  143. log.V(1).Info("aws sm findByName matches", "name", *secret.Name)
  144. err = sm.fetchAndSet(ctx, data, *secret.Name)
  145. if err != nil {
  146. return nil, err
  147. }
  148. }
  149. nextToken = it.NextToken
  150. if nextToken == nil {
  151. break
  152. }
  153. }
  154. return data, nil
  155. }
  156. func (sm *SecretsManager) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  157. filters := make([]*awssm.Filter, 0)
  158. for k, v := range ref.Tags {
  159. filters = append(filters, &awssm.Filter{
  160. Key: utilpointer.StringPtr(awssm.FilterNameStringTypeTagKey),
  161. Values: []*string{
  162. utilpointer.StringPtr(k),
  163. },
  164. }, &awssm.Filter{
  165. Key: utilpointer.StringPtr(awssm.FilterNameStringTypeTagValue),
  166. Values: []*string{
  167. utilpointer.StringPtr(v),
  168. },
  169. })
  170. }
  171. if ref.Path != nil {
  172. filters = append(filters, &awssm.Filter{
  173. Key: utilpointer.StringPtr(awssm.FilterNameStringTypeName),
  174. Values: []*string{
  175. ref.Path,
  176. },
  177. })
  178. }
  179. data := make(map[string][]byte)
  180. var nextToken *string
  181. for {
  182. log.V(1).Info("aws sm findByTag", "nextToken", nextToken)
  183. it, err := sm.client.ListSecrets(&awssm.ListSecretsInput{
  184. Filters: filters,
  185. NextToken: nextToken,
  186. })
  187. if err != nil {
  188. return nil, err
  189. }
  190. log.V(1).Info("aws sm findByTag found", "secrets", len(it.SecretList))
  191. for _, secret := range it.SecretList {
  192. err = sm.fetchAndSet(ctx, data, *secret.Name)
  193. if err != nil {
  194. return nil, err
  195. }
  196. }
  197. nextToken = it.NextToken
  198. if nextToken == nil {
  199. break
  200. }
  201. }
  202. return data, nil
  203. }
  204. func (sm *SecretsManager) fetchAndSet(ctx context.Context, data map[string][]byte, name string) error {
  205. sec, err := sm.fetch(ctx, esv1beta1.ExternalSecretDataRemoteRef{
  206. Key: name,
  207. })
  208. if err != nil {
  209. return err
  210. }
  211. if sec.SecretString != nil {
  212. data[name] = []byte(*sec.SecretString)
  213. }
  214. if sec.SecretBinary != nil {
  215. data[name] = sec.SecretBinary
  216. }
  217. return nil
  218. }
  219. // GetSecret returns a single secret from the provider.
  220. func (sm *SecretsManager) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  221. secretOut, err := sm.fetch(ctx, ref)
  222. if errors.Is(err, esv1beta1.NoSecretErr) {
  223. return nil, err
  224. }
  225. if err != nil {
  226. return nil, util.SanitizeErr(err)
  227. }
  228. if ref.Property == "" {
  229. if secretOut.SecretString != nil {
  230. return []byte(*secretOut.SecretString), nil
  231. }
  232. if secretOut.SecretBinary != nil {
  233. return secretOut.SecretBinary, nil
  234. }
  235. return nil, fmt.Errorf("invalid secret received. no secret string nor binary for key: %s", ref.Key)
  236. }
  237. var payload string
  238. if secretOut.SecretString != nil {
  239. payload = *secretOut.SecretString
  240. }
  241. if secretOut.SecretBinary != nil {
  242. payload = string(secretOut.SecretBinary)
  243. }
  244. // We need to search if a given key with a . exists before using gjson operations.
  245. idx := strings.Index(ref.Property, ".")
  246. if idx > -1 {
  247. refProperty := strings.ReplaceAll(ref.Property, ".", "\\.")
  248. val := gjson.Get(payload, refProperty)
  249. if val.Exists() {
  250. return []byte(val.String()), nil
  251. }
  252. }
  253. val := gjson.Get(payload, ref.Property)
  254. if !val.Exists() {
  255. return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
  256. }
  257. return []byte(val.String()), nil
  258. }
  259. // GetSecretMap returns multiple k/v pairs from the provider.
  260. func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  261. log.Info("fetching secret map", "key", ref.Key)
  262. data, err := sm.GetSecret(ctx, ref)
  263. if err != nil {
  264. return nil, err
  265. }
  266. kv := make(map[string]json.RawMessage)
  267. err = json.Unmarshal(data, &kv)
  268. if err != nil {
  269. return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
  270. }
  271. secretData := make(map[string][]byte)
  272. for k, v := range kv {
  273. var strVal string
  274. err = json.Unmarshal(v, &strVal)
  275. if err == nil {
  276. secretData[k] = []byte(strVal)
  277. } else {
  278. secretData[k] = v
  279. }
  280. }
  281. return secretData, nil
  282. }
  283. func (sm *SecretsManager) Close(ctx context.Context) error {
  284. return nil
  285. }
  286. func (sm *SecretsManager) Validate() (esv1beta1.ValidationResult, error) {
  287. _, err := sm.sess.Config.Credentials.Get()
  288. if err != nil {
  289. return esv1beta1.ValidationResultError, err
  290. }
  291. return esv1beta1.ValidationResultReady, nil
  292. }
  293. func (sm *SecretsManager) Capabilities() esv1beta1.SecretStoreCapabilities {
  294. return esv1beta1.SecretStoreReadOnly
  295. }