client.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  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. defaultFieldLabel = "password"
  38. errMsgUpdateItem = "failed to update item: %w"
  39. errMsgCreateItem = "failed to create item: %w"
  40. errMsgParsePushMeta = "failed to parse push secret metadata: %w"
  41. )
  42. // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
  43. var ErrKeyNotFound = errors.New("key not found")
  44. // nativeItemIDPattern matches a 1Password item ID per the Connect
  45. // server OpenAPI spec (^[\da-z]{26}$). Despite being called "UUIDs"
  46. // in 1Password's SDK and docs, they are not RFC 4122 UUIDs.
  47. // https://github.com/1Password/connect/blob/7485a59/docs/openapi/spec.yaml#L73-L75
  48. var nativeItemIDPattern = regexp.MustCompile(`^[\da-z]{26}$`)
  49. func isNativeItemID(s string) bool {
  50. return nativeItemIDPattern.MatchString(s)
  51. }
  52. // PushSecretMetadataSpec defines the metadata configuration for pushing secrets to 1Password.
  53. type PushSecretMetadataSpec struct {
  54. Tags []string `json:"tags,omitempty"`
  55. FieldType string `json:"fieldType,omitempty"`
  56. }
  57. // GetSecret returns a single secret from 1Password provider.
  58. // Follows syntax is used for the ref key: https://developer.1password.com/docs/cli/secret-reference-syntax/
  59. func (p *SecretsClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  60. if ref.Version != "" {
  61. return nil, errors.New(errVersionNotImplemented)
  62. }
  63. key := p.constructRefKey(ref.Key)
  64. if cached, ok := p.cacheGet(key); ok {
  65. return cached, nil
  66. }
  67. secret, err := p.client.Secrets().Resolve(ctx, key)
  68. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKResolve, err)
  69. if err != nil {
  70. return nil, err
  71. }
  72. result := []byte(secret)
  73. p.cacheAdd(key, result)
  74. return result, nil
  75. }
  76. // Close closes the client connection.
  77. func (p *SecretsClient) Close(_ context.Context) error {
  78. return nil
  79. }
  80. // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
  81. func (p *SecretsClient) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  82. providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
  83. if errors.Is(err, ErrKeyNotFound) {
  84. return nil
  85. }
  86. if err != nil {
  87. return err
  88. }
  89. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  90. var deleted bool
  91. providerItem.Fields, deleted, err = deleteField(providerItem.Fields, ref.GetProperty())
  92. if err != nil {
  93. return fmt.Errorf("failed to delete fields: %w", err)
  94. }
  95. if !deleted {
  96. // also invalidate the cache here, as this field might have been deleted
  97. // outside ESO.
  98. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  99. p.invalidateItemCache(ref.GetRemoteKey())
  100. return nil
  101. }
  102. // There is a chance that there is an empty item left in the section like this: [{ID: Title:}].
  103. if len(providerItem.Sections) == 1 && providerItem.Sections[0].ID == "" && providerItem.Sections[0].Title == "" {
  104. providerItem.Sections = nil
  105. }
  106. if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
  107. err = p.client.Items().Delete(ctx, providerItem.VaultID, providerItem.ID)
  108. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsDelete, err)
  109. if err != nil {
  110. return fmt.Errorf("failed to delete item: %w", err)
  111. }
  112. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  113. p.invalidateItemCache(ref.GetRemoteKey())
  114. return nil
  115. }
  116. _, err = p.client.Items().Put(ctx, providerItem)
  117. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  118. if err != nil {
  119. return fmt.Errorf(errMsgUpdateItem, err)
  120. }
  121. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  122. p.invalidateItemCache(ref.GetRemoteKey())
  123. return nil
  124. }
  125. func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.ItemField, bool, error) {
  126. // This will always iterate over all items,
  127. // but it's done to ensure that two fields with the same label
  128. // exist resulting in undefined behavior
  129. var (
  130. found bool
  131. fieldsF = make([]onepassword.ItemField, 0, len(fields))
  132. )
  133. for _, item := range fields {
  134. if item.Title == title {
  135. if found {
  136. return nil, false, fmt.Errorf("found multiple labels on item %q", title)
  137. }
  138. found = true
  139. continue
  140. }
  141. fieldsF = append(fieldsF, item)
  142. }
  143. return fieldsF, found, nil
  144. }
  145. // GetAllSecrets Not Implemented.
  146. func (p *SecretsClient) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  147. return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errNotImplemented))
  148. }
  149. // GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.
  150. func (p *SecretsClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  151. if ref.Version != "" {
  152. return nil, errors.New(errVersionNotImplemented)
  153. }
  154. cacheKey := p.constructRefKey(ref.Key) + "|" + ref.Property
  155. if cached, ok := p.cacheGet(cacheKey); ok {
  156. var result map[string][]byte
  157. if err := json.Unmarshal(cached, &result); err == nil {
  158. return result, nil
  159. }
  160. // continue with fresh instead
  161. }
  162. item, err := p.findItem(ctx, ref.Key)
  163. if err != nil {
  164. return nil, err
  165. }
  166. var result map[string][]byte
  167. propertyType, property := getObjType(item.Category, ref.Property)
  168. if propertyType == filePrefix {
  169. result, err = p.getFiles(ctx, item, property)
  170. } else {
  171. result, err = p.getFields(item, property)
  172. }
  173. if err != nil {
  174. return nil, err
  175. }
  176. if serialized, err := json.Marshal(result); err == nil {
  177. p.cacheAdd(cacheKey, serialized)
  178. }
  179. return result, nil
  180. }
  181. func (p *SecretsClient) getFields(item onepassword.Item, property string) (map[string][]byte, error) {
  182. secretData := make(map[string][]byte)
  183. for _, field := range item.Fields {
  184. if property != "" && field.Title != property {
  185. continue
  186. }
  187. if length := countFieldsWithLabel(field.Title, item.Fields); length != 1 {
  188. return nil, fmt.Errorf(errExpectedOneFieldMsgF, field.Title, item.Title, length)
  189. }
  190. // caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
  191. secretData[field.Title] = []byte(field.Value)
  192. }
  193. return secretData, nil
  194. }
  195. func (p *SecretsClient) getFiles(ctx context.Context, item onepassword.Item, property string) (map[string][]byte, error) {
  196. secretData := make(map[string][]byte)
  197. for _, file := range item.Files {
  198. if property != "" && file.Attributes.Name != property {
  199. continue
  200. }
  201. cacheKey := fileCachePrefix + p.vaultID + ":" + item.ID + ":" + file.FieldID + ":" + file.Attributes.Name
  202. if cached, ok := p.cacheGet(cacheKey); ok {
  203. secretData[file.Attributes.Name] = cached
  204. continue
  205. }
  206. contents, err := p.client.Items().Files().Read(ctx, p.vaultID, file.FieldID, file.Attributes)
  207. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKFilesRead, err)
  208. if err != nil {
  209. return nil, fmt.Errorf("failed to read file: %w", err)
  210. }
  211. p.cacheAdd(cacheKey, contents)
  212. secretData[file.Attributes.Name] = contents
  213. }
  214. return secretData, nil
  215. }
  216. func countFieldsWithLabel(fieldLabel string, fields []onepassword.ItemField) int {
  217. count := 0
  218. for _, field := range fields {
  219. if field.Title == fieldLabel {
  220. count++
  221. }
  222. }
  223. return count
  224. }
  225. // Clean property string by removing property prefix if needed.
  226. func getObjType(documentType onepassword.ItemCategory, property string) (string, string) {
  227. if strings.HasPrefix(property, fieldPrefix+prefixSplitter) {
  228. return fieldPrefix, property[6:]
  229. }
  230. if strings.HasPrefix(property, filePrefix+prefixSplitter) {
  231. return filePrefix, property[5:]
  232. }
  233. if documentType == onepassword.ItemCategoryDocument {
  234. return filePrefix, property
  235. }
  236. return fieldPrefix, property
  237. }
  238. // createItem creates a new item in the first vault. If no vaults exist, it returns an error.
  239. func (p *SecretsClient) createItem(ctx context.Context, val []byte, ref esv1.PushSecretData) error {
  240. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  241. if err != nil {
  242. return fmt.Errorf(errMsgParsePushMeta, err)
  243. }
  244. label := ref.GetProperty()
  245. if label == "" {
  246. label = defaultFieldLabel
  247. }
  248. var tags []string
  249. if mdata != nil && mdata.Spec.Tags != nil {
  250. tags = mdata.Spec.Tags
  251. }
  252. fieldType := onepassword.ItemFieldTypeConcealed
  253. if mdata != nil {
  254. fieldType = resolveFieldType(mdata.Spec.FieldType)
  255. }
  256. _, err = p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  257. Category: onepassword.ItemCategoryServer,
  258. VaultID: p.vaultID,
  259. Title: ref.GetRemoteKey(),
  260. Fields: []onepassword.ItemField{
  261. generateNewItemField(label, string(val), fieldType),
  262. },
  263. Tags: tags,
  264. })
  265. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsCreate, err)
  266. if err != nil {
  267. return fmt.Errorf(errMsgCreateItem, err)
  268. }
  269. p.invalidateCacheByPrefix(p.constructRefKey(ref.GetRemoteKey()))
  270. p.invalidateItemCache(ref.GetRemoteKey())
  271. return nil
  272. }
  273. // updateFieldValue updates the fields value of an item with the given label.
  274. // If the label does not exist, a new field is created with the given fieldType. If the label exists but
  275. // the value is different, the value is updated. If the label exists and the
  276. // value is the same, nothing is done.
  277. func updateFieldValue(fields []onepassword.ItemField, title, newVal string, fieldType onepassword.ItemFieldType) ([]onepassword.ItemField, error) {
  278. // This will always iterate over all items.
  279. // This is done to ensure that two fields with the same label
  280. // exist resulting in undefined behavior.
  281. var (
  282. found bool
  283. index int
  284. )
  285. for i, item := range fields {
  286. if item.Title == title {
  287. if found {
  288. return nil, fmt.Errorf("found multiple labels with the same key")
  289. }
  290. found = true
  291. index = i
  292. }
  293. }
  294. if !found {
  295. return append(fields, generateNewItemField(title, newVal, fieldType)), nil
  296. }
  297. if fields[index].Value != newVal {
  298. fields[index].Value = newVal
  299. }
  300. if fields[index].FieldType != fieldType {
  301. fields[index].FieldType = fieldType
  302. }
  303. return fields, nil
  304. }
  305. // resolveFieldType maps a 1Password field type name to the SDK constant.
  306. // Case-insensitive. Accepted: text|string, concealed|password, url, email, phone, date, monthYear.
  307. // Defaults to Concealed for empty/unrecognized. OTP and file excluded.
  308. // Reference: https://developer.1password.com/docs/cli/item-fields/#custom-fields
  309. func resolveFieldType(raw string) onepassword.ItemFieldType {
  310. switch strings.ToLower(raw) {
  311. case "text", "string":
  312. return onepassword.ItemFieldTypeText
  313. case "concealed", "password":
  314. return onepassword.ItemFieldTypeConcealed
  315. case "email":
  316. return onepassword.ItemFieldTypeEmail
  317. case "url":
  318. return onepassword.ItemFieldTypeURL
  319. case "phone":
  320. return onepassword.ItemFieldTypePhone
  321. case "date":
  322. return onepassword.ItemFieldTypeDate
  323. case "monthyear":
  324. return onepassword.ItemFieldTypeMonthYear
  325. }
  326. return onepassword.ItemFieldTypeConcealed
  327. }
  328. // normalizeItemFields clears empty section IDs because the 1Password SDK rejects items with a SectionID pointer to "" when the section is missing.
  329. func normalizeItemFields(fields []onepassword.ItemField) []onepassword.ItemField {
  330. for i := range fields {
  331. if fields[i].SectionID != nil && *fields[i].SectionID == "" {
  332. fields[i].SectionID = nil
  333. }
  334. }
  335. return fields
  336. }
  337. // generateNewItemField creates an ItemField with ID and Title set to the given title (unique within item), value, and field type.
  338. func generateNewItemField(title, newVal string, fieldType onepassword.ItemFieldType) onepassword.ItemField {
  339. return onepassword.ItemField{
  340. ID: title,
  341. Title: title,
  342. Value: newVal,
  343. FieldType: fieldType,
  344. }
  345. }
  346. // PushSecret creates or updates a secret in 1Password.
  347. func (p *SecretsClient) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  348. if ref.GetSecretKey() == "" {
  349. return p.pushAllKeys(ctx, secret, ref)
  350. }
  351. val, ok := secret.Data[ref.GetSecretKey()]
  352. if !ok {
  353. return fmt.Errorf("secret %s/%s does not contain a key", secret.Namespace, secret.Name)
  354. }
  355. title := ref.GetRemoteKey()
  356. providerItem, err := p.findItem(ctx, title)
  357. if errors.Is(err, ErrKeyNotFound) {
  358. return p.createItem(ctx, val, ref)
  359. } else if err != nil {
  360. return fmt.Errorf("failed to find item: %w", err)
  361. }
  362. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  363. label := ref.GetProperty()
  364. if label == "" {
  365. label = defaultFieldLabel
  366. }
  367. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  368. if err != nil {
  369. return fmt.Errorf(errMsgParsePushMeta, err)
  370. }
  371. if mdata != nil && mdata.Spec.Tags != nil {
  372. providerItem.Tags = mdata.Spec.Tags
  373. }
  374. fieldType := onepassword.ItemFieldTypeConcealed
  375. if mdata != nil {
  376. fieldType = resolveFieldType(mdata.Spec.FieldType)
  377. }
  378. providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val), fieldType)
  379. if err != nil {
  380. return fmt.Errorf("failed to update field with label: %s: %w", label, err)
  381. }
  382. _, err = p.client.Items().Put(ctx, providerItem)
  383. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  384. if err != nil {
  385. return fmt.Errorf(errMsgUpdateItem, err)
  386. }
  387. p.invalidateCacheByPrefix(p.constructRefKey(title))
  388. p.invalidateItemCache(title)
  389. return nil
  390. }
  391. // createAllKeysItem creates a new item with all keys from secret.Data.
  392. func (p *SecretsClient) createAllKeysItem(ctx context.Context, secret *corev1.Secret, title string, tags []string, fieldType onepassword.ItemFieldType) error {
  393. fields := make([]onepassword.ItemField, 0, len(secret.Data))
  394. for k, v := range secret.Data {
  395. fields = append(fields, generateNewItemField(k, string(v), fieldType))
  396. }
  397. _, err := p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  398. Category: onepassword.ItemCategoryServer,
  399. VaultID: p.vaultID,
  400. Title: title,
  401. Fields: fields,
  402. Tags: tags,
  403. })
  404. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsCreate, err)
  405. if err != nil {
  406. return fmt.Errorf(errMsgCreateItem, err)
  407. }
  408. p.invalidateCacheByPrefix(p.constructRefKey(title))
  409. p.invalidateItemCache(title)
  410. return nil
  411. }
  412. // pushAllKeys pushes all keys from secret.Data as separate fields on a single 1Password item.
  413. func (p *SecretsClient) pushAllKeys(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  414. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  415. if err != nil {
  416. return fmt.Errorf(errMsgParsePushMeta, err)
  417. }
  418. var tags []string
  419. if mdata != nil && mdata.Spec.Tags != nil {
  420. tags = mdata.Spec.Tags
  421. }
  422. fieldType := onepassword.ItemFieldTypeConcealed
  423. if mdata != nil {
  424. fieldType = resolveFieldType(mdata.Spec.FieldType)
  425. }
  426. title := ref.GetRemoteKey()
  427. providerItem, err := p.findItem(ctx, title)
  428. if errors.Is(err, ErrKeyNotFound) {
  429. return p.createAllKeysItem(ctx, secret, title, tags, fieldType)
  430. }
  431. if err != nil {
  432. return fmt.Errorf("failed to find item: %w", err)
  433. }
  434. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  435. if tags != nil {
  436. providerItem.Tags = tags
  437. }
  438. kept := make([]onepassword.ItemField, 0, len(providerItem.Fields))
  439. for _, f := range providerItem.Fields {
  440. if v, ok := secret.Data[f.Title]; ok {
  441. f.Value = string(v)
  442. f.FieldType = fieldType
  443. kept = append(kept, f)
  444. }
  445. }
  446. for k, v := range secret.Data {
  447. if countFieldsWithLabel(k, kept) == 0 {
  448. kept = append(kept, generateNewItemField(k, string(v), fieldType))
  449. }
  450. }
  451. providerItem.Fields = kept
  452. _, err = p.client.Items().Put(ctx, providerItem)
  453. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  454. if err != nil {
  455. return fmt.Errorf(errMsgUpdateItem, err)
  456. }
  457. p.invalidateCacheByPrefix(p.constructRefKey(title))
  458. p.invalidateItemCache(title)
  459. return nil
  460. }
  461. // GetVault retrieves a vault by its title or UUID from 1Password.
  462. func (p *SecretsClient) GetVault(ctx context.Context, titleOrUUID string) (string, error) {
  463. vaults, err := p.client.VaultsAPI.List(ctx)
  464. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKVaultsList, err)
  465. if err != nil {
  466. return "", fmt.Errorf("failed to list vaults: %w", err)
  467. }
  468. for _, v := range vaults {
  469. if v.Title == titleOrUUID || v.ID == titleOrUUID {
  470. return v.ID, nil
  471. }
  472. }
  473. return "", fmt.Errorf("vault %s not found", titleOrUUID)
  474. }
  475. func (p *SecretsClient) findItem(ctx context.Context, name string) (onepassword.Item, error) {
  476. cacheKey := itemCachePrefix + p.vaultID + ":" + name
  477. if cached, ok := p.cacheGet(cacheKey); ok {
  478. var item onepassword.Item
  479. if err := json.Unmarshal(cached, &item); err == nil {
  480. return item, nil
  481. }
  482. }
  483. var item onepassword.Item
  484. var err error
  485. if isNativeItemID(name) {
  486. item, err = p.client.Items().Get(ctx, p.vaultID, name)
  487. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
  488. if err != nil {
  489. if isNotFoundError(err) {
  490. return onepassword.Item{}, ErrKeyNotFound
  491. }
  492. return onepassword.Item{}, err
  493. }
  494. } else {
  495. items, err := p.client.Items().List(ctx, p.vaultID)
  496. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsList, err)
  497. if err != nil {
  498. return onepassword.Item{}, fmt.Errorf("failed to list items: %w", err)
  499. }
  500. var itemUUID string
  501. for _, v := range items {
  502. if v.Title == name {
  503. if itemUUID != "" {
  504. return onepassword.Item{}, fmt.Errorf("found multiple items with name %s", name)
  505. }
  506. itemUUID = v.ID
  507. }
  508. }
  509. if itemUUID == "" {
  510. return onepassword.Item{}, ErrKeyNotFound
  511. }
  512. item, err = p.client.Items().Get(ctx, p.vaultID, itemUUID)
  513. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
  514. if err != nil {
  515. return onepassword.Item{}, err
  516. }
  517. }
  518. if serialized, err := json.Marshal(item); err == nil {
  519. p.cacheAdd(cacheKey, serialized)
  520. }
  521. return item, nil
  522. }
  523. // SecretExists returns true if the item exists, and if a property is specified, if a field with that title exists.
  524. func (p *SecretsClient) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  525. item, err := p.findItem(ctx, ref.GetRemoteKey())
  526. if errors.Is(err, ErrKeyNotFound) {
  527. return false, nil
  528. }
  529. if err != nil {
  530. return false, err
  531. }
  532. property := ref.GetProperty()
  533. if property == "" {
  534. return true, nil // item exists; pushAllKeys handles field-level reconciliation
  535. }
  536. for _, f := range item.Fields {
  537. if f.Title == property {
  538. return true, nil
  539. }
  540. }
  541. return false, nil
  542. }
  543. // Validate does nothing here. It would be possible to ping the SDK to prove we're healthy, but
  544. // since the 1password SDK rate-limit is pretty aggressive, we prefer to do nothing.
  545. func (p *SecretsClient) Validate() (esv1.ValidationResult, error) {
  546. return esv1.ValidationResultReady, nil
  547. }
  548. func (p *SecretsClient) constructRefKey(key string) string {
  549. // remove any possible leading slashes because the vaultPrefix already contains it.
  550. return p.vaultPrefix + strings.TrimPrefix(key, "/")
  551. }
  552. // cacheGet retrieves a value from the cache. Returns false if cache is disabled or key not found.
  553. func (p *SecretsClient) cacheGet(key string) ([]byte, bool) {
  554. if p.cache == nil {
  555. return nil, false
  556. }
  557. v, ok := p.cache.Get(key)
  558. if !ok {
  559. return nil, false
  560. }
  561. return bytes.Clone(v), true
  562. }
  563. // cacheAdd stores a value in the cache. No-op if cache is disabled.
  564. func (p *SecretsClient) cacheAdd(key string, value []byte) {
  565. if p.cache == nil {
  566. return
  567. }
  568. p.cache.Add(key, value)
  569. }
  570. // invalidateCacheByPrefix removes all cache entries that start with the given prefix.
  571. // This is used to invalidate cache entries when an item is modified or deleted.
  572. // No-op if cache is disabled.
  573. // Why are we using a Prefix? Because items and properties are stored via prefixes using 1Password SDK.
  574. // This means when an item is deleted we delete the fields and properties that belong to the item as well.
  575. func (p *SecretsClient) invalidateCacheByPrefix(prefix string) {
  576. if p.cache == nil {
  577. return
  578. }
  579. keys := p.cache.Keys()
  580. for _, key := range keys {
  581. if strings.HasPrefix(key, prefix) {
  582. if len(key) == len(prefix) || key[len(prefix)] == '/' || key[len(prefix)] == '|' {
  583. p.cache.Remove(key)
  584. }
  585. }
  586. }
  587. }
  588. // invalidateItemCache removes cached item entries for the given item name.
  589. // No-op if cache is disabled.
  590. func (p *SecretsClient) invalidateItemCache(name string) {
  591. if p.cache == nil {
  592. return
  593. }
  594. cacheKey := itemCachePrefix + p.vaultID + ":" + name
  595. p.cache.Remove(cacheKey)
  596. }
  597. func isNotFoundError(err error) bool {
  598. msg := strings.ToLower(err.Error())
  599. return strings.Contains(msg, "couldn't be found") || strings.Contains(msg, "resource not found")
  600. }