client.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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. "context"
  17. "errors"
  18. "fmt"
  19. "strings"
  20. "github.com/1password/onepassword-sdk-go"
  21. corev1 "k8s.io/api/core/v1"
  22. "k8s.io/kube-openapi/pkg/validation/strfmt"
  23. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  24. "github.com/external-secrets/external-secrets/pkg/esutils/metadata"
  25. )
  26. const (
  27. fieldPrefix = "field"
  28. filePrefix = "file"
  29. prefixSplitter = "/"
  30. errExpectedOneFieldMsgF = "found more than 1 fields with title '%s' in '%s', got %d"
  31. )
  32. // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
  33. var ErrKeyNotFound = errors.New("key not found")
  34. // PushSecretMetadataSpec defines the metadata configuration for pushing secrets to 1Password.
  35. type PushSecretMetadataSpec struct {
  36. Tags []string `json:"tags,omitempty"`
  37. }
  38. // GetSecret returns a single secret from 1Password provider.
  39. // Follows syntax is used for the ref key: https://developer.1password.com/docs/cli/secret-reference-syntax/
  40. func (p *Provider) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  41. if ref.Version != "" {
  42. return nil, errors.New(errVersionNotImplemented)
  43. }
  44. key := p.constructRefKey(ref.Key)
  45. secret, err := p.client.Secrets().Resolve(ctx, key)
  46. if err != nil {
  47. return nil, err
  48. }
  49. return []byte(secret), nil
  50. }
  51. // Close closes the client connection.
  52. func (p *Provider) Close(_ context.Context) error {
  53. return nil
  54. }
  55. // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
  56. func (p *Provider) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  57. providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
  58. if err != nil {
  59. return err
  60. }
  61. providerItem.Fields, err = deleteField(providerItem.Fields, ref.GetProperty())
  62. if err != nil {
  63. return fmt.Errorf("failed to delete fields: %w", err)
  64. }
  65. // There is a chance that there is an empty item left in the section like this: [{ID: Title:}].
  66. if len(providerItem.Sections) == 1 && providerItem.Sections[0].ID == "" && providerItem.Sections[0].Title == "" {
  67. providerItem.Sections = nil
  68. }
  69. if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
  70. // Delete the item if there are no fields, files or sections
  71. if err = p.client.Items().Delete(ctx, providerItem.VaultID, providerItem.ID); err != nil {
  72. return fmt.Errorf("failed to delete item: %w", err)
  73. }
  74. return nil
  75. }
  76. if _, err = p.client.Items().Put(ctx, providerItem); err != nil {
  77. return fmt.Errorf("failed to update item: %w", err)
  78. }
  79. return nil
  80. }
  81. func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.ItemField, error) {
  82. // This will always iterate over all items,
  83. // but it's done to ensure that two fields with the same label
  84. // exist resulting in undefined behavior
  85. var (
  86. found bool
  87. fieldsF = make([]onepassword.ItemField, 0, len(fields))
  88. )
  89. for _, item := range fields {
  90. if item.Title == title {
  91. if found {
  92. return nil, fmt.Errorf("found multiple labels on item %q", title)
  93. }
  94. found = true
  95. continue
  96. }
  97. fieldsF = append(fieldsF, item)
  98. }
  99. return fieldsF, nil
  100. }
  101. // GetAllSecrets Not Implemented.
  102. func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  103. return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errNotImplemented))
  104. }
  105. // GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.
  106. func (p *Provider) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  107. if ref.Version != "" {
  108. return nil, errors.New(errVersionNotImplemented)
  109. }
  110. item, err := p.findItem(ctx, ref.Key)
  111. if err != nil {
  112. return nil, err
  113. }
  114. propertyType, property := getObjType(item.Category, ref.Property)
  115. if propertyType == filePrefix {
  116. return p.getFiles(ctx, item, property)
  117. }
  118. return p.getFields(item, property)
  119. }
  120. func (p *Provider) getFields(item onepassword.Item, property string) (map[string][]byte, error) {
  121. secretData := make(map[string][]byte)
  122. for _, field := range item.Fields {
  123. if property != "" && field.Title != property {
  124. continue
  125. }
  126. if length := countFieldsWithLabel(field.Title, item.Fields); length != 1 {
  127. return nil, fmt.Errorf(errExpectedOneFieldMsgF, field.Title, item.Title, length)
  128. }
  129. // caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
  130. secretData[field.Title] = []byte(field.Value)
  131. }
  132. return secretData, nil
  133. }
  134. func (p *Provider) getFiles(ctx context.Context, item onepassword.Item, property string) (map[string][]byte, error) {
  135. secretData := make(map[string][]byte)
  136. for _, file := range item.Files {
  137. if property != "" && file.Attributes.Name != property {
  138. continue
  139. }
  140. contents, err := p.client.Items().Files().Read(ctx, p.vaultID, file.FieldID, file.Attributes)
  141. if err != nil {
  142. return nil, fmt.Errorf("failed to read file: %w", err)
  143. }
  144. secretData[file.Attributes.Name] = contents
  145. }
  146. return secretData, nil
  147. }
  148. func countFieldsWithLabel(fieldLabel string, fields []onepassword.ItemField) int {
  149. count := 0
  150. for _, field := range fields {
  151. if field.Title == fieldLabel {
  152. count++
  153. }
  154. }
  155. return count
  156. }
  157. // Clean property string by removing property prefix if needed.
  158. func getObjType(documentType onepassword.ItemCategory, property string) (string, string) {
  159. if strings.HasPrefix(property, fieldPrefix+prefixSplitter) {
  160. return fieldPrefix, property[6:]
  161. }
  162. if strings.HasPrefix(property, filePrefix+prefixSplitter) {
  163. return filePrefix, property[5:]
  164. }
  165. if documentType == onepassword.ItemCategoryDocument {
  166. return filePrefix, property
  167. }
  168. return fieldPrefix, property
  169. }
  170. // createItem creates a new item in the first vault. If no vaults exist, it returns an error.
  171. func (p *Provider) createItem(ctx context.Context, val []byte, ref esv1.PushSecretData) error {
  172. // Get the metadata
  173. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  174. if err != nil {
  175. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  176. }
  177. // Get the label
  178. label := ref.GetProperty()
  179. if label == "" {
  180. label = "password"
  181. }
  182. var tags []string
  183. if mdata != nil && mdata.Spec.Tags != nil {
  184. tags = mdata.Spec.Tags
  185. }
  186. // Create the item
  187. _, err = p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  188. Category: onepassword.ItemCategoryServer,
  189. VaultID: p.vaultID,
  190. Title: ref.GetRemoteKey(),
  191. Fields: []onepassword.ItemField{
  192. generateNewItemField(label, string(val)),
  193. },
  194. Tags: tags,
  195. })
  196. if err != nil {
  197. return fmt.Errorf("failed to create item: %w", err)
  198. }
  199. return nil
  200. }
  201. // updateFieldValue updates the fields value of an item with the given label.
  202. // If the label does not exist, a new field is created. If the label exists but
  203. // the value is different, the value is updated. If the label exists and the
  204. // value is the same, nothing is done.
  205. func updateFieldValue(fields []onepassword.ItemField, title, newVal string) ([]onepassword.ItemField, error) {
  206. // This will always iterate over all items.
  207. // This is done to ensure that two fields with the same label
  208. // exist resulting in undefined behavior.
  209. var (
  210. found bool
  211. index int
  212. )
  213. for i, item := range fields {
  214. if item.Title == title {
  215. if found {
  216. return nil, fmt.Errorf("found multiple labels with the same key")
  217. }
  218. found = true
  219. index = i
  220. }
  221. }
  222. if !found {
  223. return append(fields, generateNewItemField(title, newVal)), nil
  224. }
  225. if fields[index].Value != newVal {
  226. fields[index].Value = newVal
  227. }
  228. return fields, nil
  229. }
  230. // generateNewItemField generates a new item field with the given label and value.
  231. func generateNewItemField(title, newVal string) onepassword.ItemField {
  232. field := onepassword.ItemField{
  233. Title: title,
  234. Value: newVal,
  235. FieldType: onepassword.ItemFieldTypeConcealed,
  236. }
  237. return field
  238. }
  239. // PushSecret creates or updates a secret in 1Password.
  240. func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  241. val, ok := secret.Data[ref.GetSecretKey()]
  242. if !ok {
  243. return fmt.Errorf("secret %s/%s does not contain a key", secret.Namespace, secret.Name)
  244. }
  245. title := ref.GetRemoteKey()
  246. providerItem, err := p.findItem(ctx, title)
  247. if errors.Is(err, ErrKeyNotFound) {
  248. if err = p.createItem(ctx, val, ref); err != nil {
  249. return fmt.Errorf("failed to create item: %w", err)
  250. }
  251. return nil
  252. } else if err != nil {
  253. return fmt.Errorf("failed to find item: %w", err)
  254. }
  255. // TODO: We are only sending info to a specific label on a 1password item.
  256. // We should change this logic eventually to allow pushing whole kubernetes Secrets to 1password as multiple labels
  257. // OOTB.
  258. label := ref.GetProperty()
  259. if label == "" {
  260. label = "password"
  261. }
  262. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  263. if err != nil {
  264. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  265. }
  266. if mdata != nil && mdata.Spec.Tags != nil {
  267. providerItem.Tags = mdata.Spec.Tags
  268. }
  269. providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val))
  270. if err != nil {
  271. return fmt.Errorf("failed to update field with value %s: %w", string(val), err)
  272. }
  273. if _, err = p.client.Items().Put(ctx, providerItem); err != nil {
  274. return fmt.Errorf("failed to update item: %w", err)
  275. }
  276. return nil
  277. }
  278. // GetVault retrieves a vault by its title or UUID from 1Password.
  279. func (p *Provider) GetVault(ctx context.Context, titleOrUUID string) (string, error) {
  280. vaults, err := p.client.VaultsAPI.List(ctx)
  281. if err != nil {
  282. return "", fmt.Errorf("failed to list vaults: %w", err)
  283. }
  284. for _, v := range vaults {
  285. if v.Title == titleOrUUID || v.ID == titleOrUUID {
  286. // cache the ID so we don't have to repeat this lookup.
  287. p.vaultID = v.ID
  288. return v.ID, nil
  289. }
  290. }
  291. return "", fmt.Errorf("vault %s not found", titleOrUUID)
  292. }
  293. func (p *Provider) findItem(ctx context.Context, name string) (onepassword.Item, error) {
  294. if strfmt.IsUUID(name) {
  295. return p.client.Items().Get(ctx, p.vaultID, name)
  296. }
  297. items, err := p.client.Items().List(ctx, p.vaultID)
  298. if err != nil {
  299. return onepassword.Item{}, fmt.Errorf("failed to list items: %w", err)
  300. }
  301. // We don't stop
  302. var itemUUID string
  303. for _, v := range items {
  304. if v.Title == name {
  305. if itemUUID != "" {
  306. return onepassword.Item{}, fmt.Errorf("found multiple items with name %s", name)
  307. }
  308. itemUUID = v.ID
  309. }
  310. }
  311. if itemUUID == "" {
  312. return onepassword.Item{}, ErrKeyNotFound
  313. }
  314. return p.client.Items().Get(ctx, p.vaultID, itemUUID)
  315. }
  316. // SecretExists checks if a secret exists in 1Password.
  317. func (p *Provider) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  318. return false, fmt.Errorf("not implemented")
  319. }
  320. // Validate checks if the client is configured correctly
  321. // currently only checks if it is possible to list vaults.
  322. func (p *Provider) Validate() (esv1.ValidationResult, error) {
  323. _, err := p.client.Vaults().List(context.Background())
  324. if err != nil {
  325. return esv1.ValidationResultError, fmt.Errorf("error listing vaults: %w", err)
  326. }
  327. return esv1.ValidationResultReady, nil
  328. }
  329. func (p *Provider) constructRefKey(key string) string {
  330. // remove any possible leading slashes because the vaultPrefix already contains it.
  331. return p.vaultPrefix + strings.TrimPrefix(key, "/")
  332. }