client.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  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 onepasswordsdk implements a provider for 1Password secrets management service.
  14. package onepasswordsdk
  15. import (
  16. "bytes"
  17. "context"
  18. "encoding/json"
  19. "errors"
  20. "fmt"
  21. "regexp"
  22. "strings"
  23. "github.com/1password/onepassword-sdk-go"
  24. corev1 "k8s.io/api/core/v1"
  25. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  26. "github.com/external-secrets/external-secrets/runtime/constants"
  27. "github.com/external-secrets/external-secrets/runtime/esutils/metadata"
  28. "github.com/external-secrets/external-secrets/runtime/metrics"
  29. )
  30. const (
  31. fieldPrefix = "field"
  32. filePrefix = "file"
  33. prefixSplitter = "/"
  34. errExpectedOneFieldMsgF = "found more than 1 fields with title '%s' in '%s', got %d"
  35. itemCachePrefix = "item:"
  36. fileCachePrefix = "file:"
  37. )
  38. // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
  39. var ErrKeyNotFound = errors.New("key not found")
  40. // nativeItemIDPattern matches a 1Password item ID per the Connect
  41. // server OpenAPI spec (^[\da-z]{26}$). Despite being called "UUIDs"
  42. // in 1Password's SDK and docs, they are not RFC 4122 UUIDs.
  43. // https://github.com/1Password/connect/blob/7485a59/docs/openapi/spec.yaml#L73-L75
  44. var nativeItemIDPattern = regexp.MustCompile(`^[\da-z]{26}$`)
  45. func isNativeItemID(s string) bool {
  46. return nativeItemIDPattern.MatchString(s)
  47. }
  48. // PushSecretMetadataSpec defines the metadata configuration for pushing secrets to 1Password.
  49. type PushSecretMetadataSpec struct {
  50. Tags []string `json:"tags,omitempty"`
  51. }
  52. // GetSecret returns a single secret from 1Password provider.
  53. // Follows syntax is used for the ref key: https://developer.1password.com/docs/cli/secret-reference-syntax/
  54. func (p *SecretsClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  55. if ref.Version != "" {
  56. return nil, errors.New(errVersionNotImplemented)
  57. }
  58. key := p.constructRefKey(ref.Key)
  59. if cached, ok := p.cacheGet(key); ok {
  60. return cached, nil
  61. }
  62. secret, err := p.client.Secrets().Resolve(ctx, key)
  63. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKResolve, err)
  64. if err != nil {
  65. return nil, err
  66. }
  67. result := []byte(secret)
  68. p.cacheAdd(key, result)
  69. return result, nil
  70. }
  71. // Close closes the client connection.
  72. func (p *SecretsClient) Close(_ context.Context) error {
  73. return nil
  74. }
  75. // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
  76. func (p *SecretsClient) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  77. providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
  78. if errors.Is(err, ErrKeyNotFound) {
  79. return nil
  80. }
  81. if err != nil {
  82. return err
  83. }
  84. var deleted bool
  85. providerItem.Fields, deleted, err = deleteField(providerItem.Fields, ref.GetProperty())
  86. if err != nil {
  87. return fmt.Errorf("failed to delete fields: %w", err)
  88. }
  89. if !deleted {
  90. // also invalidate the cache here, as this field might have been deleted
  91. // outside ESO.
  92. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  93. p.invalidateItemCache(ref.GetRemoteKey())
  94. return nil
  95. }
  96. // There is a chance that there is an empty item left in the section like this: [{ID: Title:}].
  97. if len(providerItem.Sections) == 1 && providerItem.Sections[0].ID == "" && providerItem.Sections[0].Title == "" {
  98. providerItem.Sections = nil
  99. }
  100. if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
  101. err = p.client.Items().Delete(ctx, providerItem.VaultID, providerItem.ID)
  102. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsDelete, err)
  103. if err != nil {
  104. return fmt.Errorf("failed to delete item: %w", err)
  105. }
  106. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  107. p.invalidateItemCache(ref.GetRemoteKey())
  108. return nil
  109. }
  110. _, err = p.client.Items().Put(ctx, providerItem)
  111. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  112. if err != nil {
  113. return fmt.Errorf("failed to update item: %w", err)
  114. }
  115. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  116. p.invalidateItemCache(ref.GetRemoteKey())
  117. return nil
  118. }
  119. func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.ItemField, bool, error) {
  120. // This will always iterate over all items,
  121. // but it's done to ensure that two fields with the same label
  122. // exist resulting in undefined behavior
  123. var (
  124. found bool
  125. fieldsF = make([]onepassword.ItemField, 0, len(fields))
  126. )
  127. for _, item := range fields {
  128. if item.Title == title {
  129. if found {
  130. return nil, false, fmt.Errorf("found multiple labels on item %q", title)
  131. }
  132. found = true
  133. continue
  134. }
  135. fieldsF = append(fieldsF, item)
  136. }
  137. return fieldsF, found, nil
  138. }
  139. // GetAllSecrets Not Implemented.
  140. func (p *SecretsClient) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  141. return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errNotImplemented))
  142. }
  143. // GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.
  144. func (p *SecretsClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  145. if ref.Version != "" {
  146. return nil, errors.New(errVersionNotImplemented)
  147. }
  148. cacheKey := p.constructRefKey(ref.Key) + "|" + ref.Property
  149. if cached, ok := p.cacheGet(cacheKey); ok {
  150. var result map[string][]byte
  151. if err := json.Unmarshal(cached, &result); err == nil {
  152. return result, nil
  153. }
  154. // continue with fresh instead
  155. }
  156. item, err := p.findItem(ctx, ref.Key)
  157. if err != nil {
  158. return nil, err
  159. }
  160. var result map[string][]byte
  161. propertyType, property := getObjType(item.Category, ref.Property)
  162. if propertyType == filePrefix {
  163. result, err = p.getFiles(ctx, item, property)
  164. } else {
  165. result, err = p.getFields(item, property)
  166. }
  167. if err != nil {
  168. return nil, err
  169. }
  170. if serialized, err := json.Marshal(result); err == nil {
  171. p.cacheAdd(cacheKey, serialized)
  172. }
  173. return result, nil
  174. }
  175. func (p *SecretsClient) getFields(item onepassword.Item, property string) (map[string][]byte, error) {
  176. secretData := make(map[string][]byte)
  177. for _, field := range item.Fields {
  178. if property != "" && field.Title != property {
  179. continue
  180. }
  181. if length := countFieldsWithLabel(field.Title, item.Fields); length != 1 {
  182. return nil, fmt.Errorf(errExpectedOneFieldMsgF, field.Title, item.Title, length)
  183. }
  184. // caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
  185. secretData[field.Title] = []byte(field.Value)
  186. }
  187. return secretData, nil
  188. }
  189. func (p *SecretsClient) getFiles(ctx context.Context, item onepassword.Item, property string) (map[string][]byte, error) {
  190. secretData := make(map[string][]byte)
  191. for _, file := range item.Files {
  192. if property != "" && file.Attributes.Name != property {
  193. continue
  194. }
  195. cacheKey := fileCachePrefix + p.vaultID + ":" + item.ID + ":" + file.FieldID + ":" + file.Attributes.Name
  196. if cached, ok := p.cacheGet(cacheKey); ok {
  197. secretData[file.Attributes.Name] = cached
  198. continue
  199. }
  200. contents, err := p.client.Items().Files().Read(ctx, p.vaultID, file.FieldID, file.Attributes)
  201. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKFilesRead, err)
  202. if err != nil {
  203. return nil, fmt.Errorf("failed to read file: %w", err)
  204. }
  205. p.cacheAdd(cacheKey, contents)
  206. secretData[file.Attributes.Name] = contents
  207. }
  208. return secretData, nil
  209. }
  210. func countFieldsWithLabel(fieldLabel string, fields []onepassword.ItemField) int {
  211. count := 0
  212. for _, field := range fields {
  213. if field.Title == fieldLabel {
  214. count++
  215. }
  216. }
  217. return count
  218. }
  219. // Clean property string by removing property prefix if needed.
  220. func getObjType(documentType onepassword.ItemCategory, property string) (string, string) {
  221. if strings.HasPrefix(property, fieldPrefix+prefixSplitter) {
  222. return fieldPrefix, property[6:]
  223. }
  224. if strings.HasPrefix(property, filePrefix+prefixSplitter) {
  225. return filePrefix, property[5:]
  226. }
  227. if documentType == onepassword.ItemCategoryDocument {
  228. return filePrefix, property
  229. }
  230. return fieldPrefix, property
  231. }
  232. // createItem creates a new item in the first vault. If no vaults exist, it returns an error.
  233. func (p *SecretsClient) createItem(ctx context.Context, val []byte, ref esv1.PushSecretData) error {
  234. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  235. if err != nil {
  236. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  237. }
  238. label := ref.GetProperty()
  239. if label == "" {
  240. label = "password"
  241. }
  242. var tags []string
  243. if mdata != nil && mdata.Spec.Tags != nil {
  244. tags = mdata.Spec.Tags
  245. }
  246. _, err = p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  247. Category: onepassword.ItemCategoryServer,
  248. VaultID: p.vaultID,
  249. Title: ref.GetRemoteKey(),
  250. Fields: []onepassword.ItemField{
  251. generateNewItemField(label, string(val)),
  252. },
  253. Tags: tags,
  254. })
  255. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsCreate, err)
  256. if err != nil {
  257. return fmt.Errorf("failed to create item: %w", err)
  258. }
  259. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  260. p.invalidateItemCache(ref.GetRemoteKey())
  261. return nil
  262. }
  263. // updateFieldValue updates the fields value of an item with the given label.
  264. // If the label does not exist, a new field is created. If the label exists but
  265. // the value is different, the value is updated. If the label exists and the
  266. // value is the same, nothing is done.
  267. func updateFieldValue(fields []onepassword.ItemField, title, newVal string) ([]onepassword.ItemField, error) {
  268. // This will always iterate over all items.
  269. // This is done to ensure that two fields with the same label
  270. // exist resulting in undefined behavior.
  271. var (
  272. found bool
  273. index int
  274. )
  275. for i, item := range fields {
  276. if item.Title == title {
  277. if found {
  278. return nil, fmt.Errorf("found multiple labels with the same key")
  279. }
  280. found = true
  281. index = i
  282. }
  283. }
  284. if !found {
  285. return append(fields, generateNewItemField(title, newVal)), nil
  286. }
  287. if fields[index].Value != newVal {
  288. fields[index].Value = newVal
  289. }
  290. return fields, nil
  291. }
  292. // generateNewItemField generates a new item field with the given label and value.
  293. func generateNewItemField(title, newVal string) onepassword.ItemField {
  294. field := onepassword.ItemField{
  295. Title: title,
  296. Value: newVal,
  297. FieldType: onepassword.ItemFieldTypeConcealed,
  298. }
  299. return field
  300. }
  301. // PushSecret creates or updates a secret in 1Password.
  302. func (p *SecretsClient) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  303. val, ok := secret.Data[ref.GetSecretKey()]
  304. if !ok {
  305. return fmt.Errorf("secret %s/%s does not contain a key", secret.Namespace, secret.Name)
  306. }
  307. title := ref.GetRemoteKey()
  308. providerItem, err := p.findItem(ctx, title)
  309. if errors.Is(err, ErrKeyNotFound) {
  310. if err = p.createItem(ctx, val, ref); err != nil {
  311. return fmt.Errorf("failed to create item: %w", err)
  312. }
  313. return nil
  314. } else if err != nil {
  315. return fmt.Errorf("failed to find item: %w", err)
  316. }
  317. // TODO: We are only sending info to a specific label on a 1password item.
  318. // We should change this logic eventually to allow pushing whole kubernetes Secrets to 1password as multiple labels
  319. // OOTB.
  320. label := ref.GetProperty()
  321. if label == "" {
  322. label = "password"
  323. }
  324. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  325. if err != nil {
  326. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  327. }
  328. if mdata != nil && mdata.Spec.Tags != nil {
  329. providerItem.Tags = mdata.Spec.Tags
  330. }
  331. providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val))
  332. if err != nil {
  333. return fmt.Errorf("failed to update field with label: %s: %w", label, err)
  334. }
  335. _, err = p.client.Items().Put(ctx, providerItem)
  336. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  337. if err != nil {
  338. return fmt.Errorf("failed to update item: %w", err)
  339. }
  340. p.invalidateCacheByPrefix(p.constructRefKey(title))
  341. p.invalidateItemCache(title)
  342. return nil
  343. }
  344. // GetVault retrieves a vault by its title or UUID from 1Password.
  345. func (p *SecretsClient) GetVault(ctx context.Context, titleOrUUID string) (string, error) {
  346. vaults, err := p.client.VaultsAPI.List(ctx)
  347. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKVaultsList, err)
  348. if err != nil {
  349. return "", fmt.Errorf("failed to list vaults: %w", err)
  350. }
  351. for _, v := range vaults {
  352. if v.Title == titleOrUUID || v.ID == titleOrUUID {
  353. return v.ID, nil
  354. }
  355. }
  356. return "", fmt.Errorf("vault %s not found", titleOrUUID)
  357. }
  358. func (p *SecretsClient) findItem(ctx context.Context, name string) (onepassword.Item, error) {
  359. cacheKey := itemCachePrefix + p.vaultID + ":" + name
  360. if cached, ok := p.cacheGet(cacheKey); ok {
  361. var item onepassword.Item
  362. if err := json.Unmarshal(cached, &item); err == nil {
  363. return item, nil
  364. }
  365. }
  366. var item onepassword.Item
  367. var err error
  368. if isNativeItemID(name) {
  369. item, err = p.client.Items().Get(ctx, p.vaultID, name)
  370. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
  371. if err != nil {
  372. if isNotFoundError(err) {
  373. return onepassword.Item{}, ErrKeyNotFound
  374. }
  375. return onepassword.Item{}, err
  376. }
  377. } else {
  378. items, err := p.client.Items().List(ctx, p.vaultID)
  379. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsList, err)
  380. if err != nil {
  381. return onepassword.Item{}, fmt.Errorf("failed to list items: %w", err)
  382. }
  383. var itemUUID string
  384. for _, v := range items {
  385. if v.Title == name {
  386. if itemUUID != "" {
  387. return onepassword.Item{}, fmt.Errorf("found multiple items with name %s", name)
  388. }
  389. itemUUID = v.ID
  390. }
  391. }
  392. if itemUUID == "" {
  393. return onepassword.Item{}, ErrKeyNotFound
  394. }
  395. item, err = p.client.Items().Get(ctx, p.vaultID, itemUUID)
  396. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
  397. if err != nil {
  398. return onepassword.Item{}, err
  399. }
  400. }
  401. if serialized, err := json.Marshal(item); err == nil {
  402. p.cacheAdd(cacheKey, serialized)
  403. }
  404. return item, nil
  405. }
  406. // SecretExists checks if a secret exists in 1Password.
  407. func (p *SecretsClient) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  408. return false, fmt.Errorf("not implemented")
  409. }
  410. // Validate does nothing here. It would be possible to ping the SDK to prove we're healthy, but
  411. // since the 1password SDK rate-limit is pretty aggressive, we prefer to do nothing.
  412. func (p *SecretsClient) Validate() (esv1.ValidationResult, error) {
  413. return esv1.ValidationResultReady, nil
  414. }
  415. func (p *SecretsClient) constructRefKey(key string) string {
  416. // remove any possible leading slashes because the vaultPrefix already contains it.
  417. return p.vaultPrefix + strings.TrimPrefix(key, "/")
  418. }
  419. // cacheGet retrieves a value from the cache. Returns false if cache is disabled or key not found.
  420. func (p *SecretsClient) cacheGet(key string) ([]byte, bool) {
  421. if p.cache == nil {
  422. return nil, false
  423. }
  424. v, ok := p.cache.Get(key)
  425. if !ok {
  426. return nil, false
  427. }
  428. return bytes.Clone(v), true
  429. }
  430. // cacheAdd stores a value in the cache. No-op if cache is disabled.
  431. func (p *SecretsClient) cacheAdd(key string, value []byte) {
  432. if p.cache == nil {
  433. return
  434. }
  435. p.cache.Add(key, value)
  436. }
  437. // invalidateCacheByPrefix removes all cache entries that start with the given prefix.
  438. // This is used to invalidate cache entries when an item is modified or deleted.
  439. // No-op if cache is disabled.
  440. // Why are we using a Prefix? Because items and properties are stored via prefixes using 1Password SDK.
  441. // This means when an item is deleted we delete the fields and properties that belong to the item as well.
  442. func (p *SecretsClient) invalidateCacheByPrefix(prefix string) {
  443. if p.cache == nil {
  444. return
  445. }
  446. keys := p.cache.Keys()
  447. for _, key := range keys {
  448. if strings.HasPrefix(key, prefix) {
  449. if len(key) == len(prefix) || key[len(prefix)] == '/' || key[len(prefix)] == '|' {
  450. p.cache.Remove(key)
  451. }
  452. }
  453. }
  454. }
  455. // invalidateItemCache removes cached item entries for the given item name.
  456. // No-op if cache is disabled.
  457. func (p *SecretsClient) invalidateItemCache(name string) {
  458. if p.cache == nil {
  459. return
  460. }
  461. cacheKey := itemCachePrefix + p.vaultID + ":" + name
  462. p.cache.Remove(cacheKey)
  463. }
  464. func isNotFoundError(err error) bool {
  465. msg := strings.ToLower(err.Error())
  466. return strings.Contains(msg, "couldn't be found") || strings.Contains(msg, "resource not found")
  467. }