client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  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. var errNotNound *scw.ResourceNotFoundError
  135. if !errors.As(err, &errNotNound) {
  136. return err
  137. }
  138. } else {
  139. existingSecretVersion = int64(secretVersion.Revision)
  140. }
  141. if existingSecretVersion != -1 {
  142. data, err := c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  143. if err != nil {
  144. return err
  145. }
  146. if bytes.Equal(data, value) {
  147. // No change to push.
  148. return nil
  149. }
  150. }
  151. } else {
  152. secret, err := c.api.CreateSecret(&smapi.CreateSecretRequest{
  153. ProjectID: c.projectID,
  154. Name: secretName,
  155. Path: &secretPath,
  156. }, scw.WithContext(ctx))
  157. if err != nil {
  158. return err
  159. }
  160. secretID = secret.ID
  161. }
  162. // Finally, we push the new secret version.
  163. createSecretVersionRequest := smapi.CreateSecretVersionRequest{
  164. SecretID: secretID,
  165. Data: value,
  166. }
  167. createSecretVersionResponse, err := c.api.CreateSecretVersion(&createSecretVersionRequest, scw.WithContext(ctx))
  168. if err != nil {
  169. return err
  170. }
  171. c.cache.Put(secretID, createSecretVersionResponse.Revision, value)
  172. if existingSecretVersion != -1 {
  173. _, err := c.api.DisableSecretVersion(&smapi.DisableSecretVersionRequest{
  174. SecretID: secretID,
  175. Revision: fmt.Sprintf("%d", existingSecretVersion),
  176. })
  177. if err != nil {
  178. return err
  179. }
  180. }
  181. return nil
  182. }
  183. func (c *client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
  184. scwRef, err := decodeScwSecretRef(remoteRef.GetRemoteKey())
  185. if err != nil {
  186. return err
  187. }
  188. listSecretReq := &smapi.ListSecretsRequest{
  189. ProjectID: &c.projectID,
  190. Page: scw.Int32Ptr(1),
  191. PageSize: scw.Uint32Ptr(1),
  192. }
  193. switch scwRef.RefType {
  194. case refTypeName:
  195. listSecretReq.Name = &scwRef.Value
  196. case refTypePath:
  197. name, path, ok := splitNameAndPath(scwRef.Value)
  198. if !ok {
  199. return errors.New("ref is not a path")
  200. }
  201. listSecretReq.Name = &name
  202. listSecretReq.Path = &path
  203. default:
  204. return errors.New("secrets can only be deleted by name or path")
  205. }
  206. listSecrets, err := c.api.ListSecrets(listSecretReq, scw.WithContext(ctx))
  207. if err != nil {
  208. return err
  209. }
  210. if len(listSecrets.Secrets) == 0 {
  211. return nil
  212. }
  213. request := smapi.DeleteSecretRequest{
  214. SecretID: listSecrets.Secrets[0].ID,
  215. }
  216. err = c.api.DeleteSecret(&request, scw.WithContext(ctx))
  217. if err != nil {
  218. return err
  219. }
  220. return nil
  221. }
  222. func (c *client) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  223. return false, errors.New("not implemented")
  224. }
  225. func (c *client) Validate() (esv1.ValidationResult, error) {
  226. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  227. defer cancel()
  228. _, err := c.api.ListSecrets(&smapi.ListSecretsRequest{
  229. ProjectID: &c.projectID,
  230. Page: scw.Int32Ptr(1),
  231. PageSize: scw.Uint32Ptr(0),
  232. }, scw.WithContext(ctx))
  233. if err != nil {
  234. return esv1.ValidationResultError, nil
  235. }
  236. return esv1.ValidationResultReady, nil
  237. }
  238. func (c *client) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  239. rawData, err := c.GetSecret(ctx, ref)
  240. if err != nil {
  241. return nil, err
  242. }
  243. structuredData := make(map[string]json.RawMessage)
  244. err = json.Unmarshal(rawData, &structuredData)
  245. if err != nil {
  246. // Do not return the raw error as json.Unmarshal errors may contain
  247. // sensitive secret data in the error message
  248. return nil, errors.New("failed to unmarshal secret: invalid JSON format")
  249. }
  250. values := make(map[string][]byte)
  251. for key, value := range structuredData {
  252. values[key] = jsonToSecretData(value)
  253. }
  254. return values, nil
  255. }
  256. // GetAllSecrets lists secrets matching the given criteria and return their latest versions.
  257. func (c *client) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  258. request := smapi.ListSecretsRequest{
  259. ProjectID: &c.projectID,
  260. Page: scw.Int32Ptr(1),
  261. PageSize: scw.Uint32Ptr(50),
  262. }
  263. if ref.Path != nil {
  264. request.Path = ref.Path
  265. }
  266. var nameMatcher *find.Matcher
  267. if ref.Name != nil {
  268. var err error
  269. nameMatcher, err = find.New(*ref.Name)
  270. if err != nil {
  271. return nil, err
  272. }
  273. }
  274. for tag := range ref.Tags {
  275. request.Tags = append(request.Tags, tag)
  276. }
  277. results := map[string][]byte{}
  278. for done := false; !done; {
  279. response, err := c.api.ListSecrets(&request, scw.WithContext(ctx))
  280. if err != nil {
  281. return nil, err
  282. }
  283. totalFetched := c.safeConvertInt32(request.Page)*uint64(*request.PageSize) + uint64(len(response.Secrets))
  284. done = totalFetched == response.TotalCount
  285. *request.Page++
  286. for _, secret := range response.Secrets {
  287. if nameMatcher != nil && !nameMatcher.MatchName(secret.Name) {
  288. continue
  289. }
  290. accessReq := smapi.AccessSecretVersionRequest{
  291. Region: secret.Region,
  292. SecretID: secret.ID,
  293. Revision: "latest_enabled",
  294. }
  295. accessResp, err := c.api.AccessSecretVersion(&accessReq, scw.WithContext(ctx))
  296. if err != nil {
  297. log.Error(err, "failed to access secret")
  298. continue
  299. }
  300. results[secret.Name] = accessResp.Data
  301. }
  302. }
  303. return results, nil
  304. }
  305. func (c *client) safeConvertInt32(page *int32) uint64 {
  306. if *page-1 < 0 {
  307. return 0
  308. }
  309. return uint64(*page - 1)
  310. }
  311. func (c *client) Close(context.Context) error {
  312. return nil
  313. }
  314. func (c *client) accessSecretVersion(ctx context.Context, secretRef *scwSecretRef, versionSpec string) ([]byte, error) {
  315. // if we have a secret id and a revision number, we can avoid an extra GetSecret()
  316. if secretRef.RefType == refTypeID && versionSpec != "" && '0' <= versionSpec[0] && versionSpec[0] <= '9' {
  317. secretID := secretRef.Value
  318. revision, err := strconv.ParseUint(versionSpec, 10, 32)
  319. if err == nil {
  320. return c.accessSpecificSecretVersion(ctx, secretID, uint32(revision))
  321. }
  322. }
  323. // otherwise, we do a GetSecret() first to avoid transferring the secret value if it is cached
  324. request := &smapi.ListSecretsRequest{
  325. ProjectID: &c.projectID,
  326. Page: scw.Int32Ptr(1),
  327. PageSize: scw.Uint32Ptr(1),
  328. }
  329. switch secretRef.RefType {
  330. case refTypeID:
  331. request := smapi.GetSecretVersionRequest{
  332. SecretID: secretRef.Value,
  333. Revision: versionSpec,
  334. }
  335. response, err := c.api.GetSecretVersion(&request, scw.WithContext(ctx))
  336. if err != nil {
  337. return nil, err
  338. }
  339. return c.accessSpecificSecretVersion(ctx, response.SecretID, response.Revision)
  340. case refTypeName:
  341. request.Name = &secretRef.Value
  342. case refTypePath:
  343. name, path, ok := splitNameAndPath(secretRef.Value)
  344. if !ok {
  345. return nil, errors.New("ref is not a path")
  346. }
  347. request.Name = &name
  348. request.Path = &path
  349. default:
  350. return nil, fmt.Errorf("invalid secret reference: %q", secretRef.Value)
  351. }
  352. response, err := c.api.ListSecrets(request, scw.WithContext(ctx))
  353. if err != nil {
  354. return nil, err
  355. }
  356. if len(response.Secrets) == 0 {
  357. return nil, errNoSecretForName
  358. }
  359. secretID := response.Secrets[0].ID
  360. secretVersion, err := c.api.GetSecretVersion(&smapi.GetSecretVersionRequest{
  361. SecretID: secretID,
  362. Revision: versionSpec,
  363. }, scw.WithContext(ctx))
  364. if err != nil {
  365. return nil, err
  366. }
  367. return c.accessSpecificSecretVersion(ctx, secretID, secretVersion.Revision)
  368. }
  369. func (c *client) accessSpecificSecretVersion(ctx context.Context, secretID string, revision uint32) ([]byte, error) {
  370. cachedValue, cacheHit := c.cache.Get(secretID, revision)
  371. if cacheHit {
  372. return cachedValue, nil
  373. }
  374. request := smapi.AccessSecretVersionRequest{
  375. SecretID: secretID,
  376. Revision: fmt.Sprintf("%d", revision),
  377. }
  378. response, err := c.api.AccessSecretVersion(&request, scw.WithContext(ctx))
  379. if err != nil {
  380. return nil, err
  381. }
  382. return response.Data, nil
  383. }
  384. func jsonToSecretData(value json.RawMessage) []byte {
  385. var stringValue string
  386. err := json.Unmarshal(value, &stringValue)
  387. if err == nil {
  388. return []byte(stringValue)
  389. }
  390. return []byte(strings.TrimSpace(string(value)))
  391. }
  392. func extractJSONProperty(secretData []byte, property string) ([]byte, error) {
  393. result := gjson.Get(string(secretData), property)
  394. if !result.Exists() {
  395. return nil, esv1.NoSecretError{}
  396. }
  397. return jsonToSecretData(json.RawMessage(result.Raw)), nil
  398. }
  399. func splitNameAndPath(ref string) (name, path string, ok bool) {
  400. if !strings.HasPrefix(ref, "/") {
  401. return
  402. }
  403. s := strings.Split(ref, "/")
  404. name = s[len(s)-1]
  405. if len(s) == 2 {
  406. path = "/"
  407. } else {
  408. path = strings.Join(s[:len(s)-1], "/")
  409. }
  410. ok = true
  411. return
  412. }