client.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package onepasswordsdk
  13. import (
  14. "context"
  15. "encoding/json"
  16. "errors"
  17. "fmt"
  18. "strings"
  19. "github.com/1password/onepassword-sdk-go"
  20. corev1 "k8s.io/api/core/v1"
  21. "k8s.io/kube-openapi/pkg/validation/strfmt"
  22. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  23. "github.com/external-secrets/external-secrets/pkg/utils/metadata"
  24. )
  25. // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
  26. var ErrKeyNotFound = errors.New("key not found")
  27. type PushSecretMetadataSpec struct {
  28. Tags []string `json:"tags,omitempty"`
  29. }
  30. // GetSecret returns a single secret from the provider.
  31. // Follows syntax is used for the ref key: https://developer.1password.com/docs/cli/secret-reference-syntax/
  32. func (p *Provider) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  33. if ref.Version != "" {
  34. return nil, errors.New(errVersionNotImplemented)
  35. }
  36. key := p.constructRefKey(ref.Key)
  37. secret, err := p.client.Secrets().Resolve(ctx, key)
  38. if err != nil {
  39. return nil, err
  40. }
  41. return []byte(secret), nil
  42. }
  43. // Close closes the client connection.
  44. func (p *Provider) Close(_ context.Context) error {
  45. return nil
  46. }
  47. // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
  48. func (p *Provider) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) error {
  49. providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
  50. if err != nil {
  51. return err
  52. }
  53. providerItem.Fields, err = deleteField(providerItem.Fields, ref.GetProperty())
  54. if err != nil {
  55. return fmt.Errorf("failed to delete fields: %w", err)
  56. }
  57. // There is a chance that there is an empty item left in the section like this: [{ID: Title:}].
  58. if len(providerItem.Sections) == 1 && providerItem.Sections[0].ID == "" && providerItem.Sections[0].Title == "" {
  59. providerItem.Sections = nil
  60. }
  61. if len(providerItem.Fields) == 0 && len(providerItem.Files) == 0 && len(providerItem.Sections) == 0 {
  62. // Delete the item if there are no fields, files or sections
  63. if err = p.client.Items().Delete(ctx, providerItem.VaultID, providerItem.ID); err != nil {
  64. return fmt.Errorf("failed to delete item: %w", err)
  65. }
  66. return nil
  67. }
  68. if _, err = p.client.Items().Put(ctx, providerItem); err != nil {
  69. return fmt.Errorf("failed to update item: %w", err)
  70. }
  71. return nil
  72. }
  73. func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.ItemField, error) {
  74. // This will always iterate over all items,
  75. // but it's done to ensure that two fields with the same label
  76. // exist resulting in undefined behavior
  77. var (
  78. found bool
  79. fieldsF = make([]onepassword.ItemField, 0, len(fields))
  80. )
  81. for _, item := range fields {
  82. if item.Title == title {
  83. if found {
  84. return nil, fmt.Errorf("found multiple labels on item %q", title)
  85. }
  86. found = true
  87. continue
  88. }
  89. fieldsF = append(fieldsF, item)
  90. }
  91. return fieldsF, nil
  92. }
  93. // GetAllSecrets Not Implemented.
  94. func (p *Provider) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  95. return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errNotImplemented))
  96. }
  97. // GetSecretMap implements v1.SecretsClient.
  98. func (p *Provider) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  99. if ref.Version != "" {
  100. return nil, errors.New(errVersionNotImplemented)
  101. }
  102. // Gets a secret as normal, expecting secret value to be a json object
  103. data, err := p.GetSecret(ctx, ref)
  104. if err != nil {
  105. return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
  106. }
  107. // Maps the json data to a string:string map
  108. kv := make(map[string]string)
  109. err = json.Unmarshal(data, &kv)
  110. if err != nil {
  111. return nil, fmt.Errorf("failed to unmarshal data: %w", err)
  112. }
  113. // Converts values in K:V pairs into bytes, while leaving keys as strings
  114. secretData := make(map[string][]byte)
  115. for k, v := range kv {
  116. secretData[k] = []byte(v)
  117. }
  118. return secretData, nil
  119. }
  120. // createItem creates a new item in the first vault. If no vaults exist, it returns an error.
  121. func (p *Provider) createItem(ctx context.Context, val []byte, ref esv1.PushSecretData) error {
  122. // Get the metadata
  123. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  124. if err != nil {
  125. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  126. }
  127. // Get the label
  128. label := ref.GetProperty()
  129. if label == "" {
  130. label = "password"
  131. }
  132. var tags []string
  133. if mdata != nil && mdata.Spec.Tags != nil {
  134. tags = mdata.Spec.Tags
  135. }
  136. // Create the item
  137. _, err = p.client.Items().Create(ctx, onepassword.ItemCreateParams{
  138. Category: onepassword.ItemCategoryServer,
  139. VaultID: p.vaultID,
  140. Title: ref.GetRemoteKey(),
  141. Fields: []onepassword.ItemField{
  142. generateNewItemField(label, string(val)),
  143. },
  144. Tags: tags,
  145. })
  146. if err != nil {
  147. return fmt.Errorf("failed to create item: %w", err)
  148. }
  149. return nil
  150. }
  151. // updateFieldValue updates the fields value of an item with the given label.
  152. // If the label does not exist, a new field is created. If the label exists but
  153. // the value is different, the value is updated. If the label exists and the
  154. // value is the same, nothing is done.
  155. func updateFieldValue(fields []onepassword.ItemField, title, newVal string) ([]onepassword.ItemField, error) {
  156. // This will always iterate over all items.
  157. // This is done to ensure that two fields with the same label
  158. // exist resulting in undefined behavior.
  159. var (
  160. found bool
  161. index int
  162. )
  163. for i, item := range fields {
  164. if item.Title == title {
  165. if found {
  166. return nil, fmt.Errorf("found multiple labels with the same key")
  167. }
  168. found = true
  169. index = i
  170. }
  171. }
  172. if !found {
  173. return append(fields, generateNewItemField(title, newVal)), nil
  174. }
  175. if fields[index].Value != newVal {
  176. fields[index].Value = newVal
  177. }
  178. return fields, nil
  179. }
  180. // generateNewItemField generates a new item field with the given label and value.
  181. func generateNewItemField(title, newVal string) onepassword.ItemField {
  182. field := onepassword.ItemField{
  183. Title: title,
  184. Value: newVal,
  185. FieldType: onepassword.ItemFieldTypeConcealed,
  186. }
  187. return field
  188. }
  189. func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
  190. val, ok := secret.Data[ref.GetSecretKey()]
  191. if !ok {
  192. return fmt.Errorf("secret %s/%s does not contain a key", secret.Namespace, secret.Name)
  193. }
  194. title := ref.GetRemoteKey()
  195. providerItem, err := p.findItem(ctx, title)
  196. if errors.Is(err, ErrKeyNotFound) {
  197. if err = p.createItem(ctx, val, ref); err != nil {
  198. return fmt.Errorf("failed to create item: %w", err)
  199. }
  200. return nil
  201. } else if err != nil {
  202. return fmt.Errorf("failed to find item: %w", err)
  203. }
  204. // TODO: We are only sending info to a specific label on a 1password item.
  205. // We should change this logic eventually to allow pushing whole kubernetes Secrets to 1password as multiple labels
  206. // OOTB.
  207. label := ref.GetProperty()
  208. if label == "" {
  209. label = "password"
  210. }
  211. mdata, err := metadata.ParseMetadataParameters[PushSecretMetadataSpec](ref.GetMetadata())
  212. if err != nil {
  213. return fmt.Errorf("failed to parse push secret metadata: %w", err)
  214. }
  215. if mdata != nil && mdata.Spec.Tags != nil {
  216. providerItem.Tags = mdata.Spec.Tags
  217. }
  218. providerItem.Fields, err = updateFieldValue(providerItem.Fields, label, string(val))
  219. if err != nil {
  220. return fmt.Errorf("failed to update field with value %s: %w", string(val), err)
  221. }
  222. if _, err = p.client.Items().Put(ctx, providerItem); err != nil {
  223. return fmt.Errorf("failed to update item: %w", err)
  224. }
  225. return nil
  226. }
  227. func (p *Provider) GetVault(ctx context.Context, name string) (string, error) {
  228. vaults, err := p.client.VaultsAPI.List(ctx)
  229. if err != nil {
  230. return "", fmt.Errorf("failed to list vaults: %w", err)
  231. }
  232. for _, v := range vaults {
  233. if v.Title == name {
  234. // cache the ID so we don't have to repeat this lookup.
  235. p.vaultID = v.ID
  236. return v.ID, nil
  237. }
  238. }
  239. return "", fmt.Errorf("vault %s not found", name)
  240. }
  241. func (p *Provider) findItem(ctx context.Context, name string) (onepassword.Item, error) {
  242. if strfmt.IsUUID(name) {
  243. return p.client.Items().Get(ctx, p.vaultID, name)
  244. }
  245. items, err := p.client.Items().List(ctx, p.vaultID)
  246. if err != nil {
  247. return onepassword.Item{}, fmt.Errorf("failed to list items: %w", err)
  248. }
  249. // We don't stop
  250. var itemUUID string
  251. for _, v := range items {
  252. if v.Title == name {
  253. if itemUUID != "" {
  254. return onepassword.Item{}, fmt.Errorf("found multiple items with name %s", name)
  255. }
  256. itemUUID = v.ID
  257. }
  258. }
  259. if itemUUID == "" {
  260. return onepassword.Item{}, ErrKeyNotFound
  261. }
  262. return p.client.Items().Get(ctx, p.vaultID, itemUUID)
  263. }
  264. // SecretExists Not Implemented.
  265. func (p *Provider) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
  266. return false, fmt.Errorf("not implemented")
  267. }
  268. // Validate checks if the client is configured correctly
  269. // currently only checks if it is possible to list vaults.
  270. func (p *Provider) Validate() (esv1.ValidationResult, error) {
  271. _, err := p.client.Vaults().List(context.Background())
  272. if err != nil {
  273. return esv1.ValidationResultError, fmt.Errorf("error listing vaults: %w", err)
  274. }
  275. return esv1.ValidationResultReady, nil
  276. }
  277. func (p *Provider) constructRefKey(key string) string {
  278. // remove any possible leading slashes because the vaultPrefix already contains it.
  279. return p.vaultPrefix + strings.TrimPrefix(key, "/")
  280. }