client.go 13 KB

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