client.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969
  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/find"
  29. "github.com/external-secrets/external-secrets/runtime/metrics"
  30. )
  31. const (
  32. fieldPrefix = "field"
  33. filePrefix = "file"
  34. prefixSplitter = "/"
  35. vaultCachePrefix = "vault:"
  36. itemCachePrefix = "item:"
  37. fileCachePrefix = "file:"
  38. defaultFieldLabel = "password"
  39. errMsgUpdateItem = "failed to update item: %w"
  40. errMsgCreateItem = "failed to create item: %w"
  41. errMsgParsePushMeta = "failed to parse push secret metadata: %w"
  42. errMsgExpectedOneField = "found more than 1 fields with title '%s' in '%s', got %d"
  43. errMsgExpectedOneFile = "found more than 1 files with title '%s' in '%s', got %d"
  44. errMsgFieldNotFound = "field with label '%s' not found in item '%s'"
  45. errMsgFileNotFound = "file with title '%s' not found in item '%s'"
  46. )
  47. // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
  48. var ErrKeyNotFound = errors.New("key not found")
  49. // nativeIDPattern matches a 1Password unique identifier per the SDK
  50. // docs (^[\da-z]{26}$). Despite being called "UUIDs" in 1Password's SDK and docs,
  51. // they are not RFC 4122 UUIDs.
  52. // https://www.1password.dev/cli/reference#unique-identifiers-ids
  53. var nativeIDPattern = regexp.MustCompile(`^[\da-z]{26}$`)
  54. func isNativeID(s string) bool {
  55. return nativeIDPattern.MatchString(s)
  56. }
  57. // PushSecretMetadataSpec defines the metadata configuration for pushing secrets to 1Password.
  58. type PushSecretMetadataSpec struct {
  59. Tags []string `json:"tags,omitempty"`
  60. FieldType string `json:"fieldType,omitempty"`
  61. }
  62. // GetSecret returns a single secret from 1Password provider.
  63. // Follows syntax is used for the ref key: https://developer.1password.com/docs/cli/secret-reference-syntax/
  64. func (p *SecretsClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  65. if ref.Version != "" {
  66. return nil, errors.New(errVersionNotImplemented)
  67. }
  68. key := p.constructRefKey(ref.Key)
  69. if cached, ok := p.cacheGet(key); ok {
  70. return cached, nil
  71. }
  72. // An item cached by GetAllSecrets/GetSecretMap is keyed by item name, not by the
  73. // Resolve reference. Serve plain field lookups from it to avoid a Resolve API call.
  74. if value, ok := p.resolveFieldFromCachedItem(ref.Key); ok {
  75. p.cacheAdd(key, value)
  76. return value, nil
  77. }
  78. secret, err := p.client.Secrets().Resolve(ctx, key)
  79. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKResolve, err)
  80. if err != nil {
  81. return nil, err
  82. }
  83. result := []byte(secret)
  84. p.cacheAdd(key, result)
  85. return result, nil
  86. }
  87. // Close closes the client connection.
  88. func (p *SecretsClient) Close(_ context.Context) error {
  89. return nil
  90. }
  91. // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
  92. func (p *SecretsClient) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) (err error) {
  93. providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
  94. if errors.Is(err, ErrKeyNotFound) {
  95. // Since the item no longer exists upstream, it's safe to remove it from the cache.
  96. p.invalidateItem(providerItem)
  97. return nil
  98. }
  99. if err != nil {
  100. // do not remove cache entry because the error might be a network problem
  101. // or something unrelated.
  102. return err
  103. }
  104. defer func() {
  105. if err == nil {
  106. // invalidate the cache if there was no error
  107. p.invalidateItem(providerItem)
  108. }
  109. }()
  110. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  111. var deleted bool
  112. providerItem.Fields, deleted, err = deleteField(providerItem.Fields, ref.GetProperty())
  113. if err != nil {
  114. return fmt.Errorf("failed to delete fields: %w", err)
  115. }
  116. if !deleted {
  117. // also invalidate the cache on not deleted so we refresh the fields on an item.
  118. return nil
  119. }
  120. // There is a chance that there is an empty item left in the section like this: [{ID: Title:}].
  121. if len(providerItem.Sections) == 1 && providerItem.Sections[0].ID == "" && providerItem.Sections[0].Title == "" {
  122. providerItem.Sections = nil
  123. }
  124. if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
  125. err = p.client.Items().Delete(ctx, providerItem.VaultID, providerItem.ID)
  126. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsDelete, err)
  127. if err != nil {
  128. return fmt.Errorf("failed to delete item: %w", err)
  129. }
  130. return nil
  131. }
  132. _, err = p.client.Items().Put(ctx, providerItem)
  133. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  134. if err != nil {
  135. return fmt.Errorf(errMsgUpdateItem, err)
  136. }
  137. return nil
  138. }
  139. func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.ItemField, bool, error) {
  140. // This will always iterate over all items,
  141. // but it's done to ensure that two fields with the same label
  142. // exist resulting in undefined behavior
  143. var (
  144. found bool
  145. fieldsF = make([]onepassword.ItemField, 0, len(fields))
  146. )
  147. for _, item := range fields {
  148. if item.Title == title {
  149. if found {
  150. return nil, false, fmt.Errorf("found multiple labels on item %q", title)
  151. }
  152. found = true
  153. continue
  154. }
  155. fieldsF = append(fieldsF, item)
  156. }
  157. return fieldsF, found, nil
  158. }
  159. // GetAllSecrets syncs multiple 1Password Items into a single Kubernetes Secret, for dataFrom.find.
  160. func (p *SecretsClient) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
  161. items, err := p.listItems(ctx)
  162. if err != nil {
  163. return nil, err
  164. }
  165. // If ref.Tags is set, filter to only items that match the given tags
  166. if ref.Tags != nil {
  167. var filteredItems []onepassword.ItemOverview
  168. for _, item := range items {
  169. if itemHasTags(ref.Tags, item.Tags) {
  170. filteredItems = append(filteredItems, item)
  171. }
  172. }
  173. items = filteredItems
  174. }
  175. secretData := make(map[string][]byte)
  176. for _, overview := range items {
  177. if ref.Path != nil && *ref.Path != overview.Title {
  178. continue
  179. }
  180. if err := p.collectAllSecrets(ctx, overview.Title, ref, secretData); err != nil {
  181. return nil, err
  182. }
  183. }
  184. return secretData, nil
  185. }
  186. func (p *SecretsClient) collectAllSecrets(ctx context.Context, itemName string, ref esv1.ExternalSecretFind, secretData map[string][]byte) error {
  187. item, err := p.findItem(ctx, itemName)
  188. if err != nil {
  189. return fmt.Errorf("failed to get item %s: %w", itemName, err)
  190. }
  191. if err := p.getAllFields(item, ref, secretData); err != nil {
  192. return fmt.Errorf("failed to get fields for item %s: %w", itemName, err)
  193. }
  194. if err := p.getAllFiles(ctx, item, ref, secretData); err != nil {
  195. return fmt.Errorf("failed to get files for item %s: %w", itemName, err)
  196. }
  197. return nil
  198. }
  199. // itemHasTags returns true if all required keys are present in the item's tags.
  200. func itemHasTags(required map[string]string, itemTags []string) bool {
  201. // Quickly return false if this item has fewer tags than required, since it can't possibly match.
  202. if len(itemTags) < len(required) {
  203. return false
  204. }
  205. // Use a map to track which required tags we've found in the item's tags.
  206. matchingTags := make(map[string]string)
  207. // Loop through item's tags and add any matching tags to the matchingTags map.
  208. for _, itemTag := range itemTags {
  209. if _, ok := required[itemTag]; ok {
  210. matchingTags[itemTag] = required[itemTag]
  211. }
  212. }
  213. // Check if we found all required tags in the item's tags.
  214. if len(matchingTags) < len(required) {
  215. return false
  216. }
  217. return true
  218. }
  219. // GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.
  220. func (p *SecretsClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  221. if ref.Version != "" {
  222. return nil, errors.New(errVersionNotImplemented)
  223. }
  224. cacheKey := p.constructRefKey(ref.Key) + "|" + ref.Property
  225. if cached, ok := p.cacheGet(cacheKey); ok {
  226. var result map[string][]byte
  227. if err := json.Unmarshal(cached, &result); err == nil {
  228. return result, nil
  229. }
  230. // continue with fresh instead
  231. }
  232. item, err := p.findItem(ctx, ref.Key)
  233. if err != nil {
  234. return nil, err
  235. }
  236. var result map[string][]byte
  237. propertyType, property := getObjType(item.Category, ref.Property)
  238. if propertyType == filePrefix {
  239. result, err = p.getFiles(ctx, item, property)
  240. } else {
  241. result, err = p.getFields(item, property)
  242. }
  243. if err != nil {
  244. return nil, err
  245. }
  246. if serialized, err := json.Marshal(result); err == nil {
  247. p.cacheAdd(cacheKey, serialized)
  248. }
  249. return result, nil
  250. }
  251. func (p *SecretsClient) listItems(ctx context.Context) ([]onepassword.ItemOverview, error) {
  252. var items []onepassword.ItemOverview
  253. cacheKey := vaultCachePrefix + p.vaultID
  254. if cached, ok := p.cacheGet(cacheKey); ok {
  255. if err := json.Unmarshal(cached, &items); err == nil {
  256. return items, nil
  257. }
  258. }
  259. // Vault item list not found in cache - fetch from the API
  260. items, err := p.client.Items().List(ctx, p.vaultID)
  261. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsList, err)
  262. if err != nil {
  263. return nil, fmt.Errorf("failed to list items: %w", err)
  264. }
  265. // Add the vault list to the cache
  266. if serialized, err := json.Marshal(items); err == nil {
  267. p.cacheAdd(cacheKey, serialized)
  268. } else {
  269. // If we fail to serialize the items for caching, we can still return the items, so we just log the error and continue.
  270. fmt.Printf("failed to serialize items for caching: %v\n", err)
  271. }
  272. return items, nil
  273. }
  274. // getFields gets the field matching the given property label in an item, or all fields in the item if `property` is not set.
  275. func (p *SecretsClient) getFields(item onepassword.Item, property string) (map[string][]byte, error) {
  276. secretData := make(map[string][]byte)
  277. for _, field := range item.Fields {
  278. if property != "" && field.Title != property {
  279. continue
  280. }
  281. // Throw error if there are multiple fields with the same label.
  282. if length := countFieldsWithLabel(field.Title, item.Fields); length != 1 {
  283. return nil, fmt.Errorf(errMsgExpectedOneField, field.Title, item.Title, length)
  284. }
  285. // caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
  286. secretData[field.Title] = []byte(field.Value)
  287. }
  288. return secretData, nil
  289. }
  290. // getAllFields retrieves all fields matching the given ref in an item, and adds them to the given secretData map.
  291. func (p *SecretsClient) getAllFields(item onepassword.Item, ref esv1.ExternalSecretFind, secretData map[string][]byte) error {
  292. var matcher *find.Matcher
  293. if ref.Name != nil {
  294. var err error
  295. matcher, err = find.New(*ref.Name)
  296. if err != nil {
  297. return err
  298. }
  299. }
  300. for _, field := range item.Fields {
  301. // Throw error if there are multiple fields in this item with the same label.
  302. if length := countFieldsWithLabel(field.Title, item.Fields); length != 1 {
  303. return fmt.Errorf(errMsgExpectedOneField, field.Title, item.Title, length)
  304. }
  305. // If ref.Name is set, only add fields that match the regex pattern.
  306. if matcher != nil && !matcher.MatchName(field.Title) {
  307. continue
  308. }
  309. // Throw error if there are multiple fields with the same label.
  310. if _, found := secretData[field.Title]; found {
  311. return fmt.Errorf("found multiple labels with the same key '%s'", field.Title)
  312. }
  313. secretData[field.Title] = []byte(field.Value)
  314. }
  315. return nil
  316. }
  317. // fetchFile retrieves the content of a file, using the cache if possible.
  318. // TODO - Currently, cached files are not invalidated on updates. This should be done as part of the cache refactor.
  319. // See GitHub issue: https://github.com/external-secrets/external-secrets/issues/6444
  320. func (p *SecretsClient) fetchFile(ctx context.Context, itemID, fieldID string, attributes onepassword.FileAttributes) ([]byte, error) {
  321. cacheKey := fileCachePrefix + p.vaultID + ":" + itemID + ":" + fieldID + ":" + attributes.Name
  322. if cached, ok := p.cacheGet(cacheKey); ok {
  323. return cached, nil
  324. }
  325. contents, err := p.client.Items().Files().Read(ctx, p.vaultID, fieldID, attributes)
  326. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKFilesRead, err)
  327. if err != nil {
  328. return nil, fmt.Errorf("failed to read file: %w", err)
  329. }
  330. p.cacheAdd(cacheKey, contents)
  331. return contents, nil
  332. }
  333. // getFiles gets the file matching the given property label in an item, or all files in the item if `property` is not set.
  334. func (p *SecretsClient) getFiles(ctx context.Context, item onepassword.Item, property string) (map[string][]byte, error) {
  335. secretData := make(map[string][]byte)
  336. for _, file := range item.Files {
  337. if property != "" && file.Attributes.Name != property {
  338. continue
  339. }
  340. // Throw error if there are multiple files with the same label.
  341. if length := countFilesWithLabel(file.Attributes.Name, item.Files); length != 1 {
  342. return nil, fmt.Errorf(errMsgExpectedOneFile, file.Attributes.Name, item.Title, length)
  343. }
  344. contents, err := p.fetchFile(ctx, item.ID, file.FieldID, file.Attributes)
  345. if err != nil {
  346. return nil, err
  347. }
  348. secretData[file.Attributes.Name] = contents
  349. }
  350. return secretData, nil
  351. }
  352. // getAllFiles retrieves all files matching the given ref in an item, and adds them to the given secretData map.
  353. func (p *SecretsClient) getAllFiles(ctx context.Context, item onepassword.Item, ref esv1.ExternalSecretFind, secretData map[string][]byte) error {
  354. var matcher *find.Matcher
  355. if ref.Name != nil {
  356. var err error
  357. matcher, err = find.New(*ref.Name)
  358. if err != nil {
  359. return err
  360. }
  361. }
  362. for _, file := range item.Files {
  363. if matcher != nil && !matcher.MatchName(file.Attributes.Name) {
  364. continue
  365. }
  366. // Throw error if there are multiple files with the same label.
  367. if _, found := secretData[file.Attributes.Name]; found {
  368. return fmt.Errorf("found multiple labels with the same key '%s'", file.Attributes.Name)
  369. }
  370. contents, err := p.fetchFile(ctx, item.ID, file.FieldID, file.Attributes)
  371. if err != nil {
  372. return err
  373. }
  374. secretData[file.Attributes.Name] = contents
  375. }
  376. return nil
  377. }
  378. func countFieldsWithLabel(fieldLabel string, fields []onepassword.ItemField) int {
  379. count := 0
  380. for _, field := range fields {
  381. if field.Title == fieldLabel {
  382. count++
  383. }
  384. }
  385. return count
  386. }
  387. func countFilesWithLabel(fileLabel string, files []onepassword.ItemFile) int {
  388. count := 0
  389. for _, file := range files {
  390. if file.Attributes.Name == fileLabel {
  391. count++
  392. }
  393. }
  394. return count
  395. }
  396. // Clean property string by removing property prefix if needed.
  397. func getObjType(documentType onepassword.ItemCategory, property string) (string, string) {
  398. if strings.HasPrefix(property, fieldPrefix+prefixSplitter) {
  399. return fieldPrefix, property[6:]
  400. }
  401. if strings.HasPrefix(property, filePrefix+prefixSplitter) {
  402. return filePrefix, property[5:]
  403. }
  404. if documentType == onepassword.ItemCategoryDocument {
  405. return filePrefix, property
  406. }
  407. return fieldPrefix, property
  408. }
  409. // createItem creates a new item in the first vault. If no vaults exist, it returns an error.
  410. func (p *SecretsClient) createItem(ctx context.Context, val []byte, ref esv1.PushSecretData) error {
  411. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  412. if err != nil {
  413. return fmt.Errorf(errMsgParsePushMeta, err)
  414. }
  415. label := ref.GetProperty()
  416. if label == "" {
  417. label = defaultFieldLabel
  418. }
  419. var tags []string
  420. if mdata != nil && mdata.Spec.Tags != nil {
  421. tags = mdata.Spec.Tags
  422. }
  423. fieldType := onepassword.ItemFieldTypeConcealed
  424. if mdata != nil {
  425. fieldType = resolveFieldType(mdata.Spec.FieldType)
  426. }
  427. createdItem, err := p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  428. Category: onepassword.ItemCategoryServer,
  429. VaultID: p.vaultID,
  430. Title: ref.GetRemoteKey(),
  431. Fields: []onepassword.ItemField{
  432. generateNewItemField(label, string(val), fieldType),
  433. },
  434. Tags: tags,
  435. })
  436. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsCreate, err)
  437. if err != nil {
  438. return fmt.Errorf(errMsgCreateItem, err)
  439. }
  440. p.invalidateItem(createdItem)
  441. return nil
  442. }
  443. // updateFieldValue updates the fields value of an item with the given label.
  444. // If the label does not exist, a new field is created with the given fieldType. If the label exists but
  445. // the value is different, the value is updated. If the label exists and the
  446. // value is the same, nothing is done.
  447. func updateFieldValue(fields []onepassword.ItemField, title, newVal string, fieldType onepassword.ItemFieldType) ([]onepassword.ItemField, error) {
  448. // This will always iterate over all items.
  449. // This is done to ensure that two fields with the same label
  450. // exist resulting in undefined behavior.
  451. var (
  452. found bool
  453. index int
  454. )
  455. for i, item := range fields {
  456. if item.Title == title {
  457. if found {
  458. return nil, fmt.Errorf("found multiple labels with the same key")
  459. }
  460. found = true
  461. index = i
  462. }
  463. }
  464. if !found {
  465. return append(fields, generateNewItemField(title, newVal, fieldType)), nil
  466. }
  467. if fields[index].Value != newVal {
  468. fields[index].Value = newVal
  469. }
  470. if fields[index].FieldType != fieldType {
  471. fields[index].FieldType = fieldType
  472. }
  473. return fields, nil
  474. }
  475. // resolveFieldType maps a 1Password field type name to the SDK constant.
  476. // Case-insensitive. Accepted: text|string, concealed|password, url, email, phone, date, monthYear.
  477. // Defaults to Concealed for empty/unrecognized. OTP and file excluded.
  478. // Reference: https://developer.1password.com/docs/cli/item-fields/#custom-fields
  479. func resolveFieldType(raw string) onepassword.ItemFieldType {
  480. switch strings.ToLower(raw) {
  481. case "text", "string":
  482. return onepassword.ItemFieldTypeText
  483. case "concealed", "password":
  484. return onepassword.ItemFieldTypeConcealed
  485. case "email":
  486. return onepassword.ItemFieldTypeEmail
  487. case "url":
  488. return onepassword.ItemFieldTypeURL
  489. case "phone":
  490. return onepassword.ItemFieldTypePhone
  491. case "date":
  492. return onepassword.ItemFieldTypeDate
  493. case "monthyear":
  494. return onepassword.ItemFieldTypeMonthYear
  495. }
  496. return onepassword.ItemFieldTypeConcealed
  497. }
  498. // normalizeItemFields clears empty section IDs because the 1Password SDK rejects items with a SectionID pointer to "" when the section is missing.
  499. func normalizeItemFields(fields []onepassword.ItemField) []onepassword.ItemField {
  500. for i := range fields {
  501. if fields[i].SectionID != nil && *fields[i].SectionID == "" {
  502. fields[i].SectionID = nil
  503. }
  504. }
  505. return fields
  506. }
  507. // generateNewItemField creates an ItemField with ID and Title set to the given title (unique within item), value, and field type.
  508. func generateNewItemField(title, newVal string, fieldType onepassword.ItemFieldType) onepassword.ItemField {
  509. return onepassword.ItemField{
  510. ID: title,
  511. Title: title,
  512. Value: newVal,
  513. FieldType: fieldType,
  514. }
  515. }
  516. // PushSecret creates or updates a secret in 1Password.
  517. func (p *SecretsClient) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  518. if ref.GetSecretKey() == "" {
  519. return p.pushAllKeys(ctx, secret, ref)
  520. }
  521. val, ok := secret.Data[ref.GetSecretKey()]
  522. if !ok {
  523. return fmt.Errorf("secret %s/%s does not contain a key", secret.Namespace, secret.Name)
  524. }
  525. title := ref.GetRemoteKey()
  526. providerItem, err := p.findItem(ctx, title)
  527. if errors.Is(err, ErrKeyNotFound) {
  528. return p.createItem(ctx, val, ref)
  529. } else if err != nil {
  530. return fmt.Errorf("failed to find item: %w", err)
  531. }
  532. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  533. label := ref.GetProperty()
  534. if label == "" {
  535. label = defaultFieldLabel
  536. }
  537. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  538. if err != nil {
  539. return fmt.Errorf(errMsgParsePushMeta, err)
  540. }
  541. if mdata != nil && mdata.Spec.Tags != nil {
  542. providerItem.Tags = mdata.Spec.Tags
  543. }
  544. fieldType := onepassword.ItemFieldTypeConcealed
  545. if mdata != nil {
  546. fieldType = resolveFieldType(mdata.Spec.FieldType)
  547. }
  548. providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val), fieldType)
  549. if err != nil {
  550. return fmt.Errorf("failed to update field with label: %s: %w", label, err)
  551. }
  552. _, err = p.client.Items().Put(ctx, providerItem)
  553. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  554. if err != nil {
  555. return fmt.Errorf(errMsgUpdateItem, err)
  556. }
  557. p.invalidateItem(providerItem)
  558. return nil
  559. }
  560. // createAllKeysItem creates a new item with all keys from secret.Data.
  561. func (p *SecretsClient) createAllKeysItem(ctx context.Context, secret *corev1.Secret, title string, tags []string, fieldType onepassword.ItemFieldType) error {
  562. fields := make([]onepassword.ItemField, 0, len(secret.Data))
  563. for k, v := range secret.Data {
  564. fields = append(fields, generateNewItemField(k, string(v), fieldType))
  565. }
  566. createdItem, err := p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  567. Category: onepassword.ItemCategoryServer,
  568. VaultID: p.vaultID,
  569. Title: title,
  570. Fields: fields,
  571. Tags: tags,
  572. })
  573. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsCreate, err)
  574. if err != nil {
  575. return fmt.Errorf(errMsgCreateItem, err)
  576. }
  577. p.invalidateItem(createdItem)
  578. return nil
  579. }
  580. // pushAllKeys pushes all keys from secret.Data as separate fields on a single 1Password item.
  581. func (p *SecretsClient) pushAllKeys(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  582. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  583. if err != nil {
  584. return fmt.Errorf(errMsgParsePushMeta, err)
  585. }
  586. var tags []string
  587. if mdata != nil && mdata.Spec.Tags != nil {
  588. tags = mdata.Spec.Tags
  589. }
  590. fieldType := onepassword.ItemFieldTypeConcealed
  591. if mdata != nil {
  592. fieldType = resolveFieldType(mdata.Spec.FieldType)
  593. }
  594. title := ref.GetRemoteKey()
  595. providerItem, err := p.findItem(ctx, title)
  596. if errors.Is(err, ErrKeyNotFound) {
  597. return p.createAllKeysItem(ctx, secret, title, tags, fieldType)
  598. }
  599. if err != nil {
  600. return fmt.Errorf("failed to find item: %w", err)
  601. }
  602. providerItem.Fields = normalizeItemFields(providerItem.Fields)
  603. if tags != nil {
  604. providerItem.Tags = tags
  605. }
  606. kept := make([]onepassword.ItemField, 0, len(providerItem.Fields))
  607. for _, f := range providerItem.Fields {
  608. if v, ok := secret.Data[f.Title]; ok {
  609. f.Value = string(v)
  610. f.FieldType = fieldType
  611. kept = append(kept, f)
  612. }
  613. }
  614. for k, v := range secret.Data {
  615. if countFieldsWithLabel(k, kept) == 0 {
  616. kept = append(kept, generateNewItemField(k, string(v), fieldType))
  617. }
  618. }
  619. providerItem.Fields = kept
  620. _, err = p.client.Items().Put(ctx, providerItem)
  621. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsPut, err)
  622. if err != nil {
  623. return fmt.Errorf(errMsgUpdateItem, err)
  624. }
  625. p.invalidateItem(providerItem)
  626. return nil
  627. }
  628. // GetVault retrieves a vault by its title or UUID from 1Password.
  629. func (p *SecretsClient) GetVault(ctx context.Context, titleOrUUID string) (string, error) {
  630. vaults, err := p.client.VaultsAPI.List(ctx)
  631. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKVaultsList, err)
  632. if err != nil {
  633. return "", fmt.Errorf("failed to list vaults: %w", err)
  634. }
  635. for _, v := range vaults {
  636. if v.Title == titleOrUUID || v.ID == titleOrUUID {
  637. return v.ID, nil
  638. }
  639. }
  640. return "", fmt.Errorf("vault %s not found", titleOrUUID)
  641. }
  642. // fetchItemByID retrieves an item by its ID, using the cache if possible.
  643. func (p *SecretsClient) fetchItemByID(ctx context.Context, id string) (onepassword.Item, error) {
  644. cacheKey := itemCachePrefix + p.vaultID + ":" + id
  645. if cached, ok := p.cacheGet(cacheKey); ok {
  646. var item onepassword.Item
  647. if err := json.Unmarshal(cached, &item); err == nil {
  648. return item, nil
  649. }
  650. }
  651. item, err := p.client.Items().Get(ctx, p.vaultID, id)
  652. metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
  653. if err != nil {
  654. return onepassword.Item{}, err
  655. }
  656. if serialized, err := json.Marshal(item); err == nil {
  657. p.cacheAdd(cacheKey, serialized)
  658. }
  659. return item, nil
  660. }
  661. // findItem retrieves an item by its title or ID, using the cache if possible.
  662. func (p *SecretsClient) findItem(ctx context.Context, name string) (onepassword.Item, error) {
  663. cacheKey := itemCachePrefix + p.vaultID + ":" + name
  664. if cached, ok := p.cacheGet(cacheKey); ok {
  665. var item onepassword.Item
  666. if err := json.Unmarshal(cached, &item); err == nil {
  667. return item, nil
  668. }
  669. }
  670. var item onepassword.Item
  671. var err error
  672. if isNativeID(name) {
  673. item, err = p.fetchItemByID(ctx, name)
  674. if err != nil {
  675. if isNotFoundError(err) {
  676. return onepassword.Item{}, ErrKeyNotFound
  677. }
  678. return onepassword.Item{}, err
  679. }
  680. } else {
  681. // If name is not a native item ID, we have to list items and find the matching title.
  682. items, err := p.listItems(ctx)
  683. if err != nil {
  684. return onepassword.Item{}, fmt.Errorf("failed to list items: %w", err)
  685. }
  686. // Find the ID of the item matching the given name. Throw an error if there are multiple items with the same name, or if no items are found.
  687. var itemUUID string
  688. for _, v := range items {
  689. if v.Title == name {
  690. if itemUUID != "" {
  691. return onepassword.Item{}, fmt.Errorf("found multiple items with name %s", name)
  692. }
  693. itemUUID = v.ID
  694. }
  695. }
  696. if itemUUID == "" {
  697. return onepassword.Item{}, ErrKeyNotFound
  698. }
  699. // Fetch the item by ID to get all its details.
  700. item, err = p.fetchItemByID(ctx, itemUUID)
  701. if err != nil {
  702. return onepassword.Item{}, err
  703. }
  704. // While fetchItemByID will cache the item by its ID, we also want to cache it by its name.
  705. if serialized, err := json.Marshal(item); err == nil {
  706. p.cacheAdd(cacheKey, serialized)
  707. }
  708. }
  709. return item, nil
  710. }
  711. // resolveFieldFromCachedItem satisfies a GetSecret request from an item already cached by
  712. // GetAllSecrets/GetSecretMap, avoiding a Resolve API call. It only handles plain field
  713. // lookups; files, sections, and cache misses return false so the caller falls back to Resolve.
  714. func (p *SecretsClient) resolveFieldFromCachedItem(refKey string) ([]byte, bool) {
  715. itemName, property, ok := strings.Cut(refKey, prefixSplitter)
  716. if !ok || property == "" {
  717. return nil, false
  718. }
  719. cached, ok := p.cacheGet(itemCachePrefix + p.vaultID + ":" + itemName)
  720. if !ok {
  721. return nil, false
  722. }
  723. var item onepassword.Item
  724. if err := json.Unmarshal(cached, &item); err != nil {
  725. return nil, false
  726. }
  727. objType, prop := getObjType(item.Category, property)
  728. if objType != fieldPrefix {
  729. return nil, false
  730. }
  731. fields, err := p.getFields(item, prop)
  732. if err != nil {
  733. return nil, false
  734. }
  735. value, ok := fields[prop]
  736. return value, ok
  737. }
  738. // SecretExists returns true if the item exists, and if a property is specified, if a field with that title exists.
  739. func (p *SecretsClient) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  740. item, err := p.findItem(ctx, ref.GetRemoteKey())
  741. if errors.Is(err, ErrKeyNotFound) {
  742. return false, nil
  743. }
  744. if err != nil {
  745. return false, err
  746. }
  747. property := ref.GetProperty()
  748. if property == "" {
  749. return true, nil // item exists; pushAllKeys handles field-level reconciliation
  750. }
  751. for _, f := range item.Fields {
  752. if f.Title == property {
  753. return true, nil
  754. }
  755. }
  756. return false, nil
  757. }
  758. // Validate does nothing here. It would be possible to ping the SDK to prove we're healthy, but
  759. // since the 1password SDK rate-limit is pretty aggressive, we prefer to do nothing.
  760. func (p *SecretsClient) Validate() (esv1.ValidationResult, error) {
  761. return esv1.ValidationResultReady, nil
  762. }
  763. func (p *SecretsClient) constructRefKey(key string) string {
  764. // remove any possible leading slashes because the vaultPrefix already contains it.
  765. return p.vaultPrefix + strings.TrimPrefix(key, "/")
  766. }
  767. // cacheGet retrieves a value from the cache. Returns false if cache is disabled or key not found.
  768. func (p *SecretsClient) cacheGet(key string) ([]byte, bool) {
  769. if p.cache == nil {
  770. return nil, false
  771. }
  772. v, ok := p.cache.Get(key)
  773. if !ok {
  774. return nil, false
  775. }
  776. return bytes.Clone(v), true
  777. }
  778. // cacheAdd stores a value in the cache. No-op if cache is disabled.
  779. func (p *SecretsClient) cacheAdd(key string, value []byte) {
  780. if p.cache == nil {
  781. return
  782. }
  783. p.cache.Add(key, value)
  784. }
  785. // invalidateCacheByPrefix removes all cache entries that start with the given prefix.
  786. // This is used to invalidate cache entries when an item is modified or deleted.
  787. // No-op if cache is disabled.
  788. // Why are we using a Prefix? Because items and properties are stored via prefixes using 1Password SDK.
  789. // This means when an item is deleted we delete the fields and properties that belong to the item as well.
  790. // This is a helper for invalidateItem. Do not call directly.
  791. func (p *SecretsClient) invalidateCacheByPrefix(prefix string) {
  792. if p.cache == nil {
  793. return
  794. }
  795. keys := p.cache.Keys()
  796. for _, key := range keys {
  797. if !strings.HasPrefix(key, prefix) {
  798. continue
  799. }
  800. if len(key) == len(prefix) || key[len(prefix)] == '/' || key[len(prefix)] == '|' {
  801. p.cache.Remove(key)
  802. }
  803. }
  804. }
  805. // invalidateItem drops every cache entry tied to an item after a mutation: the
  806. // resolved values (op://...), both the title- and ID-keyed item entries, and the
  807. // vault item list. Mutations are addressed by title, but findItem always resolves
  808. // through the item's UUID and listItems backs every title->UUID lookup, so all
  809. // three must be dropped or reads return stale data.
  810. // No-op if cache is disabled.
  811. func (p *SecretsClient) invalidateItem(item onepassword.Item) {
  812. if p.cache == nil {
  813. return
  814. }
  815. p.invalidateCacheByPrefix(p.constructRefKey(item.Title))
  816. if item.ID != "" && item.ID != item.Title {
  817. p.invalidateCacheByPrefix(p.constructRefKey(item.ID))
  818. }
  819. p.cache.Remove(itemCachePrefix + p.vaultID + ":" + item.Title)
  820. if item.ID != "" {
  821. p.cache.Remove(itemCachePrefix + p.vaultID + ":" + item.ID)
  822. }
  823. p.cache.Remove(vaultCachePrefix + p.vaultID)
  824. }
  825. func isNotFoundError(err error) bool {
  826. msg := strings.ToLower(err.Error())
  827. return strings.Contains(msg, "couldn't be found") || strings.Contains(msg, "resource not found")
  828. }