client.go 17 KB

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