client.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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 secretmanager
  13. import (
  14. "context"
  15. "encoding/json"
  16. "errors"
  17. "fmt"
  18. "strconv"
  19. "strings"
  20. secretmanager "cloud.google.com/go/secretmanager/apiv1"
  21. "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
  22. "github.com/googleapis/gax-go/v2"
  23. "github.com/googleapis/gax-go/v2/apierror"
  24. "github.com/tidwall/gjson"
  25. "google.golang.org/api/iterator"
  26. "google.golang.org/genproto/protobuf/field_mask"
  27. "google.golang.org/grpc/codes"
  28. "google.golang.org/grpc/status"
  29. corev1 "k8s.io/api/core/v1"
  30. ctrl "sigs.k8s.io/controller-runtime"
  31. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  32. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  33. "github.com/external-secrets/external-secrets/pkg/constants"
  34. "github.com/external-secrets/external-secrets/pkg/find"
  35. "github.com/external-secrets/external-secrets/pkg/metrics"
  36. "github.com/external-secrets/external-secrets/pkg/provider/util/locks"
  37. "github.com/external-secrets/external-secrets/pkg/utils"
  38. )
  39. const (
  40. CloudPlatformRole = "https://www.googleapis.com/auth/cloud-platform"
  41. defaultVersion = "latest"
  42. errGCPSMStore = "received invalid GCPSM SecretStore resource"
  43. errUnableGetCredentials = "unable to get credentials: %w"
  44. errClientClose = "unable to close SecretManager client: %w"
  45. errMissingStoreSpec = "invalid: missing store spec"
  46. errFetchSAKSecret = "could not fetch SecretAccessKey secret: %w"
  47. errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w"
  48. errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w"
  49. errUninitalizedGCPProvider = "provider GCP is not initialized"
  50. errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
  51. errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
  52. errInvalidStore = "invalid store"
  53. errInvalidStoreSpec = "invalid store spec"
  54. errInvalidStoreProv = "invalid store provider"
  55. errInvalidGCPProv = "invalid gcp secrets manager provider"
  56. errInvalidAuthSecretRef = "invalid auth secret data: %w"
  57. errInvalidWISARef = "invalid workload identity service account reference: %w"
  58. errUnexpectedFindOperator = "unexpected find operator"
  59. managedByKey = "managed-by"
  60. managedByValue = "external-secrets"
  61. providerName = "GCPSecretManager"
  62. )
  63. type Client struct {
  64. smClient GoogleSecretManagerClient
  65. kube kclient.Client
  66. store *esv1beta1.GCPSMProvider
  67. storeKind string
  68. // namespace of the external secret
  69. namespace string
  70. workloadIdentity *workloadIdentity
  71. }
  72. type GoogleSecretManagerClient interface {
  73. DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error
  74. AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
  75. ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator
  76. AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
  77. CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
  78. Close() error
  79. GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
  80. UpdateSecret(context.Context, *secretmanagerpb.UpdateSecretRequest, ...gax.CallOption) (*secretmanagerpb.Secret, error)
  81. }
  82. var log = ctrl.Log.WithName("provider").WithName("gcp").WithName("secretsmanager")
  83. func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
  84. gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  85. Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
  86. })
  87. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
  88. if err != nil {
  89. if status.Code(err) == codes.NotFound {
  90. return nil
  91. }
  92. return err
  93. }
  94. if manager, ok := gcpSecret.Labels[managedByKey]; !ok || manager != managedByValue {
  95. return nil
  96. }
  97. deleteSecretVersionReq := &secretmanagerpb.DeleteSecretRequest{
  98. Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, remoteRef.GetRemoteKey()),
  99. Etag: gcpSecret.Etag,
  100. }
  101. err = c.smClient.DeleteSecret(ctx, deleteSecretVersionReq)
  102. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMDeleteSecret, err)
  103. return err
  104. }
  105. func parseError(err error) error {
  106. var gerr *apierror.APIError
  107. if errors.As(err, &gerr) && gerr.GRPCStatus().Code() == codes.NotFound {
  108. return esv1beta1.NoSecretError{}
  109. }
  110. return err
  111. }
  112. func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
  113. return false, fmt.Errorf("not implemented")
  114. }
  115. // PushSecret pushes a kubernetes secret key into gcp provider Secret.
  116. func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData esv1beta1.PushSecretData) error {
  117. if pushSecretData.GetSecretKey() == "" {
  118. return fmt.Errorf("pushing the whole secret is not yet implemented")
  119. }
  120. payload := secret.Data[pushSecretData.GetSecretKey()]
  121. secretName := fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, pushSecretData.GetRemoteKey())
  122. gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  123. Name: secretName,
  124. })
  125. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
  126. if err != nil {
  127. if status.Code(err) != codes.NotFound {
  128. return err
  129. }
  130. gcpSecret, err = c.smClient.CreateSecret(ctx, &secretmanagerpb.CreateSecretRequest{
  131. Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
  132. SecretId: pushSecretData.GetRemoteKey(),
  133. Secret: &secretmanagerpb.Secret{
  134. Labels: map[string]string{
  135. managedByKey: managedByValue,
  136. },
  137. Replication: &secretmanagerpb.Replication{
  138. Replication: &secretmanagerpb.Replication_Automatic_{
  139. Automatic: &secretmanagerpb.Replication_Automatic{},
  140. },
  141. },
  142. },
  143. })
  144. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMCreateSecret, err)
  145. if err != nil {
  146. return err
  147. }
  148. }
  149. builder, err := newPushSecretBuilder(payload, pushSecretData)
  150. if err != nil {
  151. return err
  152. }
  153. annotations, labels, err := builder.buildMetadata(gcpSecret.Annotations, gcpSecret.Labels)
  154. if err != nil {
  155. return err
  156. }
  157. if !mapEqual(gcpSecret.Annotations, annotations) || !mapEqual(gcpSecret.Labels, labels) {
  158. _, err = c.smClient.UpdateSecret(ctx, &secretmanagerpb.UpdateSecretRequest{
  159. Secret: &secretmanagerpb.Secret{
  160. Name: gcpSecret.Name,
  161. Etag: gcpSecret.Etag,
  162. Labels: labels,
  163. Annotations: annotations,
  164. },
  165. UpdateMask: &field_mask.FieldMask{
  166. Paths: []string{"labels", "annotations"},
  167. },
  168. })
  169. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMUpdateSecret, err)
  170. if err != nil {
  171. return err
  172. }
  173. }
  174. unlock, err := locks.TryLock(providerName, secretName)
  175. if err != nil {
  176. return err
  177. }
  178. defer unlock()
  179. gcpVersion, err := c.smClient.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
  180. Name: fmt.Sprintf("%s/versions/latest", secretName),
  181. })
  182. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
  183. if err != nil && status.Code(err) != codes.NotFound {
  184. return err
  185. }
  186. if gcpVersion != nil && gcpVersion.Payload != nil && !builder.needUpdate(gcpVersion.Payload.Data) {
  187. return nil
  188. }
  189. var original []byte
  190. if gcpVersion != nil && gcpVersion.Payload != nil {
  191. original = gcpVersion.Payload.Data
  192. }
  193. data, err := builder.buildData(original)
  194. if err != nil {
  195. return err
  196. }
  197. addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
  198. Parent: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, pushSecretData.GetRemoteKey()),
  199. Payload: &secretmanagerpb.SecretPayload{
  200. Data: data,
  201. },
  202. }
  203. _, err = c.smClient.AddSecretVersion(ctx, addSecretVersionReq)
  204. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAddSecretVersion, err)
  205. return err
  206. }
  207. // GetAllSecrets syncs multiple secrets from gcp provider into a single Kubernetes Secret.
  208. func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  209. if ref.Name != nil {
  210. return c.findByName(ctx, ref)
  211. }
  212. if len(ref.Tags) > 0 {
  213. return c.findByTags(ctx, ref)
  214. }
  215. return nil, errors.New(errUnexpectedFindOperator)
  216. }
  217. func (c *Client) findByName(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  218. // regex matcher
  219. matcher, err := find.New(*ref.Name)
  220. if err != nil {
  221. return nil, err
  222. }
  223. req := &secretmanagerpb.ListSecretsRequest{
  224. Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
  225. }
  226. if ref.Path != nil {
  227. req.Filter = fmt.Sprintf("name:%s", *ref.Path)
  228. }
  229. // Call the API.
  230. it := c.smClient.ListSecrets(ctx, req)
  231. secretMap := make(map[string][]byte)
  232. var resp *secretmanagerpb.Secret
  233. defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
  234. for {
  235. resp, err = it.Next()
  236. if errors.Is(err, iterator.Done) {
  237. break
  238. }
  239. if err != nil {
  240. return nil, fmt.Errorf("failed to list secrets: %w", err)
  241. }
  242. log.V(1).Info("gcp sm findByName found", "secrets", strconv.Itoa(it.PageInfo().Remaining()))
  243. key := c.trimName(resp.Name)
  244. // If we don't match we skip.
  245. // Also, if we have path, and it is not at the beguining we skip.
  246. // We have to check if path is at the beguining of the key because
  247. // there is no way to create a `name:%s*` (starts with) filter
  248. // At https://cloud.google.com/secret-manager/docs/filtering you can use `*`
  249. // but not like that it seems.
  250. if !matcher.MatchName(key) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
  251. continue
  252. }
  253. log.V(1).Info("gcp sm findByName matches", "name", resp.Name)
  254. secretMap[key], err = c.getData(ctx, key)
  255. if err != nil {
  256. return nil, err
  257. }
  258. }
  259. return utils.ConvertKeys(ref.ConversionStrategy, secretMap)
  260. }
  261. func (c *Client) getData(ctx context.Context, key string) ([]byte, error) {
  262. dataRef := esv1beta1.ExternalSecretDataRemoteRef{
  263. Key: key,
  264. }
  265. data, err := c.GetSecret(ctx, dataRef)
  266. if err != nil {
  267. return []byte(""), err
  268. }
  269. return data, nil
  270. }
  271. func (c *Client) findByTags(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
  272. var tagFilter string
  273. for k, v := range ref.Tags {
  274. tagFilter = fmt.Sprintf("%slabels.%s=%s ", tagFilter, k, v)
  275. }
  276. tagFilter = strings.TrimSuffix(tagFilter, " ")
  277. if ref.Path != nil {
  278. tagFilter = fmt.Sprintf("%s name:%s", tagFilter, *ref.Path)
  279. }
  280. req := &secretmanagerpb.ListSecretsRequest{
  281. Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
  282. }
  283. log.V(1).Info("gcp sm findByTags", "tagFilter", tagFilter)
  284. req.Filter = tagFilter
  285. // Call the API.
  286. it := c.smClient.ListSecrets(ctx, req)
  287. var resp *secretmanagerpb.Secret
  288. var err error
  289. defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
  290. secretMap := make(map[string][]byte)
  291. for {
  292. resp, err = it.Next()
  293. if errors.Is(err, iterator.Done) {
  294. break
  295. }
  296. if err != nil {
  297. return nil, fmt.Errorf("failed to list secrets: %w", err)
  298. }
  299. key := c.trimName(resp.Name)
  300. if ref.Path != nil && !strings.HasPrefix(key, *ref.Path) {
  301. continue
  302. }
  303. log.V(1).Info("gcp sm findByTags matches tags", "name", resp.Name)
  304. secretMap[key], err = c.getData(ctx, key)
  305. if err != nil {
  306. return nil, err
  307. }
  308. }
  309. return utils.ConvertKeys(ref.ConversionStrategy, secretMap)
  310. }
  311. func (c *Client) trimName(name string) string {
  312. projectIDNumuber := c.extractProjectIDNumber(name)
  313. key := strings.TrimPrefix(name, fmt.Sprintf("projects/%s/secrets/", projectIDNumuber))
  314. return key
  315. }
  316. // extractProjectIDNumber grabs the project id from the full name returned by gcp api
  317. // gcp api seems to always return the number and not the project name
  318. // (and users would always use the name, while requests accept both).
  319. func (c *Client) extractProjectIDNumber(secretFullName string) string {
  320. s := strings.Split(secretFullName, "/")
  321. ProjectIDNumuber := s[1]
  322. return ProjectIDNumuber
  323. }
  324. // GetSecret returns a single secret from the provider.
  325. func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  326. if utils.IsNil(c.smClient) || c.store.ProjectID == "" {
  327. return nil, fmt.Errorf(errUninitalizedGCPProvider)
  328. }
  329. if ref.MetadataPolicy == esv1beta1.ExternalSecretMetadataPolicyFetch {
  330. return c.getSecretMetadata(ctx, ref)
  331. }
  332. version := ref.Version
  333. if version == "" {
  334. version = defaultVersion
  335. }
  336. req := &secretmanagerpb.AccessSecretVersionRequest{
  337. Name: fmt.Sprintf("projects/%s/secrets/%s/versions/%s", c.store.ProjectID, ref.Key, version),
  338. }
  339. result, err := c.smClient.AccessSecretVersion(ctx, req)
  340. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
  341. err = parseError(err)
  342. if err != nil {
  343. return nil, fmt.Errorf(errClientGetSecretAccess, err)
  344. }
  345. if ref.Property == "" {
  346. if result.Payload.Data != nil {
  347. return result.Payload.Data, nil
  348. }
  349. return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
  350. }
  351. val := getDataByProperty(result.Payload.Data, ref.Property)
  352. if !val.Exists() {
  353. return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
  354. }
  355. return []byte(val.String()), nil
  356. }
  357. func (c *Client) getSecretMetadata(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
  358. secret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  359. Name: fmt.Sprintf("projects/%s/secrets/%s", c.store.ProjectID, ref.Key),
  360. })
  361. err = parseError(err)
  362. if err != nil {
  363. return nil, fmt.Errorf(errClientGetSecretAccess, err)
  364. }
  365. const (
  366. annotations = "annotations"
  367. labels = "labels"
  368. )
  369. extractMetadataKey := func(s string, p string) string {
  370. prefix := p + "."
  371. if !strings.HasPrefix(s, prefix) {
  372. return ""
  373. }
  374. return strings.TrimPrefix(s, prefix)
  375. }
  376. if annotation := extractMetadataKey(ref.Property, annotations); annotation != "" {
  377. v, ok := secret.GetAnnotations()[annotation]
  378. if !ok {
  379. return nil, fmt.Errorf("annotation with key %s does not exist in secret %s", annotation, ref.Key)
  380. }
  381. return []byte(v), nil
  382. }
  383. if label := extractMetadataKey(ref.Property, labels); label != "" {
  384. v, ok := secret.GetLabels()[label]
  385. if !ok {
  386. return nil, fmt.Errorf("label with key %s does not exist in secret %s", label, ref.Key)
  387. }
  388. return []byte(v), nil
  389. }
  390. if ref.Property == annotations {
  391. j, err := json.Marshal(secret.GetAnnotations())
  392. if err != nil {
  393. return nil, fmt.Errorf("faild marshaling annotations into json: %w", err)
  394. }
  395. return j, nil
  396. }
  397. if ref.Property == labels {
  398. j, err := json.Marshal(secret.GetLabels())
  399. if err != nil {
  400. return nil, fmt.Errorf("faild marshaling labels into json: %w", err)
  401. }
  402. return j, nil
  403. }
  404. if ref.Property != "" {
  405. return nil, fmt.Errorf("invalid property %s: metadata property should start with either %s or %s", ref.Property, annotations, labels)
  406. }
  407. j, err := json.Marshal(map[string]map[string]string{
  408. "annotations": secret.GetAnnotations(),
  409. "labels": secret.GetLabels(),
  410. })
  411. if err != nil {
  412. return nil, fmt.Errorf("faild marshaling metadata map into json: %w", err)
  413. }
  414. return j, nil
  415. }
  416. // GetSecretMap returns multiple k/v pairs from the provider.
  417. func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  418. if c.smClient == nil || c.store.ProjectID == "" {
  419. return nil, fmt.Errorf(errUninitalizedGCPProvider)
  420. }
  421. data, err := c.GetSecret(ctx, ref)
  422. if err != nil {
  423. return nil, err
  424. }
  425. kv := make(map[string]json.RawMessage)
  426. err = json.Unmarshal(data, &kv)
  427. if err != nil {
  428. return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
  429. }
  430. secretData := make(map[string][]byte)
  431. for k, v := range kv {
  432. var strVal string
  433. err = json.Unmarshal(v, &strVal)
  434. if err == nil {
  435. secretData[k] = []byte(strVal)
  436. } else {
  437. secretData[k] = v
  438. }
  439. }
  440. return secretData, nil
  441. }
  442. func (c *Client) Close(_ context.Context) error {
  443. var err error
  444. if c.smClient != nil {
  445. err = c.smClient.Close()
  446. }
  447. if c.workloadIdentity != nil {
  448. err = c.workloadIdentity.Close()
  449. }
  450. useMu.Unlock()
  451. if err != nil {
  452. return fmt.Errorf(errClientClose, err)
  453. }
  454. return nil
  455. }
  456. func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
  457. if c.storeKind == esv1beta1.ClusterSecretStoreKind && isReferentSpec(c.store) {
  458. return esv1beta1.ValidationResultUnknown, nil
  459. }
  460. return esv1beta1.ValidationResultReady, nil
  461. }
  462. func getDataByProperty(data []byte, property string) gjson.Result {
  463. var payload string
  464. if data != nil {
  465. payload = string(data)
  466. }
  467. idx := strings.Index(property, ".")
  468. refProperty := property
  469. if idx > 0 {
  470. refProperty = strings.ReplaceAll(refProperty, ".", "\\.")
  471. val := gjson.Get(payload, refProperty)
  472. if val.Exists() {
  473. return val
  474. }
  475. }
  476. return gjson.Get(payload, property)
  477. }
  478. func mapEqual(m1, m2 map[string]string) bool {
  479. if len(m1) != len(m2) {
  480. return false
  481. }
  482. for k1, v1 := range m1 {
  483. if v2, ok := m2[k1]; !ok || v1 != v2 {
  484. return false
  485. }
  486. }
  487. return true
  488. }