client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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 scaleway
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "strconv"
  21. "strings"
  22. "time"
  23. smapi "github.com/scaleway/scaleway-sdk-go/api/secret/v1beta1"
  24. "github.com/scaleway/scaleway-sdk-go/scw"
  25. "github.com/tidwall/gjson"
  26. corev1 "k8s.io/api/core/v1"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. "github.com/external-secrets/external-secrets/runtime/find"
  29. )
  30. var errNoSecretForName = errors.New("no secret for this name")
  31. type client struct {
  32. api secretAPI
  33. projectID string
  34. cache cache
  35. }
  36. const (
  37. refTypeName = "name"
  38. refTypeID = "id"
  39. refTypePath = "path"
  40. )
  41. type scwSecretRef struct {
  42. RefType string
  43. Value string
  44. }
  45. func (r scwSecretRef) String() string {
  46. return fmt.Sprintf("%s:%s", r.RefType, r.Value)
  47. }
  48. func decodeScwSecretRef(key string) (*scwSecretRef, error) {
  49. sepIndex := strings.IndexRune(key, ':')
  50. if sepIndex < 0 {
  51. return nil, errors.New("invalid secret reference: missing colon ':'")
  52. }
  53. return &scwSecretRef{
  54. RefType: key[:sepIndex],
  55. Value: key[sepIndex+1:],
  56. }, nil
  57. }
  58. func (c *client) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  59. scwRef, err := decodeScwSecretRef(ref.Key)
  60. if err != nil {
  61. return nil, err
  62. }
  63. versionSpec := "latest_enabled"
  64. if ref.Version != "" {
  65. versionSpec = ref.Version
  66. }
  67. value, err := c.accessSecretVersion(ctx, scwRef, versionSpec)
  68. if err != nil {
  69. //nolint:errorlint
  70. if _, isNotFoundErr := err.(*scw.ResourceNotFoundError); isNotFoundErr {
  71. return nil, esv1.NoSecretError{}
  72. } else if errors.Is(err, errNoSecretForName) {
  73. return nil, esv1.NoSecretError{}
  74. }
  75. return nil, err
  76. }
  77. if ref.Property != "" {
  78. extracted, err := extractJSONProperty(value, ref.Property)
  79. if err != nil {
  80. return nil, err
  81. }
  82. value = extracted
  83. }
  84. return value, nil
  85. }
  86. func (c *client) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1.PushSecretData) error {
  87. if data.GetSecretKey() == "" {
  88. return errors.New("pushing the whole secret is not yet implemented")
  89. }
  90. value := secret.Data[data.GetSecretKey()]
  91. scwRef, err := decodeScwSecretRef(data.GetRemoteKey())
  92. if err != nil {
  93. return err
  94. }
  95. listSecretReq := &smapi.ListSecretsRequest{
  96. ProjectID: &c.projectID,
  97. Page: scw.Int32Ptr(1),
  98. PageSize: scw.Uint32Ptr(1),
  99. }
  100. var secretName string
  101. secretPath := "/"
  102. switch scwRef.RefType {
  103. case refTypeName:
  104. listSecretReq.Name = &scwRef.Value
  105. secretName = scwRef.Value
  106. case refTypePath:
  107. name, path, ok := splitNameAndPath(scwRef.Value)
  108. if !ok {
  109. return errors.New("ref is not a path")
  110. }
  111. listSecretReq.Name = &name
  112. listSecretReq.Path = &path
  113. secretName = name
  114. secretPath = path
  115. default:
  116. return errors.New("secrets can only be pushed by name or path")
  117. }
  118. var secretID string
  119. existingSecretVersion := int64(-1)
  120. // list secret by ref
  121. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  122. if err != nil {
  123. return err
  124. }
  125. // secret exists
  126. if len(listSecrets.Secrets) > 0 {
  127. secretID = listSecrets.Secrets[0].ID
  128. // get the latest version
  129. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  130. SecretID: secretID,
  131. Revision: "latest",
  132. }, scw.WithContext(ctx))
  133. if err != nil {
  134. if _, ok := errors.AsType[*scw.ResourceNotFoundError](err); !ok {
  135. return err
  136. }
  137. } else {
  138. existingSecretVersion = int64(secretVersion.Revision)
  139. }
  140. if existingSecretVersion != -1 {
  141. data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  142. if err != nil {
  143. return err
  144. }
  145. if bytes.Equal(data, value) {
  146. // No change to push.
  147. return nil
  148. }
  149. }
  150. } else {
  151. secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
  152. ProjectID: c.projectID,
  153. Name: secretName,
  154. Path: &secretPath,
  155. }, scw.WithContext(ctx))
  156. if err != nil {
  157. return err
  158. }
  159. secretID = secret.ID
  160. }
  161. // Finally, we push the new secret version.
  162. createSecretVersionRequest := smapi.CreateSecretVersionRequest{
  163. SecretID: secretID,
  164. Data: value,
  165. }
  166. createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
  167. if err != nil {
  168. return err
  169. }
  170. c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
  171. if existingSecretVersion != -1 {
  172. _, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
  173. SecretID: secretID,
  174. Revision: fmt.Sprintf("%d", existingSecretVersion),
  175. })
  176. if err != nil {
  177. return err
  178. }
  179. }
  180. return nil
  181. }
  182. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
  183. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  184. if err != nil {
  185. return err
  186. }
  187. listSecretReq := &smapi.ListSecretsRequest{
  188. ProjectID: &c.projectID,
  189. Page: scw.Int32Ptr(1),
  190. PageSize: scw.Uint32Ptr(1),
  191. }
  192. switch scwRef.RefType {
  193. case refTypeName:
  194. listSecretReq.Name = &scwRef.Value
  195. case refTypePath:
  196. name, path, ok := splitNameAndPath(scwRef.Value)
  197. if !ok {
  198. return errors.New("ref is not a path")
  199. }
  200. listSecretReq.Name = &name
  201. listSecretReq.Path = &path
  202. default:
  203. return errors.New("secrets can only be deleted by name or path")
  204. }
  205. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  206. if err != nil {
  207. return err
  208. }
  209. if len(listSecrets.Secrets) == 0 {
  210. return nil
  211. }
  212. request := smapi.DeleteSecretRequest{
  213. SecretID: listSecrets.Secrets[0].ID,
  214. }
  215. err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
  216. if err != nil {
  217. return err
  218. }
  219. return nil
  220. }
  221. func (c *client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  222. return false, errors.New("not implemented")
  223. }
  224. func (c *client) Validate() (esv1.ValidationResult, error) {
  225. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  226. defer cancel()
  227. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  228. ProjectID: &c.projectID,
  229. Page: scw.Int32Ptr(1),
  230. PageSize: scw.Uint32Ptr(0),
  231. }, scw.WithContext(ctx))
  232. if err != nil {
  233. return esv1.ValidationResultError, nil
  234. }
  235. return esv1.ValidationResultReady, nil
  236. }
  237. func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  238. rawData, err := c.GetSecret(ctx, ref)
  239. if err != nil {
  240. return nil, err
  241. }
  242. structuredData := make(map[string]json.RawMessage)
  243. err = json.Unmarshal(rawData, &structuredData)
  244. if err != nil {
  245. // Do not return the raw error as json.Unmarshal errors may contain
  246. // sensitive secret data in the error message
  247. return nil, errors.New("failed to unmarshal secret: invalid JSON format")
  248. }
  249. values := make(map[string][]byte)
  250. for key, value := range structuredData {
  251. values[key] = jsonToSecretData(value)
  252. }
  253. return values, nil
  254. }
  255. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  256. func (c *client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  257. request := smapi.ListSecretsRequest{
  258. ProjectID: &c.projectID,
  259. Page: scw.Int32Ptr(1),
  260. PageSize: scw.Uint32Ptr(50),
  261. }
  262. if ref.Path != nil {
  263. request.Path = ref.Path
  264. }
  265. var nameMatcher *find.Matcher
  266. if ref.Name != nil {
  267. var err error
  268. nameMatcher, err = find.New(*ref.Name)
  269. if err != nil {
  270. return nil, err
  271. }
  272. }
  273. for tag := range ref.Tags {
  274. request.Tags = append(request.Tags, tag)
  275. }
  276. results := map[string][]byte{}
  277. for done := false; !done; {
  278. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  279. if err != nil {
  280. return nil, err
  281. }
  282. totalFetched := c.safeConvertInt32(request.Page)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  283. done = totalFetched == response.TotalCount
  284. *request.Page++
  285. for _, secret := range response.Secrets {
  286. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  287. continue
  288. }
  289. accessReq := smapi.AccessSecretVersionRequest{
  290. Region: secret.Region,
  291. SecretID: secret.ID,
  292. Revision: "latest_enabled",
  293. }
  294. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  295. if err != nil {
  296. log.Error(err, "failed to access secret")
  297. continue
  298. }
  299. results[secret.Name] = accessResp.Data
  300. }
  301. }
  302. return results, nil
  303. }
  304. func (c *client) safeConvertInt32(page *int32) uint64 {
  305. if *page-1 < 0 {
  306. return 0
  307. }
  308. return uint64(*page - 1)
  309. }
  310. func (c *client) Close(context.Context) error {
  311. return nil
  312. }
  313. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  314. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  315. if secretRef.RefType == refTypeID && versionSpec != "" && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  316. secretID := secretRef.Value
  317. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  318. if err == nil {
  319. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  320. }
  321. }
  322. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  323. request := &smapi.ListSecretsRequest{
  324. ProjectID: &c.projectID,
  325. Page: scw.Int32Ptr(1),
  326. PageSize: scw.Uint32Ptr(1),
  327. }
  328. switch secretRef.RefType {
  329. case refTypeID:
  330. request := smapi.GetSecretVersionRequest{
  331. SecretID: secretRef.Value,
  332. Revision: versionSpec,
  333. }
  334. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  335. if err != nil {
  336. return nil, err
  337. }
  338. return c.accessSpecificSecretVersion(ctx, response.SecretID, response.Revision)
  339. case refTypeName:
  340. request.Name = &secretRef.Value
  341. case refTypePath:
  342. name, path, ok := splitNameAndPath(secretRef.Value)
  343. if !ok {
  344. return nil, errors.New("ref is not a path")
  345. }
  346. request.Name = &name
  347. request.Path = &path
  348. default:
  349. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  350. }
  351. response, err := c.api.ListSecrets(request, scw.WithContext(ctx))
  352. if err != nil {
  353. return nil, err
  354. }
  355. if len(response.Secrets) == 0 {
  356. return nil, errNoSecretForName
  357. }
  358. secretID := response.Secrets[0].ID
  359. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  360. SecretID: secretID,
  361. Revision: versionSpec,
  362. }, scw.WithContext(ctx))
  363. if err != nil {
  364. return nil, err
  365. }
  366. return c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  367. }
  368. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  369. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  370. if cacheHit {
  371. return cachedValue, nil
  372. }
  373. request := smapi.AccessSecretVersionRequest{
  374. SecretID: secretID,
  375. Revision: fmt.Sprintf("%d", revision),
  376. }
  377. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  378. if err != nil {
  379. return nil, err
  380. }
  381. return response.Data, nil
  382. }
  383. func jsonToSecretData(value json.RawMessage) []byte {
  384. var stringValue string
  385. err := json.Unmarshal(value, &stringValue)
  386. if err == nil {
  387. return []byte(stringValue)
  388. }
  389. return []byte(strings.TrimSpace(string(value)))
  390. }
  391. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  392. result := gjson.Get(string(secretData), property)
  393. if !result.Exists() {
  394. return nil, esv1.NoSecretError{}
  395. }
  396. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  397. }
  398. func splitNameAndPath(ref string) (name, path string, ok bool) {
  399. if !strings.HasPrefix(ref, "/") {
  400. return
  401. }
  402. s := strings.Split(ref, "/")
  403. name = s[len(s)-1]
  404. if len(s) == 2 {
  405. path = "/"
  406. } else {
  407. path = strings.Join(s[:len(s)-1], "/")
  408. }
  409. ok = true
  410. return
  411. }