Browse Source

Add 1Password support

david amick 4 years ago
parent
commit
435aefc7ac

+ 40 - 0
apis/externalsecrets/v1beta1/secretstore_onepassword_types.go

@@ -0,0 +1,40 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1beta1
+
+import (
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+// OnePasswordAuth contains a secretRef for credentials.
+type OnePasswordAuth struct {
+	SecretRef *OnePasswordAuthSecretRef `json:"secretRef"`
+}
+
+// OnePasswordAuthSecretRef holds secret references for 1Password credentials.
+type OnePasswordAuthSecretRef struct {
+	// The ConnectToken is used for authentication to a 1Password Connect Server.
+	ConnectToken esmeta.SecretKeySelector `json:"connectTokenSecretRef"`
+}
+
+// OnePasswordProvider configures a store to sync secrets using the 1Password Secret Manager provider.
+type OnePasswordProvider struct {
+	// Auth defines the information necessary to authenticate against OnePassword Connect Server
+	Auth *OnePasswordAuth `json:"auth"`
+	// ConnectHost defines the OnePassword Connect Server to connect to
+	ConnectHost string `json:"connectHost"`
+	// Vaults defines which OnePassword vaults to search in which order
+	Vaults map[string]int `json:"vaults"`
+}

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -82,6 +82,10 @@ type SecretStoreProvider struct {
 	// +optional
 	Alibaba *AlibabaProvider `json:"alibaba,omitempty"`
 
+	// OnePassword configures this store to sync secrets using the 1Password Cloud provider
+	// +optional
+	OnePassword *OnePasswordProvider `json:"onepassword,omitempty"`
+
 	// Webhook configures this store to sync secrets using a generic templated webhook
 	// +optional
 	Webhook *WebhookProvider `json:"webhook,omitempty"`

+ 68 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1172,6 +1172,69 @@ func (in *NoSecretError) DeepCopy() *NoSecretError {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OnePasswordAuth) DeepCopyInto(out *OnePasswordAuth) {
+	*out = *in
+	if in.SecretRef != nil {
+		in, out := &in.SecretRef, &out.SecretRef
+		*out = new(OnePasswordAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordAuth.
+func (in *OnePasswordAuth) DeepCopy() *OnePasswordAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(OnePasswordAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OnePasswordAuthSecretRef) DeepCopyInto(out *OnePasswordAuthSecretRef) {
+	*out = *in
+	in.ConnectToken.DeepCopyInto(&out.ConnectToken)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordAuthSecretRef.
+func (in *OnePasswordAuthSecretRef) DeepCopy() *OnePasswordAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(OnePasswordAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OnePasswordProvider) DeepCopyInto(out *OnePasswordProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(OnePasswordAuth)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Vaults != nil {
+		in, out := &in.Vaults, &out.Vaults
+		*out = make(map[string]int, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnePasswordProvider.
+func (in *OnePasswordProvider) DeepCopy() *OnePasswordProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OnePasswordProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OracleAuth) DeepCopyInto(out *OracleAuth) {
 	*out = *in
 	in.SecretRef.DeepCopyInto(&out.SecretRef)
@@ -1336,6 +1399,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(AlibabaProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.OnePassword != nil {
+		in, out := &in.OnePassword, &out.OnePassword
+		*out = new(OnePasswordProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Webhook != nil {
 		in, out := &in.Webhook, &out.Webhook
 		*out = new(WebhookProvider)

+ 54 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2083,6 +2083,60 @@ spec:
                     required:
                     - auth
                     type: object
+                  onepassword:
+                    description: OnePassword configures this store to sync secrets
+                      using the 1Password Cloud provider
+                    properties:
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against OnePassword Connect Server
+                        properties:
+                          secretRef:
+                            description: OnePasswordAuthSecretRef holds secret references
+                              for 1Password credentials.
+                            properties:
+                              connectTokenSecretRef:
+                                description: The ConnectToken is used for authentication
+                                  to a 1Password Connect Server.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            required:
+                            - connectTokenSecretRef
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      connectHost:
+                        description: ConnectHost defines the OnePassword Connect Server
+                          to connect to
+                        type: string
+                      vaults:
+                        additionalProperties:
+                          type: integer
+                        description: Vaults defines which OnePassword vaults to search
+                          in which order
+                        type: object
+                    required:
+                    - auth
+                    - connectHost
+                    - vaults
+                    type: object
                   oracle:
                     description: Oracle configures this store to sync secrets using
                       Oracle Vault provider

+ 54 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2086,6 +2086,60 @@ spec:
                     required:
                     - auth
                     type: object
+                  onepassword:
+                    description: OnePassword configures this store to sync secrets
+                      using the 1Password Cloud provider
+                    properties:
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against OnePassword Connect Server
+                        properties:
+                          secretRef:
+                            description: OnePasswordAuthSecretRef holds secret references
+                              for 1Password credentials.
+                            properties:
+                              connectTokenSecretRef:
+                                description: The ConnectToken is used for authentication
+                                  to a 1Password Connect Server.
+                                properties:
+                                  key:
+                                    description: The key of the entry in the Secret
+                                      resource's `data` field to be used. Some instances
+                                      of this field may be defaulted, in others it
+                                      may be required.
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    type: string
+                                  namespace:
+                                    description: Namespace of the resource being referred
+                                      to. Ignored if referent is not cluster-scoped.
+                                      cluster-scoped defaults to the namespace of
+                                      the referent.
+                                    type: string
+                                type: object
+                            required:
+                            - connectTokenSecretRef
+                            type: object
+                        required:
+                        - secretRef
+                        type: object
+                      connectHost:
+                        description: ConnectHost defines the OnePassword Connect Server
+                          to connect to
+                        type: string
+                      vaults:
+                        additionalProperties:
+                          type: integer
+                        description: Vaults defines which OnePassword vaults to search
+                          in which order
+                        type: object
+                    required:
+                    - auth
+                    - connectHost
+                    - vaults
+                    type: object
                   oracle:
                     description: Oracle configures this store to sync secrets using
                       Oracle Vault provider

+ 82 - 0
deploy/crds/bundle.yaml

@@ -1879,6 +1879,47 @@ spec:
                       required:
                         - auth
                       type: object
+                    onepassword:
+                      description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
+                      properties:
+                        auth:
+                          description: Auth defines the information necessary to authenticate against OnePassword Connect Server
+                          properties:
+                            secretRef:
+                              description: OnePasswordAuthSecretRef holds secret references for 1Password credentials.
+                              properties:
+                                connectTokenSecretRef:
+                                  description: The ConnectToken is used for authentication to a 1Password Connect Server.
+                                  properties:
+                                    key:
+                                      description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  type: object
+                              required:
+                                - connectTokenSecretRef
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        connectHost:
+                          description: ConnectHost defines the OnePassword Connect Server to connect to
+                          type: string
+                        vaults:
+                          additionalProperties:
+                            type: integer
+                          description: Vaults defines which OnePassword vaults to search in which order
+                          type: object
+                      required:
+                        - auth
+                        - connectHost
+                        - vaults
+                      type: object
                     oracle:
                       description: Oracle configures this store to sync secrets using Oracle Vault provider
                       properties:
@@ -4473,6 +4514,47 @@ spec:
                       required:
                         - auth
                       type: object
+                    onepassword:
+                      description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
+                      properties:
+                        auth:
+                          description: Auth defines the information necessary to authenticate against OnePassword Connect Server
+                          properties:
+                            secretRef:
+                              description: OnePasswordAuthSecretRef holds secret references for 1Password credentials.
+                              properties:
+                                connectTokenSecretRef:
+                                  description: The ConnectToken is used for authentication to a 1Password Connect Server.
+                                  properties:
+                                    key:
+                                      description: The key of the entry in the Secret resource's `data` field to be used. Some instances of this field may be defaulted, in others it may be required.
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      type: string
+                                    namespace:
+                                      description: Namespace of the resource being referred to. Ignored if referent is not cluster-scoped. cluster-scoped defaults to the namespace of the referent.
+                                      type: string
+                                  type: object
+                              required:
+                                - connectTokenSecretRef
+                              type: object
+                          required:
+                            - secretRef
+                          type: object
+                        connectHost:
+                          description: ConnectHost defines the OnePassword Connect Server to connect to
+                          type: string
+                        vaults:
+                          additionalProperties:
+                            type: integer
+                          description: Vaults defines which OnePassword vaults to search in which order
+                          type: object
+                      required:
+                        - auth
+                        - connectHost
+                        - vaults
+                      type: object
                     oracle:
                       description: Oracle configures this store to sync secrets using Oracle Vault provider
                       properties:

BIN
docs/pictures/screenshot_1password_create_document.png


BIN
docs/pictures/screenshot_1password_create_password.png


BIN
docs/pictures/screenshot_1password_password_field.png


+ 173 - 0
docs/provider-1password-automation.md

@@ -0,0 +1,173 @@
+## 1Password Secrets Automation
+
+External Secrets Operator integrates with [1Password Secrets Automation](https://1password.com/products/secrets/) for secret management.
+
+### Important note about this documentation
+_**The 1Password API calls the entries in vaults 'Items'. These docs use the same term.**_
+
+### Behavior
+* How an Item is equated to an ExternalSecret:
+    * `remoteRef.key` is equated to an Item's Title
+    * `remoteRef.property` is equated to:
+        * An Item's field's Label (Password type)
+        * An Item's file's Name (Document type)
+        * If empty, defaults to the first file name, or the field labeled `password`
+    * `remoteRef.version` is currently not supported.
+    * One Item in a vault can equate to one Kubernetes Secret to keep things easy to comprehend.
+* Support for 1Password secret types of `Password` and `Document`.
+    * The `Password` type can get data from multiple `fields` in the Item.
+    * The `Document` type can get data from files.
+    * See [creating 1Password Items compatible with ExternalSecrets](#creating-compatible-1password-items).
+* Ordered vaults
+    * Specify an ordered list of vaults in a SecretStore and the value will be sourced from the first vault with a matching Item.
+    * If no matching Item is found, an error is returned.
+    * This supports having a default or shared set of values that can also be overriden for specific environments.
+* `dataFrom`:
+    * `find.path` is equated to Item Title.
+    * `find.name.regexp` is equated to field Labels.
+    * `find.tags` are not supported at this time.
+
+### Prerequisites
+* 1Password requires running a 1Password Connect Server to which the API requests will be made.
+    * External Secrets does not run this server. See [Deploy a Connect Server](#deploy-a-connect-server).
+    * One Connect Server is needed per 1Password Automation Environment.
+    * Many Vaults can be added to an Automation Environment, and Tokens can be generated in that Environment with access to any set or subset of those Vaults.
+* 1Password Connect Server version 1.3.0 or higher. 1.3.0 and 1.5.0 have been tested.
+
+### Setup Authentication
+_Authentication requires a `1password-credentials.json` file provided to the Connect Server, and a related 'Access Token' for the client in this provider to authenticate to that Connect Server. Both of these are generated by 1Password._
+
+1. Setup an Automation Environment [at 1Password.com](https://support.1password.com/secrets-automation/), or [via the op CLI](https://github.com/1Password/connect/blob/a0a5f3d92e68497098d9314721335a7bb68a3b2d/README.md#create-server-and-access-token).
+    * Note: don't be confused by the `op connect server create` syntax. This will create an Automation Environment in 1Password, and corresponding credentials for a Connect Server, nothing more.
+    * This will result in a `1password-credentials.json` file to provide to a Connect Server Deployment, and an Access Token to provide as a Secret referenced by a `SecretStore` or `ClusterSecretStore`.
+1. Create a Kubernetes secret with the Access Token
+```yaml
+{% include '1password-token-secret.yaml' %}
+```
+1. Reference the secret in a SecretStore or ClusterSecretStore
+```yaml
+{% include '1password-secret-store.yaml' %}
+```
+1. Create a Kubernetes secret with the Connect Server credentials
+```yaml
+{% include '1password-connect-server-secret.yaml' %}
+```
+1. Reference the secret in a Connect Server Deployment
+```yaml
+{% include '1password-connect-server-deployment.yaml' %}
+```
+
+### Deploy a Connect Server
+* Follow the remaining instructions in the [Quick Start guide](https://github.com/1Password/connect/blob/a0a5f3d92e68497098d9314721335a7bb68a3b2d/README.md#quick-start).
+    * Deploy at minimum a Deployment and Service for a Connect Server, to go along with the Secret for the Server created in the [Setup Authentication section](#setup-authentication).
+* The Service's name will be referenced in SecretStores/ClusterSecretStores.
+* Keep in mind the likely need for additional Connect Servers for other Automation Environments when naming objects. For example dev, staging, prod, etc.
+* Unencrypted secret values are passed over the connection between the Operator and the Connect Server. **Encrypting the connection is recommended.**
+
+### Creating Compatible 1Password Items
+_Also see [examples below](#examples) for matching SecretStore and ExternalSecret specs._
+#### Manually (Password type)
+1. Click the plus button to create a new Password type Item.
+1. Change the title to what you want `remoteRef.key` to be.
+1. Set what you want `remoteRef.property` to be in the field sections where is says 'label', and values where it says 'new field'.
+1. Click the 'Save' button.
+
+![create-password-screenshot](./pictures/screenshot_1password_create_password.png)
+#### Manually (Document type)
+* Click the plus button to create a new Document type Item.
+* Choose the file to upload and upload it.
+* Change the title to match `remoteRef.key`
+* Click the 'Add New File' button to add more files.
+* Click the 'Save' button.
+
+![create-document-screenshot](./pictures/screenshot_1password_create_document.png)
+#### Scripting (Password type with op [CLI](https://developer.1password.com/docs/cli/v1/get-started/))
+* Create `file.json` with the following contents, swapping in your keys and values. Note: `section.name`'s and `section.title`'s values are ignored by the Operator, but cannot be empty for the `op` CLI
+    ```json
+    {
+      "sections": [
+        {
+          "fields": [
+            {
+              "k": "concealed",
+              "n": "MY_ENV_VAR1",
+              "t": "MY_ENV_VAR1",
+              "v": "value1"
+            },
+            {
+              "k": "concealed",
+              "n": "MY_ENV_VAR2",
+              "t": "MY_ENV_VAR2",
+              "v": "value2"
+            }
+          ],
+          "name": "EXTERNAL-SECRETS",
+          "title": "EXTERNAL-SECRETS"
+        }
+      ]
+    }
+    ```
+* Run `op create item password --template file.json --vault my-vault --title my-item`
+#### Scripting (Document type)
+* Unfortunately the `op` CLI doesn't seem to support uploading multiple files to the same Item, and the current Go lib has a [bug](https://github.com/1Password/connect-sdk-go/issues/45). `op` can be used to create a Document type Item with one file in it, but for now it's necessary to add multiple files to the same Document via the GUI.
+
+#### In-built field labeled `password` on Password type Items
+* TL;DR if you need a field labeled `password`, use the in-built one rather than the one in a fields Section.
+
+![password-field-example](./pictures/screenshot_1password_password_field.png)
+
+* 1Password automatically adds a field labeled `password` on every Password type Item, whether it's created through a GUI or the API or `op` CLI.
+* There's no problem with using this field just like any other field, _just make sure you don't end up with two fields with the same label_. (For example, by automating the `op` CLI to create Items.)
+* The in-built `password` field is not otherwise special for the purposes of ExternalSecrets. It can be ignored when not in use.
+
+### Examples
+Examples of using the `my-env-config` and `my-cert` Items [seen above](#manually-password-type).
+
+* Note: with this configuration a 1Password Item titled `my-env-config` is correlated to a ExternalSecret named `my-env-config` that results in a Kubernetes secret named `my-env-config`, all with matching names for the key/value pairs. This is a way to increase comprehensibility.
+```yaml
+{% include '1password-secret-store.yaml' %}
+```
+```yaml
+{% include '1password-external-secret-my-env-config.yaml' %}
+```
+```yaml
+{% include '1password-external-secret-my-cert.yaml' %}
+```
+
+### Additional Notes
+#### General
+* It's intuative to use Document type Items for Kubernetes secrets mounted as files, and Password type Items for ones that will be mounted as environment variables, but either can be used for either. It comes down to what's more convenient.
+
+#### Why no version history
+* 1Password only supports version history on their in-built `password` field. Therefore, implementing version history in this provider would require one Item in 1Password per `remoteRef` in an ExternalSecret. Additionally `remoteRef.property` would be pointless/unusable.
+* For example, a Kubernetes secret with 15 keys (say, used in `envFrom`,) would require 15 Items in the 1Password vault, instead of 15 Fields in 1 Item. This would quickly get untenable for more than a few secrets, because:
+    * All Items would have to have unique names which means `secretKey` couldn't match the Item name the `remoteRef` is targeting.
+    * Maintenance, particularly clean up of no longer used secrets, would be significantly more work.
+    * A vault would often become a huge list of unorganized entries as opposed to a much smaller list organized by Kubernetes Secret.
+* To support new and old versions of a secret value at the same time, create a new Item in 1Password with the new value, and point some ExternalSecrets at a time to the new Item.
+
+#### Keeping misconfiguration from working
+* One instance of the ExternalSecrets Operator _can_ work with many Connect Server instances, but it may not be the best approach.
+* With one Operator instance per Connect Server instance, namespaces and RBAC can be used to improve security posture, and perhaps just as importantly, it's harder to misconfigure something and have it work (supply env A's secret values to env B for example.)
+* You can run as many 1Password Connect Servers as you need security boundaries to help protect against accidental misconfiguration.
+
+#### Patching ExternalSecrets with Kustomize
+* An overlay can provide a SecretStore specific to that overlay, and then use JSON6902 to patch all the ExternalSecrets coming from base to point to that SecretStore. Here's an example `overlays/staging/kustomization.yaml`:
+    ```yaml
+    ---
+    apiVersion: kustomize.config.k8s.io/v1beta1
+    kind: Kustomization
+
+    resources:
+    - ../../base/something-with-external-secrets
+    - secretStore.staging.yaml
+
+    patchesJson6902:
+    - target:
+        kind: ExternalSecret
+        name: ".*"
+      patch: |-
+        - op: replace
+          path: /spec/secretStoreRef/name
+          value: staging
+    ```

+ 28 - 0
docs/snippets/1password-connect-server-deployment.yaml

@@ -0,0 +1,28 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: onepassword-connect-staging
+spec:
+  template:
+    spec:
+      containers:
+      - name: connect-api
+        image: 1password/connect-api:1.5.0
+        env:
+        - name: OP_SESSION
+          valueFrom:
+            secretKeyRef:
+              name: connect-server-credentials
+              key: 1password-credentials.json
+        ...
+      - name: connect-sync
+        image: 1password/connect-sync:1.5.0
+        env:
+        - name: OP_SESSION
+          valueFrom:
+            secretKeyRef:
+              name: connect-server-credentials
+              key: 1password-credentials.json
+        ...
+      ...

+ 10 - 0
docs/snippets/1password-connect-server-secret.yaml

@@ -0,0 +1,10 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: connect-server-credentials
+type: Opaque
+stringData:
+  # NOTE: This secret value must be base64 encoded after it becomes the OP_SESSION env var in the Connect Server Deployment, that means double base64 encoded here. (Or single w/ stringData.)
+  1password-credentials.json: |-
+    eyJ2ZXJpZmllciI6eyJzYWx0IjoiZXhhbXBsZSIsImxvY2FsSGFzaCI6ImV4YW1wbGUifSwiZW5jQ3JlZGVudGlhbHMiOnsia2lkIjoiZXhhbXBsZSIsImVuYyI6ImV4YW1wbGUiLCJjdHkiOiJleGFtcGxlIiwiaXYiOiJleGFtcGxlIiwiZGF0YSI6ImV4YW1wbGUifSwidmVyc2lvbiI6IjIiLCJkZXZpY2VVdWlkIjoiZXhhbXBsZSIsInVuaXF1ZUtleSI6eyJhbGciOiJleGFtcGxlIiwiZXh0Ijp0cnVlLCJrIjoiZXhhbXBsZSIsImtleV9vcHMiOlsiZW5jcnlwdCIsImRlY3J5cHQiXSwia3R5Ijoib2N0Iiwia2lkIjoiZXhhbXBsZSJ9fQ==

+ 30 - 0
docs/snippets/1password-external-secret-my-cert.yaml

@@ -0,0 +1,30 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: my-cert
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: staging
+  target:
+    creationPolicy: Owner
+  data:
+  - secretKey: cert.crt
+    remoteRef:
+      key: my-cert
+      property: cert.crt
+  - secretKey: cert.key
+    remoteRef:
+      key: my-cert
+      property: cert.key
+  # OR
+  dataFrom:
+  - extract:
+      key: my-cert
+      property: cert.key  # optional field Label to match exactly
+  # OR
+  - find:
+      path: my-cert  # optional Item Title to match exactly
+      name:
+        regexp: "^cert.*"

+ 30 - 0
docs/snippets/1password-external-secret-my-env-config.yaml

@@ -0,0 +1,30 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: my-env-config
+spec:
+  secretStoreRef:
+    kind: SecretStore
+    name: staging
+  target:
+    creationPolicy: Owner
+  data:
+  - secretKey: MY_ENV_VAR1
+    remoteRef:
+      key: my-env-config
+      property: MY_ENV_VAR1
+  - secretKey: MY_ENV_VAR2
+    remoteRef:
+      key: my-env-config
+      property: MY_ENV_VAR2
+  # OR
+  dataFrom:
+  - extract:
+      key: my-env-config
+      property: MY_ENV_VAR1  # optional field Label to match exactly
+  # OR
+  - find:
+      path: my-env-config  # optional Item Title to match exactly
+      name:
+        regexp: "^MY_ENV_VAR.*"

+ 17 - 0
docs/snippets/1password-secret-store.yaml

@@ -0,0 +1,17 @@
+---
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: staging
+spec:
+  provider:
+    onepassword:
+      connectHost: https://onepassword-connect-staging
+      vaults:
+        staging: 1  # look in this vault first
+        shared: 2   # next look in here. error if not found
+      auth:
+        secretRef:
+        connectTokenSecretRef:
+          name: onepassword-connect-token-staging
+          key: token

+ 8 - 0
docs/snippets/1password-token-secret.yaml

@@ -0,0 +1,8 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: onepassword-connect-token-staging
+type: Opaque
+stringData:
+  token: my-token

+ 1 - 0
docs/stability-support.md

@@ -27,6 +27,7 @@ The following table describes the stability level of each provider and who's res
 | Alibaba Cloud KMS                                                                                          |   alpha   |                                                                                                                            [@ElsaChelala](https://github.com/ElsaChelala) |
 | [Oracle Vault](https://external-secrets.io/latest/provider-oracle-vault)                                   |   alpha   |                                                                                   [@KianTigger](https://github.com/KianTigger) [@EladGabay](https://github.com/EladGabay) |
 | [Akeyless](https://external-secrets.io/latest/provider-akeyless)                                           |   alpha   |                                                                                                                      [@renanaAkeyless](https://github.com/renanaAkeyless) |
+| [1Password](https://external-secrets.io/latest/provider-1password-automation)                              |   alpha   |                                                                         [@SimSpaceCorp](https://github.com/Simspace) [@snarlysodboxer](https://github.com/snarlysodboxer) |
 | [Generic Webhook](https://external-secrets.io/latest/provider-webhook)                                     |   alpha   |                                                                                                                                    [@willemm](https://github.com/willemm) |
 | [senhasegura DevOps Secrets Management (DSM)](https://external-secrets.io/latest/provider-senhasegura-dsm) |   alpha   |                                                                                                                                      [@lfraga](https://github.com/lfraga) |
 | [Kubernetes](https://external-secrets.io/latest/provider-kubernetes) |   alpha   |                                                                                                                                      [@rodrmartinez](https://github.com/rodrmartinez) |

+ 5 - 0
go.mod

@@ -93,6 +93,8 @@ require (
 	software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78
 )
 
+require github.com/1Password/connect-sdk-go v1.2.0
+
 require (
 	cloud.google.com/go/compute v1.6.0 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
@@ -177,6 +179,7 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/oklog/run v1.1.0 // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
+	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/pierrec/lz4 v2.6.1+incompatible // indirect
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
@@ -191,6 +194,8 @@ require (
 	github.com/stretchr/objx v0.2.0 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.0 // indirect
+	github.com/uber/jaeger-client-go v2.25.0+incompatible // indirect
+	github.com/uber/jaeger-lib v2.4.0+incompatible // indirect
 	go.mongodb.org/mongo-driver v1.7.5 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect

+ 10 - 0
go.sum

@@ -58,6 +58,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/Azure/azure-sdk-for-go v63.4.0+incompatible h1:fle3M5Q7vr8auaiPffKyUQmLbvYeqpw30bKU6PrWJFo=
 github.com/Azure/azure-sdk-for-go v63.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/1Password/connect-sdk-go v1.2.0 h1:WbIvmbDUpA89nyH0l3LF2iRSFJAv86d2D7IjVNjw6iw=
+github.com/1Password/connect-sdk-go v1.2.0/go.mod h1:qK2bF/GweAq812xj+HGfbauaE6cKX1MXfKhpAvoHEq8=
 github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
@@ -95,6 +97,8 @@ github.com/IBM/go-sdk-core/v5 v5.9.5 h1:+uMyHpOyBlFFd/I0PB+7JqqXOPY2DzRR0tbBjTc4
 github.com/IBM/go-sdk-core/v5 v5.9.5/go.mod h1:YlOwV9LeuclmT/qi/LAK2AsobbAP42veV0j68/rlZsE=
 github.com/IBM/secrets-manager-go-sdk v1.0.44 h1:nCxEAjC2g2BaQSRNMqLQMx1G+bKimHxAK7MRY4kslCg=
 github.com/IBM/secrets-manager-go-sdk v1.0.44/go.mod h1:KxVv6JWYWJ22l9X0GOMacaJXB/Q/8T1f7sWjHh5iwSg=
+github.com/HdrHistogram/hdrhistogram-go v1.0.1 h1:GX8GAYDuhlFQnI2fRDHQhTlkHMz8bEn0jTI6LJU0mpw=
+github.com/HdrHistogram/hdrhistogram-go v1.0.1/go.mod h1:BWJ+nMSHY3L41Zj7CA3uXnloDp7xxV0YvstAE7nKTaM=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
@@ -677,6 +681,8 @@ github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5h
 github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
 github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
 github.com/oracle/oci-go-sdk/v56 v56.1.0 h1:HOr9P+MkwgrilEGTJCU7a6GMFrUG/RZAzvh/2JeRXvI=
 github.com/oracle/oci-go-sdk/v56 v56.1.0/go.mod h1:kDJAL3HEAF+4oQR8GfaOkY6rz2kU3/kZ6vYJnJXSCkA=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -801,6 +807,10 @@ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/uber/jaeger-client-go v2.25.0+incompatible h1:IxcNZ7WRY1Y3G4poYlx24szfsn/3LvK9QHCq9oQw8+U=
+github.com/uber/jaeger-client-go v2.25.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
+github.com/uber/jaeger-lib v2.4.0+incompatible h1:fY7QsGQWiCt8pajv4r7JEvmATdCVaWxXbjwyYwsNaLQ=
+github.com/uber/jaeger-lib v2.4.0+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
 github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
 github.com/xanzy/go-gitlab v0.64.0 h1:rMgQdW9S1w3qvNAH2LYpFd2xh7KNLk+JWJd7sorNuTc=

+ 2 - 0
hack/api-docs/mkdocs.yml

@@ -65,6 +65,8 @@ nav:
       - Gitlab Project Variables: provider-gitlab-project-variables.md
     - Oracle:
       - Oracle Vault: provider-oracle-vault.md
+    - 1Password:
+      - Secrets Automation: provider-1password-automation.md
     - Webhook: provider-webhook.md
     - Fake: provider-fake.md
     - Kubernetes: provider-kubernetes.md

+ 186 - 0
pkg/provider/onepassword/fake/fake.go

@@ -0,0 +1,186 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package fake
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/1Password/connect-sdk-go/onepassword"
+)
+
+// OnePasswordMockClient is a fake connect.Client.
+type OnePasswordMockClient struct {
+	MockVaults       map[string][]onepassword.Vault
+	MockItems        map[string][]onepassword.Item // ID and Title only
+	MockItemFields   map[string]map[string][]*onepassword.ItemField
+	MockFileContents map[string][]byte
+}
+
+// NewMockClient returns an instantiated mock client.
+func NewMockClient() *OnePasswordMockClient {
+	return &OnePasswordMockClient{
+		MockVaults:       map[string][]onepassword.Vault{},
+		MockItems:        map[string][]onepassword.Item{},
+		MockItemFields:   map[string]map[string][]*onepassword.ItemField{},
+		MockFileContents: map[string][]byte{},
+	}
+}
+
+// GetVaultsByTitle returns a list of vaults, you must preload.
+func (mockClient *OnePasswordMockClient) GetVaultsByTitle(uuid string) ([]onepassword.Vault, error) {
+	return mockClient.MockVaults[uuid], nil
+}
+
+// GetItemsByTitle returns a list of items, you must preload.
+func (mockClient *OnePasswordMockClient) GetItemsByTitle(itemUUID, vaultUUID string) ([]onepassword.Item, error) {
+	items := []onepassword.Item{}
+	for _, item := range mockClient.MockItems[vaultUUID] {
+		if item.Title == itemUUID {
+			items = append(items, item)
+		}
+	}
+
+	return items, nil
+}
+
+// GetItem returns a *onepassword.Item, you must preload.
+func (mockClient *OnePasswordMockClient) GetItem(itemUUID, vaultUUID string) (*onepassword.Item, error) {
+	for _, item := range mockClient.MockItems[vaultUUID] {
+		if item.ID == itemUUID {
+			// load the fields that GetItemsByTitle does not
+			item.Fields = mockClient.MockItemFields[vaultUUID][itemUUID]
+
+			return &item, nil
+		}
+	}
+
+	return &onepassword.Item{}, errors.New("status 400: Invalid Item UUID")
+}
+
+// GetItems returns []onepassword.Item, you must preload.
+func (mockClient *OnePasswordMockClient) GetItems(vaultUUID string) ([]onepassword.Item, error) {
+	return mockClient.MockItems[vaultUUID], nil
+}
+
+// GetFileContent returns file data, you must preload.
+func (mockClient *OnePasswordMockClient) GetFileContent(file *onepassword.File) ([]byte, error) {
+	value, ok := mockClient.MockFileContents[file.Name]
+	if !ok {
+		return []byte{}, errors.New("status 400: Invalid File Name")
+	}
+
+	return value, nil
+}
+
+// GetVaults fake.
+func (mockClient *OnePasswordMockClient) GetVaults() ([]onepassword.Vault, error) {
+	return []onepassword.Vault{}, nil
+}
+
+// GetVault fake.
+func (mockClient *OnePasswordMockClient) GetVault(uuid string) (*onepassword.Vault, error) {
+	return &onepassword.Vault{}, nil
+}
+
+// GetItemByTitle fake.
+func (mockClient *OnePasswordMockClient) GetItemByTitle(title, vaultUUID string) (*onepassword.Item, error) {
+	return &onepassword.Item{}, nil
+}
+
+// CreateItem fake.
+func (mockClient *OnePasswordMockClient) CreateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
+	return &onepassword.Item{}, nil
+}
+
+// UpdateItem fake.
+func (mockClient *OnePasswordMockClient) UpdateItem(item *onepassword.Item, vaultUUID string) (*onepassword.Item, error) {
+	return &onepassword.Item{}, nil
+}
+
+// DeleteItem fake.
+func (mockClient *OnePasswordMockClient) DeleteItem(item *onepassword.Item, vaultUUID string) error {
+	return nil
+}
+
+// GetFile fake.
+func (mockClient *OnePasswordMockClient) GetFile(fileUUID, itemUUID, vaultUUID string) (*onepassword.File, error) {
+	return &onepassword.File{}, nil
+}
+
+// // For rigging test cases
+
+// AddPredictableVault adds vaults to the mock client in a predictable way.
+func (mockClient *OnePasswordMockClient) AddPredictableVault(name string) *OnePasswordMockClient {
+	mockClient.MockVaults[name] = append(mockClient.MockVaults[name], onepassword.Vault{
+		ID:   fmt.Sprintf("%s-id", name),
+		Name: name,
+	})
+
+	return mockClient
+}
+
+// AddPredictableItemWithField adds an item and it's fields to the mock client in a predictable way.
+func (mockClient *OnePasswordMockClient) AddPredictableItemWithField(vaultName, title, label, value string) *OnePasswordMockClient {
+	itemID := fmt.Sprintf("%s-id", title)
+	vaultID := fmt.Sprintf("%s-id", vaultName)
+
+	mockClient.MockItems[vaultID] = append(mockClient.MockItems[vaultID], onepassword.Item{
+		ID:    itemID,
+		Title: title,
+		Vault: onepassword.ItemVault{ID: vaultID},
+	})
+
+	if mockClient.MockItemFields[vaultID] == nil {
+		mockClient.MockItemFields[vaultID] = make(map[string][]*onepassword.ItemField)
+	}
+	mockClient.MockItemFields[vaultID][itemID] = append(mockClient.MockItemFields[vaultID][itemID], &onepassword.ItemField{
+		Label: label,
+		Value: value,
+	})
+
+	return mockClient
+}
+
+// AppendVault appends a onepassword.Vault to the mock client.
+func (mockClient *OnePasswordMockClient) AppendVault(name string, vault onepassword.Vault) *OnePasswordMockClient {
+	mockClient.MockVaults[name] = append(mockClient.MockVaults[name], vault)
+
+	return mockClient
+}
+
+// AppendItem appends a onepassword.Item to the mock client.
+func (mockClient *OnePasswordMockClient) AppendItem(vaultID string, item onepassword.Item) *OnePasswordMockClient {
+	mockClient.MockItems[vaultID] = append(mockClient.MockItems[vaultID], item)
+
+	return mockClient
+}
+
+// AppendItemField appends a onepassword.ItemField to the mock client.
+func (mockClient *OnePasswordMockClient) AppendItemField(vaultID, itemID string, itemField onepassword.ItemField) *OnePasswordMockClient {
+	if mockClient.MockItemFields[vaultID] == nil {
+		mockClient.MockItemFields[vaultID] = make(map[string][]*onepassword.ItemField)
+	}
+	mockClient.MockItemFields[vaultID][itemID] = append(mockClient.MockItemFields[vaultID][itemID], &itemField)
+
+	return mockClient
+}
+
+// SetFileContents adds file contents to the mock client.
+func (mockClient *OnePasswordMockClient) SetFileContents(name string, contents []byte) *OnePasswordMockClient {
+	// no need to test or mock same file names in different vaults, because we only GetFileContent after findItem, which already tests getting the right item from the right vault
+	mockClient.MockFileContents[name] = contents
+
+	return mockClient
+}

+ 471 - 0
pkg/provider/onepassword/onepassword.go

@@ -0,0 +1,471 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+package onepassword
+
+import (
+	"context"
+	"fmt"
+	"net/url"
+	"sort"
+
+	"github.com/1Password/connect-sdk-go/connect"
+	"github.com/1Password/connect-sdk-go/onepassword"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/find"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	userAgent = "external-secrets"
+
+	errOnePasswordStore                           = "received invalid 1Password SecretStore resource: %w"
+	errOnePasswordStoreNilSpec                    = "nil spec"
+	errOnePasswordStoreNilSpecProvider            = "nil spec.provider"
+	errOnePasswordStoreNilSpecProviderOnePassword = "nil spec.provider.onepassword"
+	errOnePasswordStoreMissingRefName             = "missing: spec.provider.onepassword.auth.secretRef.connectTokenSecretRef.name"
+	errOnePasswordStoreMissingRefKey              = "missing: spec.provider.onepassword.auth.secretRef.connectTokenSecretRef.key"
+	errOnePasswordStoreAtLeastOneVault            = "must be at least one vault: spec.provider.onepassword.vaults"
+	errOnePasswordStoreInvalidConnectHost         = "unable to parse URL: spec.provider.onepassword.connectHost: %w"
+	errOnePasswordStoreNonUniqueVaultNumbers      = "vault order numbers must be unique"
+	errFetchK8sSecret                             = "could not fetch ConnectToken Secret: %w"
+	errMissingToken                               = "missing Secret Token"
+	errGetVault                                   = "error finding 1Password Vault: %w"
+	errExpectedOneVault                           = "expected one 1Password Vault matching %w"
+	errExpectedOneItem                            = "expected one 1Password Item matching %w"
+	errGetItem                                    = "error finding 1Password Item: %w"
+	errKeyNotFound                                = "key not found in 1Password Vaults: %w"
+	errDocumentNotFound                           = "error finding 1Password Document: %w"
+	errExpectedOneField                           = "expected one 1Password ItemField matching %w"
+	errTagsNotImplemented                         = "'find.tags' is not implemented in the 1Password provider"
+	errVersionNotImplemented                      = "'remoteRef.version' is not implemented in the 1Password provider"
+
+	documentCategory      = "DOCUMENT"
+	fieldsWithLabelFormat = "'%s' in '%s', got %d"
+	incorrectCountFormat  = "'%s', got %d"
+)
+
+// ProviderOnePassword is a provider for 1Password.
+type ProviderOnePassword struct {
+	vaults map[string]int
+	client connect.Client
+}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &ProviderOnePassword{}
+var _ esv1beta1.Provider = &ProviderOnePassword{}
+
+// NewClient constructs a 1Password Provider.
+func (provider *ProviderOnePassword) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	config := store.GetSpec().Provider.OnePassword
+
+	credentialsSecret := &corev1.Secret{}
+	objectKey := types.NamespacedName{
+		Name:      config.Auth.SecretRef.ConnectToken.Name,
+		Namespace: namespace,
+	}
+
+	// only ClusterSecretStore is allowed to set namespace (and then it's required)
+	if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
+		objectKey.Namespace = *config.Auth.SecretRef.ConnectToken.Namespace
+	}
+
+	err := kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return nil, fmt.Errorf(errFetchK8sSecret, err)
+	}
+	token := credentialsSecret.Data[config.Auth.SecretRef.ConnectToken.Key]
+	if (token == nil) || (len(token) == 0) {
+		return nil, fmt.Errorf(errMissingToken)
+	}
+	provider.client = connect.NewClientWithUserAgent(config.ConnectHost, string(token), userAgent)
+	provider.vaults = config.Vaults
+
+	return provider, nil
+}
+
+// ValidateStore checks if the provided store is valid.
+func (provider *ProviderOnePassword) ValidateStore(store esv1beta1.GenericStore) error {
+	return validateStore(store)
+}
+
+func validateStore(store esv1beta1.GenericStore) error {
+	// check nils
+	storeSpec := store.GetSpec()
+	if storeSpec == nil {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreNilSpec))
+	}
+	if storeSpec.Provider == nil {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreNilSpecProvider))
+	}
+	if storeSpec.Provider.OnePassword == nil {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreNilSpecProviderOnePassword))
+	}
+
+	// check mandatory fields
+	config := storeSpec.Provider.OnePassword
+	if config.Auth.SecretRef.ConnectToken.Name == "" {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreMissingRefName))
+	}
+	if config.Auth.SecretRef.ConnectToken.Key == "" {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreMissingRefKey))
+	}
+
+	// check namespace compared to kind
+	if err := utils.ValidateSecretSelector(store, config.Auth.SecretRef.ConnectToken); err != nil {
+		return fmt.Errorf(errOnePasswordStore, err)
+	}
+
+	// check at least one vault
+	if len(config.Vaults) == 0 {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreAtLeastOneVault))
+	}
+
+	// ensure vault numbers are unique
+	if !hasUniqueVaultNumbers(config.Vaults) {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreNonUniqueVaultNumbers))
+	}
+
+	// check valid URL
+	if _, err := url.Parse(config.ConnectHost); err != nil {
+		return fmt.Errorf(errOnePasswordStore, fmt.Errorf(errOnePasswordStoreInvalidConnectHost, err))
+	}
+
+	return nil
+}
+
+// GetSecret returns a single secret from the provider.
+func (provider *ProviderOnePassword) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if ref.Version != "" {
+		return nil, fmt.Errorf(errVersionNotImplemented)
+	}
+
+	item, err := provider.findItem(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	// handle files
+	if item.Category == documentCategory {
+		// default to the first file when ref.Property is empty
+		return provider.getFile(item, ref.Property)
+	}
+
+	// handle fields
+	return provider.getField(item, ref.Property)
+}
+
+// Validate checks if the client is configured correctly
+// to be able to retrieve secrets from the provider.
+func (provider *ProviderOnePassword) Validate() (esv1beta1.ValidationResult, error) {
+	for vaultName := range provider.vaults {
+		_, err := provider.client.GetItems(vaultName)
+		if err != nil {
+			return esv1beta1.ValidationResultError, err
+		}
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}
+
+// GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.
+func (provider *ProviderOnePassword) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	if ref.Version != "" {
+		return nil, fmt.Errorf(errVersionNotImplemented)
+	}
+
+	item, err := provider.findItem(ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	// handle files
+	if item.Category == documentCategory {
+		return provider.getFiles(item, ref.Property)
+	}
+
+	// handle fields
+	return provider.getFields(item, ref.Property)
+}
+
+// GetAllSecrets syncs multiple 1Password Items into a single Kubernetes Secret, for dataFrom.find.
+func (provider *ProviderOnePassword) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	if ref.Tags != nil {
+		return nil, fmt.Errorf(errTagsNotImplemented)
+	}
+
+	secretData := make(map[string][]byte)
+	sortedVaults := sortVaults(provider.vaults)
+	for _, vaultName := range sortedVaults {
+		vaults, err := provider.client.GetVaultsByTitle(vaultName)
+		if err != nil {
+			return nil, fmt.Errorf(errGetVault, err)
+		}
+		if len(vaults) != 1 {
+			return nil, fmt.Errorf(errExpectedOneVault, fmt.Errorf(incorrectCountFormat, vaultName, len(vaults)))
+		}
+
+		err = provider.getAllForVault(vaults[0].ID, ref, secretData)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return secretData, nil
+}
+
+// Close closes the client connection.
+func (provider *ProviderOnePassword) Close(ctx context.Context) error {
+	return nil
+}
+
+func (provider *ProviderOnePassword) findItem(name string) (*onepassword.Item, error) {
+	sortedVaults := sortVaults(provider.vaults)
+	for _, vaultName := range sortedVaults {
+		vaults, err := provider.client.GetVaultsByTitle(vaultName)
+		if err != nil {
+			return nil, fmt.Errorf(errGetVault, err)
+		}
+		if len(vaults) != 1 {
+			return nil, fmt.Errorf(errExpectedOneVault, fmt.Errorf(incorrectCountFormat, vaultName, len(vaults)))
+		}
+
+		// use GetItemsByTitle instead of GetItemByTitle in order to handle length cases
+		items, err := provider.client.GetItemsByTitle(name, vaults[0].ID)
+		if err != nil {
+			return nil, fmt.Errorf(errGetItem, err)
+		}
+		switch {
+		case len(items) == 1:
+			return provider.client.GetItem(items[0].ID, items[0].Vault.ID)
+		case len(items) > 1:
+			return nil, fmt.Errorf(errExpectedOneItem, fmt.Errorf(incorrectCountFormat, name, len(items)))
+		}
+	}
+
+	return nil, fmt.Errorf(errKeyNotFound, fmt.Errorf("%s in: %v", name, provider.vaults))
+}
+
+func (provider *ProviderOnePassword) getField(item *onepassword.Item, property string) ([]byte, error) {
+	// default to a field labeled "password"
+	fieldLabel := "password"
+	if property != "" {
+		fieldLabel = property
+	}
+
+	if length := countFieldsWithLabel(fieldLabel, item.Fields); length != 1 {
+		return nil, fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, fieldLabel, item.Title, length))
+	}
+
+	// caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
+	value := ""
+	for _, field := range item.Fields {
+		if field.Label == fieldLabel {
+			value = field.Value
+			break
+		}
+	}
+
+	return []byte(value), nil
+}
+
+func (provider *ProviderOnePassword) getFields(item *onepassword.Item, property string) (map[string][]byte, error) {
+	secretData := make(map[string][]byte)
+	for _, field := range item.Fields {
+		if property != "" && field.Label != property {
+			continue
+		}
+		if length := countFieldsWithLabel(field.Label, item.Fields); length != 1 {
+			return nil, fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, field.Label, item.Title, length))
+		}
+
+		// caution: do not use client.GetValue here because it has undesirable behavior on keys with a dot in them
+		secretData[field.Label] = []byte(field.Value)
+	}
+
+	return secretData, nil
+}
+
+func (provider *ProviderOnePassword) getAllFields(item onepassword.Item, ref esv1beta1.ExternalSecretFind, secretData map[string][]byte) error {
+	i, err := provider.client.GetItem(item.ID, item.Vault.ID)
+	if err != nil {
+		return fmt.Errorf(errGetItem, err)
+	}
+	item = *i
+	for _, field := range item.Fields {
+		if length := countFieldsWithLabel(field.Label, item.Fields); length != 1 {
+			return fmt.Errorf(errExpectedOneField, fmt.Errorf(fieldsWithLabelFormat, field.Label, item.Title, length))
+		}
+		if ref.Name != nil {
+			matcher, err := find.New(*ref.Name)
+			if err != nil {
+				return err
+			}
+			if !matcher.MatchName(field.Label) {
+				continue
+			}
+		}
+		if _, ok := secretData[field.Label]; !ok {
+			secretData[field.Label] = []byte(field.Value)
+		}
+	}
+
+	return nil
+}
+
+func (provider *ProviderOnePassword) getFile(item *onepassword.Item, property string) ([]byte, error) {
+	for _, file := range item.Files {
+		// default to the first file when ref.Property is empty
+		if file.Name == property || property == "" {
+			contents, err := provider.client.GetFileContent(file)
+			if err != nil {
+				return nil, err
+			}
+
+			return contents, nil
+		}
+	}
+
+	return nil, fmt.Errorf(errDocumentNotFound, fmt.Errorf("'%s', '%s'", item.Title, property))
+}
+
+func (provider *ProviderOnePassword) getFiles(item *onepassword.Item, property string) (map[string][]byte, error) {
+	secretData := make(map[string][]byte)
+	for _, file := range item.Files {
+		if property != "" && file.Name != property {
+			continue
+		}
+		contents, err := provider.client.GetFileContent(file)
+		if err != nil {
+			return nil, err
+		}
+		secretData[file.Name] = contents
+	}
+
+	return secretData, nil
+}
+
+func (provider *ProviderOnePassword) getAllFiles(item onepassword.Item, ref esv1beta1.ExternalSecretFind, secretData map[string][]byte) error {
+	for _, file := range item.Files {
+		if ref.Name != nil {
+			matcher, err := find.New(*ref.Name)
+			if err != nil {
+				return err
+			}
+			if !matcher.MatchName(file.Name) {
+				continue
+			}
+		}
+		if _, ok := secretData[file.Name]; !ok {
+			contents, err := provider.client.GetFileContent(file)
+			if err != nil {
+				return err
+			}
+			secretData[file.Name] = contents
+		}
+	}
+
+	return nil
+}
+
+func (provider *ProviderOnePassword) getAllForVault(vaultID string, ref esv1beta1.ExternalSecretFind, secretData map[string][]byte) error {
+	items, err := provider.client.GetItems(vaultID)
+	if err != nil {
+		return fmt.Errorf(errGetItem, err)
+	}
+	for _, item := range items {
+		if ref.Path != nil && *ref.Path != item.Title {
+			continue
+		}
+
+		// handle files
+		if item.Category == documentCategory {
+			err = provider.getAllFiles(item, ref, secretData)
+			if err != nil {
+				return err
+			}
+
+			continue
+		}
+
+		// handle fields
+		err = provider.getAllFields(item, ref, secretData)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func countFieldsWithLabel(fieldLabel string, fields []*onepassword.ItemField) int {
+	count := 0
+	for _, field := range fields {
+		if field.Label == fieldLabel {
+			count++
+		}
+	}
+
+	return count
+}
+
+type orderedVault struct {
+	Name  string
+	Order int
+}
+
+type orderedVaultList []orderedVault
+
+func (list orderedVaultList) Len() int           { return len(list) }
+func (list orderedVaultList) Swap(i, j int)      { list[i], list[j] = list[j], list[i] }
+func (list orderedVaultList) Less(i, j int) bool { return list[i].Order < list[j].Order }
+
+func sortVaults(vaults map[string]int) []string {
+	list := make(orderedVaultList, len(vaults))
+	index := 0
+	for key, value := range vaults {
+		list[index] = orderedVault{key, value}
+		index++
+	}
+	sort.Sort(list)
+	sortedVaults := []string{}
+	for _, item := range list {
+		sortedVaults = append(sortedVaults, item.Name)
+	}
+
+	return sortedVaults
+}
+
+func hasUniqueVaultNumbers(vaults map[string]int) bool {
+	unique := make([]int, 0, len(vaults))
+	tracker := make(map[int]bool)
+
+	for _, number := range vaults {
+		if _, ok := tracker[number]; !ok {
+			tracker[number] = true
+			unique = append(unique, number)
+		}
+	}
+
+	return len(vaults) == len(unique)
+}
+
+func init() {
+	esv1beta1.Register(&ProviderOnePassword{}, &esv1beta1.SecretStoreProvider{
+		OnePassword: &esv1beta1.OnePasswordProvider{},
+	})
+}

File diff suppressed because it is too large
+ 1407 - 0
pkg/provider/onepassword/onepassword_test.go


+ 1 - 0
pkg/provider/register/register.go

@@ -26,6 +26,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/senhasegura"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/vault"