client.go 16 KB

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