parameterstore.go 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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 parameterstore
  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. "github.com/aws/aws-sdk-go/service/ssm"
  23. "github.com/tidwall/gjson"
  24. utilpointer "k8s.io/utils/pointer"
  25. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  26. "github.com/external-secrets/external-secrets/pkg/find"
  27. "github.com/external-secrets/external-secrets/pkg/provider/aws/util"
  28. )
  29. // https://github.com/external-secrets/external-secrets/issues/644
  30. var (
  31. _ esv1beta1.SecretsClient = &ParameterStore{}
  32. managedBy = "managed-by"
  33. externalSecrets = "external-secrets"
  34. )
  35. // ParameterStore is a provider for AWS ParameterStore.
  36. type ParameterStore struct {
  37. sess *session.Session
  38. client PMInterface
  39. }
  40. // PMInterface is a subset of the parameterstore api.
  41. // see: https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/ssmiface/
  42. type PMInterface interface {
  43. GetParameterWithContext(aws.Context, *ssm.GetParameterInput, ...request.Option) (*ssm.GetParameterOutput, error)
  44. PutParameterWithContext(aws.Context, *ssm.PutParameterInput, ...request.Option) (*ssm.PutParameterOutput, error)
  45. DescribeParametersWithContext(aws.Context, *ssm.DescribeParametersInput, ...request.Option) (*ssm.DescribeParametersOutput, error)
  46. ListTagsForResourceWithContext(aws.Context, *ssm.ListTagsForResourceInput, ...request.Option) (*ssm.ListTagsForResourceOutput, error)
  47. }
  48. const (
  49. errUnexpectedFindOperator = "unexpected find operator"
  50. )
  51. // New constructs a ParameterStore Provider that is specific to a store.
  52. func New(sess *session.Session, cfg *aws.Config) (*ParameterStore, error) {
  53. return &ParameterStore{
  54. sess: sess,
  55. client: ssm.New(sess, cfg),
  56. }, nil
  57. }
  58. func (pm *ParameterStore) getTagsByName(ctx aws.Context, ref *ssm.GetParameterOutput) ([]*ssm.Tag, error) {
  59. parameterTags := ssm.ListTagsForResourceInput{
  60. ResourceId: ref.Parameter.ARN,
  61. ResourceType: ref.Parameter.Type,
  62. }
  63. data, err := pm.client.ListTagsForResourceWithContext(ctx, &parameterTags)
  64. if err != nil {
  65. return nil, fmt.Errorf("error listing tags %w", err)
  66. }
  67. return data.TagList, nil
  68. }
  69. func (pm *ParameterStore) SetSecret(ctx context.Context, value []byte, remoteRef esv1beta1.PushRemoteRef) error {
  70. parameterType := "String"
  71. stringValue := string(value)
  72. secretName := remoteRef.GetRemoteKey()
  73. secretRequest := ssm.PutParameterInput{
  74. Name: &secretName,
  75. Value: &stringValue,
  76. Type: &parameterType,
  77. }
  78. secretValue := ssm.GetParameterInput{
  79. Name: &secretName,
  80. }
  81. existing, err := pm.client.GetParameterWithContext(ctx, &secretValue)
  82. if err != nil && err.Error() != ssm.ErrCodeParameterNotFound {
  83. // TODO: give some more context to the error
  84. return err
  85. }
  86. // If we have a valid parameter returned to us, check its tags
  87. if existing != nil && existing.Parameter != nil {
  88. fmt.Println("The existing value contains data:", existing.String())
  89. tags, err := pm.getTagsByName(ctx, existing)
  90. if err != nil {
  91. return fmt.Errorf("error getting the existing tags for the parameter: %w", err)
  92. }
  93. isManaged := isManagedByESO(tags)
  94. if !isManaged {
  95. // TODO Can we refactor this error message to a higher scope to stop duplicates
  96. return fmt.Errorf("secret not managed by external-secrets")
  97. }
  98. }
  99. // let's set the secret
  100. // Do we need to delete the existing parameter on the remote?
  101. return pm.setManagedRemoteParameter(ctx, secretRequest)
  102. }
  103. func isManagedByESO(tags []*ssm.Tag) (isManaged bool) {
  104. for _, tag := range tags {
  105. if *tag.Key == managedBy && *tag.Value == externalSecrets {
  106. isManaged = true
  107. }
  108. }
  109. return
  110. }
  111. func (pm *ParameterStore) setManagedRemoteParameter(ctx context.Context, secretRequest ssm.PutParameterInput) error {
  112. externalSecretsTag := ssm.Tag{
  113. Key: &managedBy,
  114. Value: &externalSecrets,
  115. }
  116. isManaged := isManagedByESO(secretRequest.Tags)
  117. if !isManaged {
  118. secretRequest.Tags = append(secretRequest.Tags, &externalSecretsTag)
  119. }
  120. _, err := pm.client.PutParameterWithContext(ctx, &secretRequest)
  121. if err != nil {
  122. // TODO: Edit test so that we can pass more context and have the test not fail because it's not exactly the error `err`
  123. //return fmt.fmErrorf("failed to push the parameter: %w", err)
  124. return err
  125. }
  126. return nil
  127. }
  128. // GetAllSecrets fetches information from multiple secrets into a single kubernetes secret.
  129. func (pm *ParameterStore) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  130. if ref.Name != nil {
  131. return pm.findByName(ctx, ref)
  132. }
  133. if ref.Tags != nil {
  134. return pm.findByTags(ctx, ref)
  135. }
  136. return nil, errors.New(errUnexpectedFindOperator)
  137. }
  138. func (pm *ParameterStore) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  139. matcher, err := find.New(*ref.Name)
  140. if err != nil {
  141. return nil, err
  142. }
  143. pathFilter := make([]*ssm.ParameterStringFilter, 0)
  144. if ref.Path != nil {
  145. pathFilter = append(pathFilter, &ssm.ParameterStringFilter{
  146. Key: aws.String("Path"),
  147. Option: aws.String("Recursive"),
  148. Values: []*string{ref.Path},
  149. })
  150. }
  151. data := make(map[string][]byte)
  152. var nextToken *string
  153. for {
  154. it, err := pm.client.DescribeParametersWithContext(
  155. ctx,
  156. &ssm.DescribeParametersInput{
  157. NextToken: nextToken,
  158. ParameterFilters: pathFilter,
  159. })
  160. if err != nil {
  161. return nil, err
  162. }
  163. for _, param := range it.Parameters {
  164. if !matcher.MatchName(*param.Name) {
  165. continue
  166. }
  167. err = pm.fetchAndSet(data, *param.Name)
  168. if err != nil {
  169. return nil, err
  170. }
  171. }
  172. nextToken = it.NextToken
  173. if nextToken == nil {
  174. break
  175. }
  176. }
  177. return data, nil
  178. }
  179. func (pm *ParameterStore) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  180. filters := make([]*ssm.ParameterStringFilter, 0)
  181. for k, v := range ref.Tags {
  182. filters = append(filters, &ssm.ParameterStringFilter{
  183. Key: utilpointer.StringPtr(fmt.Sprintf("tag:%s", k)),
  184. Values: []*string{utilpointer.StringPtr(v)},
  185. Option: utilpointer.StringPtr("Equals"),
  186. })
  187. }
  188. if ref.Path != nil {
  189. filters = append(filters, &ssm.ParameterStringFilter{
  190. Key: aws.String("Path"),
  191. Option: aws.String("Recursive"),
  192. Values: []*string{ref.Path},
  193. })
  194. }
  195. data := make(map[string][]byte)
  196. var nextToken *string
  197. for {
  198. it, err := pm.client.DescribeParametersWithContext(
  199. ctx,
  200. &ssm.DescribeParametersInput{
  201. ParameterFilters: filters,
  202. NextToken: nextToken,
  203. })
  204. if err != nil {
  205. return nil, err
  206. }
  207. for _, param := range it.Parameters {
  208. err = pm.fetchAndSet(data, *param.Name)
  209. if err != nil {
  210. return nil, err
  211. }
  212. }
  213. nextToken = it.NextToken
  214. if nextToken == nil {
  215. break
  216. }
  217. }
  218. return data, nil
  219. }
  220. func (pm *ParameterStore) fetchAndSet(data map[string][]byte, name string) error {
  221. ctx := context.Background()
  222. out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  223. Name: utilpointer.StringPtr(name),
  224. WithDecryption: aws.Bool(true),
  225. })
  226. if err != nil {
  227. return util.SanitizeErr(err)
  228. }
  229. data[name] = []byte(*out.Parameter.Value)
  230. return nil
  231. }
  232. // GetSecret returns a single secret from the provider.
  233. func (pm *ParameterStore) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  234. out, err := pm.client.GetParameterWithContext(ctx, &ssm.GetParameterInput{
  235. Name: &ref.Key,
  236. WithDecryption: aws.Bool(true),
  237. })
  238. nsf := esv1beta1.NoSecretError{}
  239. var nf *ssm.ParameterNotFound
  240. if errors.As(err, &nf) || errors.As(err, &nsf) {
  241. return nil, esv1beta1.NoSecretErr
  242. }
  243. if err != nil {
  244. return nil, util.SanitizeErr(err)
  245. }
  246. if ref.Property == "" {
  247. if out.Parameter.Value != nil {
  248. return []byte(*out.Parameter.Value), nil
  249. }
  250. return nil, fmt.Errorf("invalid secret received. parameter value is nil for key: %s", ref.Key)
  251. }
  252. idx := strings.Index(ref.Property, ".")
  253. if idx > -1 {
  254. refProperty := strings.ReplaceAll(ref.Property, ".", "\\.")
  255. val := gjson.Get(*out.Parameter.Value, refProperty)
  256. if val.Exists() {
  257. return []byte(val.String()), nil
  258. }
  259. }
  260. val := gjson.Get(*out.Parameter.Value, ref.Property)
  261. if !val.Exists() {
  262. return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
  263. }
  264. return []byte(val.String()), nil
  265. }
  266. // GetSecretMap returns multiple k/v pairs from the provider.
  267. func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  268. data, err := pm.GetSecret(ctx, ref)
  269. if err != nil {
  270. return nil, err
  271. }
  272. kv := make(map[string]string)
  273. err = json.Unmarshal(data, &kv)
  274. if err != nil {
  275. return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
  276. }
  277. secretData := make(map[string][]byte)
  278. for k, v := range kv {
  279. secretData[k] = []byte(v)
  280. }
  281. return secretData, nil
  282. }
  283. func (pm *ParameterStore) Close(ctx context.Context) error {
  284. return nil
  285. }
  286. func (pm *ParameterStore) Validate() (esv1beta1.ValidationResult, error) {
  287. _, err := pm.sess.Config.Credentials.Get()
  288. if err != nil {
  289. return esv1beta1.ValidationResultError, err
  290. }
  291. return esv1beta1.ValidationResultReady, nil
  292. }