client.go 33 KB

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