client.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  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 secretmanager
  14. import (
  15. "context"
  16. "encoding/json"
  17. "errors"
  18. "fmt"
  19. "maps"
  20. "slices"
  21. "strconv"
  22. "strings"
  23. "time"
  24. secretmanager "cloud.google.com/go/secretmanager/apiv1"
  25. "cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
  26. "github.com/googleapis/gax-go/v2"
  27. "github.com/googleapis/gax-go/v2/apierror"
  28. "github.com/tidwall/gjson"
  29. "google.golang.org/api/iterator"
  30. "google.golang.org/genproto/protobuf/field_mask"
  31. "google.golang.org/grpc/codes"
  32. "google.golang.org/grpc/status"
  33. corev1 "k8s.io/api/core/v1"
  34. ctrl "sigs.k8s.io/controller-runtime"
  35. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  36. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  37. "github.com/external-secrets/external-secrets/runtime/constants"
  38. "github.com/external-secrets/external-secrets/runtime/esutils"
  39. "github.com/external-secrets/external-secrets/runtime/esutils/metadata"
  40. "github.com/external-secrets/external-secrets/runtime/find"
  41. "github.com/external-secrets/external-secrets/runtime/metrics"
  42. "github.com/external-secrets/external-secrets/runtime/util/locks"
  43. )
  44. const (
  45. // CloudPlatformRole is the OAuth2 scope required for GCP Cloud Platform access.
  46. CloudPlatformRole = "https://www.googleapis.com/auth/cloud-platform"
  47. defaultVersion = "latest"
  48. errGCPSMStore = "received invalid GCPSM SecretStore resource"
  49. errUnableGetCredentials = "unable to get credentials: %w"
  50. errClientClose = "unable to close SecretManager client: %w"
  51. errUnableProcessJSONCredentials = "failed to process the provided JSON credentials: %w"
  52. errUnableCreateGCPSMClient = "failed to create GCP secretmanager client: %w"
  53. errUninitalizedGCPProvider = "provider GCP is not initialized"
  54. errClientGetSecretAccess = "unable to access Secret from SecretManager Client: %w"
  55. errJSONSecretUnmarshal = "unable to unmarshal secret from JSON: %w"
  56. errInvalidStore = "invalid store"
  57. errInvalidStoreSpec = "invalid store spec"
  58. errInvalidStoreProv = "invalid store provider"
  59. errInvalidGCPProv = "invalid gcp secrets manager provider"
  60. errInvalidAuthSecretRef = "invalid auth secret data: %w"
  61. errInvalidWISARef = "invalid workload identity service account reference: %w"
  62. errUnexpectedFindOperator = "unexpected find operator"
  63. managedByKey = "managed-by"
  64. managedByValue = "external-secrets"
  65. providerName = "GCPSecretManager"
  66. topicsKey = "topics"
  67. globalSecretPath = "projects/%s/secrets/%s"
  68. globalSecretParentPath = "projects/%s"
  69. regionalSecretParentPath = "projects/%s/locations/%s"
  70. regionalSecretPath = "projects/%s/locations/%s/secrets/%s"
  71. globalSecretVersionsPath = "projects/%s/secrets/%s/versions/%s"
  72. regionalSecretVersionsPath = "projects/%s/locations/%s/secrets/%s/versions/%s"
  73. )
  74. // Client represents a Google Cloud Platform Secret Manager client.
  75. type Client struct {
  76. smClient GoogleSecretManagerClient
  77. kube kclient.Client
  78. store *esv1.GCPSMProvider
  79. storeKind string
  80. // namespace of the external secret
  81. namespace string
  82. workloadIdentity *workloadIdentity
  83. }
  84. // GoogleSecretManagerClient defines the interface for interacting with Google Secret Manager.
  85. type GoogleSecretManagerClient interface {
  86. DeleteSecret(ctx context.Context, req *secretmanagerpb.DeleteSecretRequest, opts ...gax.CallOption) error
  87. AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
  88. ListSecrets(ctx context.Context, req *secretmanagerpb.ListSecretsRequest, opts ...gax.CallOption) *secretmanager.SecretIterator
  89. AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
  90. CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
  91. Close() error
  92. GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
  93. UpdateSecret(context.Context, *secretmanagerpb.UpdateSecretRequest, ...gax.CallOption) (*secretmanagerpb.Secret, error)
  94. ListSecretVersions(ctx context.Context, req *secretmanagerpb.ListSecretVersionsRequest, opts ...gax.CallOption) *secretmanager.SecretVersionIterator
  95. }
  96. var log = ctrl.Log.WithName("provider").WithName("gcp").WithName("secretsmanager")
  97. // DeleteSecret deletes a secret from Google Cloud Secret Manager.
  98. func (c *Client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
  99. name := getName(c.store.ProjectID, c.store.Location, remoteRef.GetRemoteKey())
  100. gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  101. Name: name,
  102. })
  103. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
  104. if err != nil {
  105. if status.Code(err) == codes.NotFound {
  106. return nil
  107. }
  108. return err
  109. }
  110. if manager, ok := gcpSecret.Labels[managedByKey]; !ok || manager != managedByValue {
  111. return nil
  112. }
  113. deleteSecretVersionReq := &secretmanagerpb.DeleteSecretRequest{
  114. Name: name,
  115. Etag: gcpSecret.Etag,
  116. }
  117. err = c.smClient.DeleteSecret(ctx, deleteSecretVersionReq)
  118. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMDeleteSecret, err)
  119. return err
  120. }
  121. func parseError(err error) error {
  122. var gerr *apierror.APIError
  123. if errors.As(err, &gerr) && gerr.GRPCStatus().Code() == codes.NotFound {
  124. return esv1.NoSecretError{}
  125. }
  126. return err
  127. }
  128. // SecretExists checks if a secret exists in Google Cloud Secret Manager and has at least one accessible version.
  129. func (c *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  130. secretName := getName(c.store.ProjectID, c.store.Location, ref.GetRemoteKey())
  131. _, err := getLatestEnabledVersion(ctx, c.smClient, secretName)
  132. if err != nil {
  133. if status.Code(err) == codes.NotFound {
  134. return false, nil
  135. }
  136. return false, err
  137. }
  138. return true, nil
  139. }
  140. // PushSecret pushes a kubernetes secret key into gcp provider Secret.
  141. func (c *Client) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData esv1.PushSecretData) error {
  142. var (
  143. payload []byte
  144. err error
  145. )
  146. if pushSecretData.GetSecretKey() == "" {
  147. // Must convert secret values to string, otherwise data will be sent as base64 to Vault
  148. secretStringVal := make(map[string]string)
  149. for k, v := range secret.Data {
  150. secretStringVal[k] = string(v)
  151. }
  152. payload, err = esutils.JSONMarshal(secretStringVal)
  153. if err != nil {
  154. return fmt.Errorf("failed to serialize secret content as JSON: %w", err)
  155. }
  156. } else {
  157. payload = secret.Data[pushSecretData.GetSecretKey()]
  158. }
  159. secretName := getName(c.store.ProjectID, c.store.Location, pushSecretData.GetRemoteKey())
  160. gcpSecret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  161. Name: secretName,
  162. })
  163. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMGetSecret, err)
  164. if err != nil {
  165. if status.Code(err) != codes.NotFound {
  166. return err
  167. }
  168. replication := &secretmanagerpb.Replication{
  169. Replication: &secretmanagerpb.Replication_Automatic_{
  170. Automatic: &secretmanagerpb.Replication_Automatic{},
  171. },
  172. }
  173. if pushSecretData.GetMetadata() != nil {
  174. replica := &secretmanagerpb.Replication_UserManaged_Replica{}
  175. var err error
  176. meta, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](pushSecretData.GetMetadata())
  177. if err != nil {
  178. return fmt.Errorf("failed to parse PushSecret metadata: %w", err)
  179. }
  180. if meta != nil && meta.Spec.ReplicationLocation != "" {
  181. replica.Location = meta.Spec.ReplicationLocation
  182. }
  183. if meta != nil && meta.Spec.CMEKKeyName != "" {
  184. replica.CustomerManagedEncryption = &secretmanagerpb.CustomerManagedEncryption{
  185. KmsKeyName: meta.Spec.CMEKKeyName,
  186. }
  187. }
  188. replication = &secretmanagerpb.Replication{
  189. Replication: &secretmanagerpb.Replication_UserManaged_{
  190. UserManaged: &secretmanagerpb.Replication_UserManaged{
  191. Replicas: []*secretmanagerpb.Replication_UserManaged_Replica{
  192. replica,
  193. },
  194. },
  195. },
  196. }
  197. }
  198. parent := getParentName(c.store.ProjectID, c.store.Location)
  199. scrt := &secretmanagerpb.Secret{
  200. Labels: map[string]string{
  201. managedByKey: managedByValue,
  202. },
  203. }
  204. // fix: cannot set Replication at all when using regional Secrets.
  205. if c.store.Location == "" {
  206. scrt.Replication = replication
  207. }
  208. topics, err := esutils.FetchValueFromMetadata(topicsKey, pushSecretData.GetMetadata(), []any{})
  209. if err != nil {
  210. return fmt.Errorf("failed to fetch topics from metadata: %w", err)
  211. }
  212. for _, t := range topics {
  213. name, ok := t.(string)
  214. if !ok {
  215. return fmt.Errorf("invalid topic type")
  216. }
  217. scrt.Topics = append(scrt.Topics, &secretmanagerpb.Topic{
  218. Name: name,
  219. })
  220. }
  221. gcpSecret, err = c.smClient.CreateSecret(ctx, &secretmanagerpb.CreateSecretRequest{
  222. Parent: parent,
  223. SecretId: pushSecretData.GetRemoteKey(),
  224. Secret: scrt,
  225. })
  226. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMCreateSecret, err)
  227. if err != nil {
  228. return err
  229. }
  230. }
  231. builder, err := newPushSecretBuilder(payload, pushSecretData)
  232. if err != nil {
  233. return err
  234. }
  235. annotations, labels, topics, err := builder.buildMetadata(gcpSecret.Annotations, gcpSecret.Labels, gcpSecret.Topics)
  236. if err != nil {
  237. return err
  238. }
  239. // Comparing with a pointer based slice doesn't work so we are converting
  240. // it to a string slice.
  241. existingTopics := make([]string, 0, len(gcpSecret.Topics))
  242. for _, t := range gcpSecret.Topics {
  243. existingTopics = append(existingTopics, t.Name)
  244. }
  245. if !maps.Equal(gcpSecret.Annotations, annotations) || !maps.Equal(gcpSecret.Labels, labels) || !slices.Equal(existingTopics, topics) {
  246. scrt := &secretmanagerpb.Secret{
  247. Name: gcpSecret.Name,
  248. Etag: gcpSecret.Etag,
  249. Labels: labels,
  250. Annotations: annotations,
  251. Topics: gcpSecret.Topics,
  252. }
  253. if c.store.Location != "" {
  254. scrt.Replication = &secretmanagerpb.Replication{
  255. Replication: &secretmanagerpb.Replication_UserManaged_{
  256. UserManaged: &secretmanagerpb.Replication_UserManaged{
  257. Replicas: []*secretmanagerpb.Replication_UserManaged_Replica{
  258. {
  259. Location: c.store.Location,
  260. },
  261. },
  262. },
  263. },
  264. }
  265. }
  266. _, err = c.smClient.UpdateSecret(ctx, &secretmanagerpb.UpdateSecretRequest{
  267. Secret: scrt,
  268. UpdateMask: &field_mask.FieldMask{
  269. Paths: []string{"labels", "annotations"},
  270. },
  271. })
  272. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMUpdateSecret, err)
  273. if err != nil {
  274. return err
  275. }
  276. }
  277. unlock, err := locks.TryLock(providerName, secretName)
  278. if err != nil {
  279. return err
  280. }
  281. defer unlock()
  282. gcpVersion, err := c.smClient.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
  283. Name: fmt.Sprintf("%s/versions/latest", secretName),
  284. })
  285. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
  286. if err != nil && status.Code(err) != codes.NotFound {
  287. return err
  288. }
  289. if gcpVersion != nil && gcpVersion.Payload != nil && !builder.needUpdate(gcpVersion.Payload.Data) {
  290. return nil
  291. }
  292. var original []byte
  293. if gcpVersion != nil && gcpVersion.Payload != nil {
  294. original = gcpVersion.Payload.Data
  295. }
  296. data, err := builder.buildData(original)
  297. if err != nil {
  298. return err
  299. }
  300. parent := getName(c.store.ProjectID, c.store.Location, pushSecretData.GetRemoteKey())
  301. addSecretVersionReq := &secretmanagerpb.AddSecretVersionRequest{
  302. Parent: parent,
  303. Payload: &secretmanagerpb.SecretPayload{
  304. Data: data,
  305. },
  306. }
  307. _, err = c.smClient.AddSecretVersion(ctx, addSecretVersionReq)
  308. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAddSecretVersion, err)
  309. return err
  310. }
  311. // GetAllSecrets syncs multiple secrets from gcp provider into a single Kubernetes Secret.
  312. func (c *Client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  313. if ref.Name != nil {
  314. return c.findByName(ctx, ref)
  315. }
  316. if len(ref.Tags) > 0 {
  317. return c.findByTags(ctx, ref)
  318. }
  319. return nil, errors.New(errUnexpectedFindOperator)
  320. }
  321. func (c *Client) findByName(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  322. // regex matcher
  323. matcher, err := find.New(*ref.Name)
  324. if err != nil {
  325. return nil, err
  326. }
  327. parent := getParentName(c.store.ProjectID, c.store.Location)
  328. req := &secretmanagerpb.ListSecretsRequest{
  329. Parent: parent,
  330. }
  331. if ref.Path != nil {
  332. req.Filter = fmt.Sprintf("name:%s", *ref.Path)
  333. }
  334. // Call the API.
  335. it := c.smClient.ListSecrets(ctx, req)
  336. secretMap := make(map[string][]byte)
  337. var resp *secretmanagerpb.Secret
  338. defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
  339. for {
  340. resp, err = it.Next()
  341. if errors.Is(err, iterator.Done) {
  342. break
  343. }
  344. if err != nil {
  345. return nil, fmt.Errorf("failed to list secrets: %w", err)
  346. }
  347. log.V(1).Info("gcp sm findByName found", "secrets", strconv.Itoa(it.PageInfo().Remaining()))
  348. key := c.trimName(resp.Name)
  349. // If we don't match we skip.
  350. // Also, if we have path, and it is not at the beguining we skip.
  351. // We have to check if path is at the beguining of the key because
  352. // there is no way to create a `name:%s*` (starts with) filter
  353. // At https://cloud.google.com/secret-manager/docs/filtering you can use `*`
  354. // but not like that it seems.
  355. if !matcher.MatchName(key) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
  356. continue
  357. }
  358. log.V(1).Info("gcp sm findByName matches", "name", resp.Name)
  359. secretMap[key], err = c.getData(ctx, key)
  360. if err != nil {
  361. return nil, err
  362. }
  363. }
  364. return esutils.ConvertKeys(ref.ConversionStrategy, secretMap)
  365. }
  366. func (c *Client) getData(ctx context.Context, key string) ([]byte, error) {
  367. dataRef := esv1.ExternalSecretDataRemoteRef{
  368. Key: key,
  369. }
  370. data, err := c.GetSecret(ctx, dataRef)
  371. if err != nil {
  372. return []byte(""), err
  373. }
  374. return data, nil
  375. }
  376. func (c *Client) findByTags(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  377. var tagFilter string
  378. for k, v := range ref.Tags {
  379. tagFilter = fmt.Sprintf("%slabels.%s=%s ", tagFilter, k, v)
  380. }
  381. tagFilter = strings.TrimSuffix(tagFilter, " ")
  382. if ref.Path != nil {
  383. tagFilter = fmt.Sprintf("%s name:%s", tagFilter, *ref.Path)
  384. }
  385. req := &secretmanagerpb.ListSecretsRequest{
  386. Parent: fmt.Sprintf("projects/%s", c.store.ProjectID),
  387. }
  388. log.V(1).Info("gcp sm findByTags", "tagFilter", tagFilter)
  389. req.Filter = tagFilter
  390. // Call the API.
  391. it := c.smClient.ListSecrets(ctx, req)
  392. var resp *secretmanagerpb.Secret
  393. var err error
  394. defer metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMListSecrets, err)
  395. secretMap := make(map[string][]byte)
  396. for {
  397. resp, err = it.Next()
  398. if errors.Is(err, iterator.Done) {
  399. break
  400. }
  401. if err != nil {
  402. return nil, fmt.Errorf("failed to list secrets: %w", err)
  403. }
  404. key := c.trimName(resp.Name)
  405. if ref.Path != nil && !strings.HasPrefix(key, *ref.Path) {
  406. continue
  407. }
  408. log.V(1).Info("gcp sm findByTags matches tags", "name", resp.Name)
  409. secretMap[key], err = c.getData(ctx, key)
  410. if err != nil {
  411. return nil, err
  412. }
  413. }
  414. return esutils.ConvertKeys(ref.ConversionStrategy, secretMap)
  415. }
  416. func (c *Client) trimName(name string) string {
  417. projectIDNumuber := c.extractProjectIDNumber(name)
  418. prefix := getParentName(projectIDNumuber, c.store.Location)
  419. key := strings.TrimPrefix(name, fmt.Sprintf("%s/secrets/", prefix))
  420. return key
  421. }
  422. // extractProjectIDNumber grabs the project id from the full name returned by gcp api
  423. // gcp api seems to always return the number and not the project name
  424. // (and users would always use the name, while requests accept both).
  425. func (c *Client) extractProjectIDNumber(secretFullName string) string {
  426. s := strings.Split(secretFullName, "/")
  427. ProjectIDNumuber := s[1]
  428. return ProjectIDNumuber
  429. }
  430. // GetSecret returns a single secret from the provider.
  431. func (c *Client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  432. if esutils.IsNil(c.smClient) || c.store.ProjectID == "" {
  433. return nil, errors.New(errUninitalizedGCPProvider)
  434. }
  435. if ref.MetadataPolicy == esv1.ExternalSecretMetadataPolicyFetch {
  436. return c.getSecretMetadata(ctx, ref)
  437. }
  438. version := ref.Version
  439. if version == "" {
  440. version = defaultVersion
  441. }
  442. name := fmt.Sprintf(globalSecretVersionsPath, c.store.ProjectID, ref.Key, version)
  443. if c.store.Location != "" {
  444. name = fmt.Sprintf(regionalSecretVersionsPath, c.store.ProjectID, c.store.Location, ref.Key, version)
  445. }
  446. req := &secretmanagerpb.AccessSecretVersionRequest{
  447. Name: name,
  448. }
  449. result, err := c.smClient.AccessSecretVersion(ctx, req)
  450. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
  451. if err != nil && c.store.SecretVersionSelectionPolicy == esv1.SecretVersionSelectionPolicyLatestOrFetch &&
  452. ref.Version == "" && isErrSecretDestroyedOrDisabled(err) {
  453. // if the secret is destroyed or disabled, and we are configured to get the latest enabled secret,
  454. // we need to get the latest enabled secret
  455. // Extract the secret name from the version name for ListSecretVersions
  456. secretName := fmt.Sprintf(globalSecretPath, c.store.ProjectID, ref.Key)
  457. if c.store.Location != "" {
  458. secretName = fmt.Sprintf(regionalSecretPath, c.store.ProjectID, c.store.Location, ref.Key)
  459. }
  460. result, err = getLatestEnabledVersion(ctx, c.smClient, secretName)
  461. }
  462. if err != nil {
  463. err = parseError(err)
  464. return nil, fmt.Errorf(errClientGetSecretAccess, err)
  465. }
  466. if ref.Property == "" {
  467. if result.Payload.Data != nil {
  468. return result.Payload.Data, nil
  469. }
  470. return nil, fmt.Errorf("invalid secret received. no secret string for key: %s", ref.Key)
  471. }
  472. val, err := getDataByProperty(result.Payload.Data, ref.Property)
  473. if err != nil {
  474. return nil, err
  475. }
  476. if !val.Exists() {
  477. return nil, fmt.Errorf("key %s does not exist in secret %s", ref.Property, ref.Key)
  478. }
  479. return []byte(val.String()), nil
  480. }
  481. func (c *Client) getSecretMetadata(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  482. name := getName(c.store.ProjectID, c.store.Location, ref.Key)
  483. secret, err := c.smClient.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
  484. Name: name,
  485. })
  486. err = parseError(err)
  487. if err != nil {
  488. return nil, fmt.Errorf(errClientGetSecretAccess, err)
  489. }
  490. const (
  491. annotations = "annotations"
  492. labels = "labels"
  493. )
  494. if annotation := c.extractMetadataKey(ref.Property, annotations); annotation != "" {
  495. v, ok := secret.GetAnnotations()[annotation]
  496. if !ok {
  497. return nil, fmt.Errorf("annotation with key %s does not exist in secret %s", annotation, ref.Key)
  498. }
  499. return []byte(v), nil
  500. }
  501. if label := c.extractMetadataKey(ref.Property, labels); label != "" {
  502. v, ok := secret.GetLabels()[label]
  503. if !ok {
  504. return nil, fmt.Errorf("label with key %s does not exist in secret %s", label, ref.Key)
  505. }
  506. return []byte(v), nil
  507. }
  508. if ref.Property == annotations {
  509. j, err := json.Marshal(secret.GetAnnotations())
  510. if err != nil {
  511. return nil, fmt.Errorf("faild marshaling annotations into json: %w", err)
  512. }
  513. return j, nil
  514. }
  515. if ref.Property == labels {
  516. j, err := json.Marshal(secret.GetLabels())
  517. if err != nil {
  518. return nil, fmt.Errorf("faild marshaling labels into json: %w", err)
  519. }
  520. return j, nil
  521. }
  522. if ref.Property != "" {
  523. return nil, fmt.Errorf("invalid property %s: metadata property should start with either %s or %s", ref.Property, annotations, labels)
  524. }
  525. j, err := json.Marshal(map[string]map[string]string{
  526. "annotations": secret.GetAnnotations(),
  527. "labels": secret.GetLabels(),
  528. })
  529. if err != nil {
  530. return nil, fmt.Errorf("faild marshaling metadata map into json: %w", err)
  531. }
  532. return j, nil
  533. }
  534. func (c *Client) extractMetadataKey(s, p string) string {
  535. prefix := p + "."
  536. if !strings.HasPrefix(s, prefix) {
  537. return ""
  538. }
  539. return strings.TrimPrefix(s, prefix)
  540. }
  541. // GetSecretMap returns multiple k/v pairs from the provider.
  542. func (c *Client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  543. if c.smClient == nil || c.store.ProjectID == "" {
  544. return nil, errors.New(errUninitalizedGCPProvider)
  545. }
  546. data, err := c.GetSecret(ctx, ref)
  547. if err != nil {
  548. return nil, err
  549. }
  550. kv := make(map[string]json.RawMessage)
  551. err = json.Unmarshal(data, &kv)
  552. if err != nil {
  553. return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
  554. }
  555. secretData := make(map[string][]byte)
  556. for k, v := range kv {
  557. var strVal string
  558. err = json.Unmarshal(v, &strVal)
  559. if err == nil {
  560. secretData[k] = []byte(strVal)
  561. } else {
  562. secretData[k] = v
  563. }
  564. }
  565. return secretData, nil
  566. }
  567. // Close closes the Google Cloud Secret Manager client connection.
  568. func (c *Client) Close(_ context.Context) error {
  569. var err error
  570. if c.smClient != nil {
  571. err = c.smClient.Close()
  572. }
  573. if c.workloadIdentity != nil {
  574. err = c.workloadIdentity.Close()
  575. }
  576. useMu.Unlock()
  577. if err != nil {
  578. return fmt.Errorf(errClientClose, err)
  579. }
  580. return nil
  581. }
  582. // Validate performs validation of the Google Cloud Secret Manager client configuration.
  583. func (c *Client) Validate() (esv1.ValidationResult, error) {
  584. if c.storeKind == esv1.ClusterSecretStoreKind && isReferentSpec(c.store) {
  585. return esv1.ValidationResultUnknown, nil
  586. }
  587. return esv1.ValidationResultReady, nil
  588. }
  589. func getDataByProperty(data []byte, property string) (gjson.Result, error) {
  590. if !json.Valid(data) {
  591. return gjson.Result{}, errors.New(errJSONSecretUnmarshal)
  592. }
  593. var payload string
  594. if data != nil {
  595. payload = string(data)
  596. }
  597. idx := strings.Index(property, ".")
  598. refProperty := property
  599. if idx > 0 {
  600. refProperty = strings.ReplaceAll(refProperty, ".", "\\.")
  601. val := gjson.Get(payload, refProperty)
  602. if val.Exists() {
  603. return val, nil
  604. }
  605. }
  606. return gjson.Get(payload, property), nil
  607. }
  608. // getName constructs the full resource name for a GCP secret.
  609. // If location is provided, it returns a regional secret path; otherwise, it returns a global secret path.
  610. func getName(projectID, location, key string) string {
  611. if location != "" {
  612. return fmt.Sprintf(regionalSecretPath, projectID, location, key)
  613. }
  614. return fmt.Sprintf(globalSecretPath, projectID, key)
  615. }
  616. // getParentName constructs the parent resource name for listing secrets in GCP.
  617. // If location is provided, it returns a regional parent path; otherwise, it returns a global parent path.
  618. func getParentName(projectID, location string) string {
  619. if location != "" {
  620. return fmt.Sprintf(regionalSecretParentPath, projectID, location)
  621. }
  622. return fmt.Sprintf(globalSecretParentPath, projectID)
  623. }
  624. // isErrSecretDestroyedOrDisabled checks if an error indicates that a secret version
  625. // is in DESTROYED or DISABLED state. These states occur when a version has been
  626. // explicitly destroyed or disabled and cannot be accessed.
  627. func isErrSecretDestroyedOrDisabled(err error) bool {
  628. st, _ := status.FromError(err)
  629. return st.Code() == codes.FailedPrecondition &&
  630. (strings.Contains(st.Message(), "DESTROYED state") || strings.Contains(st.Message(), "DISABLED state"))
  631. }
  632. // getLatestEnabledVersion retrieves the most recent enabled version of a secret.
  633. // It lists all enabled versions and selects the one with the latest creation time.
  634. // If no enabled versions are found, it falls back to accessing the "latest" version,
  635. // which will return an appropriate error if the secret doesn't exist or has no accessible versions.
  636. //
  637. // Note: The version.Name field contains the full resource path (e.g., projects/*/secrets/*/versions/*),
  638. // not just the version number, so it's used directly in the AccessSecretVersion request.
  639. func getLatestEnabledVersion(ctx context.Context, client GoogleSecretManagerClient, name string) (*secretmanagerpb.AccessSecretVersionResponse, error) {
  640. iter := client.ListSecretVersions(ctx, &secretmanagerpb.ListSecretVersionsRequest{
  641. Parent: name,
  642. Filter: "state:ENABLED",
  643. })
  644. latestCreateTime := time.Unix(0, 0)
  645. var versionName string
  646. for {
  647. version, err := iter.Next()
  648. if err != nil {
  649. if errors.Is(err, iterator.Done) {
  650. break
  651. }
  652. return nil, err
  653. }
  654. if version.CreateTime.AsTime().After(latestCreateTime) {
  655. latestCreateTime = version.CreateTime.AsTime()
  656. versionName = version.Name
  657. }
  658. }
  659. // If no enabled versions found, fall back to "latest"
  660. // This will return the appropriate error (NotFound, FailedPrecondition, etc.)
  661. if versionName == "" {
  662. versionName = fmt.Sprintf("%s/versions/latest", name)
  663. }
  664. req := &secretmanagerpb.AccessSecretVersionRequest{
  665. Name: versionName,
  666. }
  667. version, err := client.AccessSecretVersion(ctx, req)
  668. metrics.ObserveAPICall(constants.ProviderGCPSM, constants.CallGCPSMAccessSecretVersion, err)
  669. if err != nil {
  670. return nil, err
  671. }
  672. return version, nil
  673. }