client.go 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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 ngrok provides integration with the ngrok API for secret management
  14. package ngrok
  15. import (
  16. "context"
  17. "crypto/sha256"
  18. "encoding/hex"
  19. "encoding/json"
  20. "errors"
  21. "fmt"
  22. "sync"
  23. "time"
  24. "github.com/ngrok/ngrok-api-go/v7"
  25. corev1 "k8s.io/api/core/v1"
  26. v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/runtime/esutils/metadata"
  29. )
  30. const (
  31. defaultDescription = "Managed by External Secrets Operator"
  32. defaultListTimeout = 1 * time.Minute
  33. )
  34. var (
  35. errWriteOnlyOperations = errors.New("not implemented - the ngrok provider only supports write operations")
  36. errVaultDoesNotExist = errors.New("vault does not exist")
  37. errVaultSecretDoesNotExist = errors.New("vault secret does not exist")
  38. )
  39. // PushSecretMetadataSpec defines the structure for metadata used when pushing secrets to ngrok.
  40. type PushSecretMetadataSpec struct {
  41. // The description of the secret in the ngrok API.
  42. Description string `json:"description,omitempty"`
  43. // Custom metadata to be merged with generated metadata for the secret in the ngrok API.
  44. // This metadata is different from Kubernetes metadata.
  45. Metadata map[string]string `json:"metadata,omitempty"`
  46. }
  47. // VaultClient defines interface for interactions with ngrok vault API.
  48. type VaultClient interface {
  49. Create(context.Context, *ngrok.VaultCreate) (*ngrok.Vault, error)
  50. Get(context.Context, string) (*ngrok.Vault, error)
  51. GetSecretsByVault(string, *ngrok.Paging) ngrok.Iter[*ngrok.Secret]
  52. List(*ngrok.Paging) ngrok.Iter[*ngrok.Vault]
  53. }
  54. // SecretsClient defines interface for interactions with ngrok secrets API.
  55. type SecretsClient interface {
  56. Create(context.Context, *ngrok.SecretCreate) (*ngrok.Secret, error)
  57. Delete(context.Context, string) error
  58. Get(context.Context, string) (*ngrok.Secret, error)
  59. List(*ngrok.Paging) ngrok.Iter[*ngrok.Secret]
  60. Update(context.Context, *ngrok.SecretUpdate) (*ngrok.Secret, error)
  61. }
  62. type client struct {
  63. vaultClient VaultClient
  64. secretsClient SecretsClient
  65. vaultName string
  66. vaultID string
  67. vaultIDMu sync.RWMutex
  68. }
  69. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  70. // First, make sure the vault name still matches the ID we have stored. If not, we have to look it up again.
  71. err := c.verifyVaultNameStillMatchesID(ctx)
  72. if err != nil {
  73. return fmt.Errorf("failed to verify vault name still matches ID: %w", err)
  74. }
  75. // Prepare the secret data for pushing
  76. var value []byte
  77. // If key is specified, get the value from the secret data
  78. if data.GetSecretKey() != "" {
  79. var ok bool
  80. value, ok = secret.Data[data.GetSecretKey()]
  81. if !ok {
  82. return fmt.Errorf("key %s not found in secret", data.GetSecretKey())
  83. }
  84. } else { // otherwise, marshal the entire secret data as JSON
  85. value, err = json.Marshal(secret.Data)
  86. if err != nil {
  87. return fmt.Errorf("json.Marshal failed with error: %w", err)
  88. }
  89. }
  90. // Calculate the checksum of the value to add to metadata
  91. valueChecksum := sha256.Sum256(value)
  92. psmd, err := parseAndDefaultMetadata(data.GetMetadata())
  93. if err != nil {
  94. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  95. }
  96. psmd.Metadata["_sha256"] = hex.EncodeToString(valueChecksum[:])
  97. metadataJSON, err := json.Marshal(psmd.Metadata)
  98. if err != nil {
  99. return fmt.Errorf("failed to marshal metadata for ngrok api: %w", err)
  100. }
  101. // Check if the secret already exists in the vault
  102. existingSecret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), data.GetRemoteKey())
  103. if err != nil {
  104. if !errors.Is(err, errVaultSecretDoesNotExist) {
  105. return fmt.Errorf("failed to get secret: %w", err)
  106. }
  107. // If the secret does not exist, create it
  108. _, err = c.secretsClient.Create(ctx, &ngrok.SecretCreate{
  109. VaultID: c.getVaultID(),
  110. Name: data.GetRemoteKey(),
  111. Value: string(value),
  112. Metadata: string(metadataJSON),
  113. Description: psmd.Description,
  114. })
  115. return err
  116. }
  117. // If the secret exists, update it
  118. _, err = c.secretsClient.Update(ctx, &ngrok.SecretUpdate{
  119. ID: existingSecret.ID,
  120. Value: new(string(value)),
  121. Metadata: new(string(metadataJSON)),
  122. Description: new(psmd.Description),
  123. })
  124. return err
  125. }
  126. func (c *client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  127. err := c.verifyVaultNameStillMatchesID(ctx)
  128. if errors.Is(err, errVaultDoesNotExist) {
  129. return false, nil
  130. }
  131. if err != nil {
  132. return false, err
  133. }
  134. // Implementation for checking if a secret exists in ngrok
  135. secret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), ref.GetRemoteKey())
  136. if errors.Is(err, errVaultDoesNotExist) || errors.Is(err, errVaultSecretDoesNotExist) {
  137. return false, nil
  138. }
  139. if err != nil {
  140. return false, fmt.Errorf("error fetching secret: %w", err)
  141. }
  142. return (secret != nil), nil
  143. }
  144. // DeleteSecret deletes a secret from ngrok by its reference.
  145. func (c *client) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  146. err := c.verifyVaultNameStillMatchesID(ctx)
  147. if errors.Is(err, errVaultDoesNotExist) {
  148. return nil
  149. } else if err != nil {
  150. return err
  151. }
  152. secret, err := c.getSecretByVaultIDAndName(ctx, c.getVaultID(), ref.GetRemoteKey())
  153. if errors.Is(err, errVaultDoesNotExist) || errors.Is(err, errVaultSecretDoesNotExist) {
  154. // If the secret or vault do not exist, we can consider it deleted.
  155. return nil
  156. }
  157. if err != nil {
  158. return err
  159. }
  160. if secret == nil {
  161. return nil
  162. }
  163. return c.secretsClient.Delete(ctx, secret.ID)
  164. }
  165. func (c *client) Validate() (esv1.ValidationResult, error) {
  166. // Validate the client can list secrets with a timeout. If we
  167. // can list secrets, we assume the client is valid(API keys, URL, etc.)
  168. iter := c.secretsClient.List(nil)
  169. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  170. defer cancel()
  171. for iter.Next(ctx) {
  172. return esv1.ValidationResultReady, nil
  173. }
  174. if iter.Err() != nil {
  175. return esv1.ValidationResultError, fmt.Errorf("store is not allowed to list secrets: %w", iter.Err())
  176. }
  177. return esv1.ValidationResultReady, nil
  178. }
  179. func (c *client) GetSecret(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  180. // Implementation for getting a secret from ngrok
  181. return nil, errWriteOnlyOperations
  182. }
  183. func (c *client) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  184. // Implementation for getting a map of secrets from ngrok
  185. return nil, errWriteOnlyOperations
  186. }
  187. func (c *client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  188. // Implementation for getting all secrets from ngrok
  189. return nil, errWriteOnlyOperations
  190. }
  191. func (c *client) Close(_ context.Context) error {
  192. return nil
  193. }
  194. func (c *client) verifyVaultNameStillMatchesID(ctx context.Context) error {
  195. vaultID := c.getVaultID()
  196. if vaultID == "" {
  197. return c.refreshVaultID(ctx)
  198. }
  199. vault, err := c.vaultClient.Get(ctx, vaultID)
  200. if err != nil || vault.Name != c.vaultName {
  201. return c.refreshVaultID(ctx)
  202. }
  203. return nil
  204. }
  205. // getVaultID safely retrieves the current vault ID.
  206. func (c *client) getVaultID() string {
  207. c.vaultIDMu.RLock()
  208. defer c.vaultIDMu.RUnlock()
  209. return c.vaultID
  210. }
  211. // setVaultID safely sets the vault ID.
  212. func (c *client) setVaultID(vaultID string) {
  213. c.vaultIDMu.Lock()
  214. defer c.vaultIDMu.Unlock()
  215. c.vaultID = vaultID
  216. }
  217. func (c *client) refreshVaultID(ctx context.Context) error {
  218. v, err := c.getVaultByName(ctx, c.vaultName)
  219. if err != nil {
  220. return fmt.Errorf("failed to refresh vault ID: %w", err)
  221. }
  222. c.setVaultID(v.ID)
  223. return nil
  224. }
  225. func (c *client) getVaultByName(ctx context.Context, name string) (*ngrok.Vault, error) {
  226. listCtx, cancel := context.WithTimeout(ctx, defaultListTimeout)
  227. defer cancel()
  228. iter := c.vaultClient.List(nil)
  229. for iter.Next(listCtx) {
  230. vault := iter.Item()
  231. if vault.Name == name {
  232. return vault, nil
  233. }
  234. }
  235. if iter.Err() != nil {
  236. return nil, iter.Err()
  237. }
  238. return nil, errVaultDoesNotExist
  239. }
  240. // getSecretByVaultIDAndName retrieves a secret by its vault ID and secret name.
  241. func (c *client) getSecretByVaultIDAndName(ctx context.Context, vaultID, name string) (*ngrok.Secret, error) {
  242. iter := c.vaultClient.GetSecretsByVault(vaultID, nil)
  243. for iter.Next(ctx) {
  244. secret := iter.Item()
  245. if secret.Name == name {
  246. return secret, nil
  247. }
  248. }
  249. if iter.Err() != nil {
  250. return nil, iter.Err()
  251. }
  252. return nil, fmt.Errorf("secret '%s' does not exist: %w", name, errVaultSecretDoesNotExist)
  253. }
  254. func parseAndDefaultMetadata(data *v1.JSON) (PushSecretMetadataSpec, error) {
  255. def := PushSecretMetadataSpec{
  256. Description: defaultDescription,
  257. Metadata: make(map[string]string),
  258. }
  259. res, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](data)
  260. if err != nil {
  261. return def, err
  262. }
  263. if res == nil {
  264. return def, nil
  265. }
  266. if res.Spec.Description != "" {
  267. def.Description = res.Spec.Description
  268. }
  269. if res.Spec.Metadata != nil {
  270. def.Metadata = res.Spec.Metadata
  271. }
  272. return def, nil
  273. }