Browse Source

Feat: Add Passbolt Provider (#3334)

* add passbolt provider

Signed-off-by: Thorben Below <56894536+thorbenbelow@users.noreply.github.com>

* Fix: return err for unimplemented methods

Signed-off-by: Thorben Below <56894536+thorbenbelow@users.noreply.github.com>

---------

Signed-off-by: Thorben Below <56894536+thorbenbelow@users.noreply.github.com>
Thorben Below 2 years ago
parent
commit
432c6bf9ab

+ 32 - 0
apis/externalsecrets/v1beta1/secretsstore_passbolt_types.go

@@ -0,0 +1,32 @@
+/*
+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"
+)
+
+// Passbolt contains a secretRef for the passbolt credentials.
+type PassboltAuth struct {
+	PasswordSecretRef   *esmeta.SecretKeySelector `json:"passwordSecretRef"`
+	PrivateKeySecretRef *esmeta.SecretKeySelector `json:"privateKeySecretRef"`
+}
+
+type PassboltProvider struct {
+	// Auth defines the information necessary to authenticate against Passbolt Server
+	Auth *PassboltAuth `json:"auth"`
+	// Host defines the Passbolt Server to connect to
+	Host string `json:"host"`
+}

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

@@ -160,6 +160,9 @@ type SecretStoreProvider struct {
 
 	// +optional
 	PasswordDepot *PasswordDepotProvider `json:"passworddepot,omitempty"`
+
+	// +optional
+	Passbolt *PassboltProvider `json:"passbolt,omitempty"`
 }
 
 type CAProviderType string

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

@@ -1921,6 +1921,51 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassboltAuth) DeepCopyInto(out *PassboltAuth) {
+	*out = *in
+	if in.PasswordSecretRef != nil {
+		in, out := &in.PasswordSecretRef, &out.PasswordSecretRef
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.PrivateKeySecretRef != nil {
+		in, out := &in.PrivateKeySecretRef, &out.PrivateKeySecretRef
+		*out = new(metav1.SecretKeySelector)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassboltAuth.
+func (in *PassboltAuth) DeepCopy() *PassboltAuth {
+	if in == nil {
+		return nil
+	}
+	out := new(PassboltAuth)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassboltProvider) DeepCopyInto(out *PassboltProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(PassboltAuth)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassboltProvider.
+func (in *PassboltProvider) DeepCopy() *PassboltProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(PassboltProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PasswordDepotAuth) DeepCopyInto(out *PasswordDepotAuth) {
 	*out = *in
 	in.SecretRef.DeepCopyInto(&out.SecretRef)
@@ -2245,6 +2290,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(PasswordDepotProvider)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Passbolt != nil {
+		in, out := &in.Passbolt, &out.Passbolt
+		*out = new(PassboltProvider)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.

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

@@ -3263,6 +3263,63 @@ spec:
                     - region
                     - vault
                     type: object
+                  passbolt:
+                    properties:
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Passbolt Server
+                        properties:
+                          passwordSecretRef:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource,
+                              In some instances, `key` is a required field.
+                            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
+                          privateKeySecretRef:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource,
+                              In some instances, `key` is a required field.
+                            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:
+                        - passwordSecretRef
+                        - privateKeySecretRef
+                        type: object
+                      host:
+                        description: Host defines the Passbolt Server to connect to
+                        type: string
+                    required:
+                    - auth
+                    - host
+                    type: object
                   passworddepot:
                     description: Configures a store to sync secrets with a Password
                       Depot instance.

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

@@ -3263,6 +3263,63 @@ spec:
                     - region
                     - vault
                     type: object
+                  passbolt:
+                    properties:
+                      auth:
+                        description: Auth defines the information necessary to authenticate
+                          against Passbolt Server
+                        properties:
+                          passwordSecretRef:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource,
+                              In some instances, `key` is a required field.
+                            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
+                          privateKeySecretRef:
+                            description: |-
+                              A reference to a specific 'key' within a Secret resource,
+                              In some instances, `key` is a required field.
+                            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:
+                        - passwordSecretRef
+                        - privateKeySecretRef
+                        type: object
+                      host:
+                        description: Host defines the Passbolt Server to connect to
+                        type: string
+                    required:
+                    - auth
+                    - host
+                    type: object
                   passworddepot:
                     description: Configures a store to sync secrets with a Password
                       Depot instance.

+ 108 - 0
deploy/crds/bundle.yaml

@@ -3685,6 +3685,60 @@ spec:
                         - region
                         - vault
                       type: object
+                    passbolt:
+                      properties:
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Passbolt Server
+                          properties:
+                            passwordSecretRef:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource,
+                                In some instances, `key` is a required field.
+                              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
+                            privateKeySecretRef:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource,
+                                In some instances, `key` is a required field.
+                              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:
+                            - passwordSecretRef
+                            - privateKeySecretRef
+                          type: object
+                        host:
+                          description: Host defines the Passbolt Server to connect to
+                          type: string
+                      required:
+                        - auth
+                        - host
+                      type: object
                     passworddepot:
                       description: Configures a store to sync secrets with a Password Depot instance.
                       properties:
@@ -8955,6 +9009,60 @@ spec:
                         - region
                         - vault
                       type: object
+                    passbolt:
+                      properties:
+                        auth:
+                          description: Auth defines the information necessary to authenticate against Passbolt Server
+                          properties:
+                            passwordSecretRef:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource,
+                                In some instances, `key` is a required field.
+                              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
+                            privateKeySecretRef:
+                              description: |-
+                                A reference to a specific 'key' within a Secret resource,
+                                In some instances, `key` is a required field.
+                              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:
+                            - passwordSecretRef
+                            - privateKeySecretRef
+                          type: object
+                        host:
+                          description: Host defines the Passbolt Server to connect to
+                          type: string
+                      required:
+                        - auth
+                        - host
+                      type: object
                     passworddepot:
                       description: Configures a store to sync secrets with a Password Depot instance.
                       properties:

+ 98 - 0
docs/api/spec.md

@@ -5019,6 +5019,91 @@ External Secrets meta/v1.SecretKeySelector
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1beta1.PassboltAuth">PassboltAuth
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.PassboltProvider">PassboltProvider</a>)
+</p>
+<p>
+<p>Passbolt contains a secretRef for the passbolt credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>passwordSecretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>privateKeySecretRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.PassboltProvider">PassboltProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.PassboltAuth">
+PassboltAuth
+</a>
+</em>
+</td>
+<td>
+<p>Auth defines the information necessary to authenticate against Passbolt Server</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>host</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Host defines the Passbolt Server to connect to</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.PasswordDepotAuth">PasswordDepotAuth
 </h3>
 <p>
@@ -5918,6 +6003,19 @@ PasswordDepotProvider
 <em>(Optional)</em>
 </td>
 </tr>
+<tr>
+<td>
+<code>passbolt</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.PassboltProvider">
+PassboltProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef

+ 2 - 0
docs/introduction/stability-support.md

@@ -54,6 +54,7 @@ The following table describes the stability level of each provider and who's res
 | [Conjur](https://external-secrets.io/latest/provider/conjur)                                               |   alpha   |                                                                                                                                 [@davidh-cyberark](https://github.com/davidh-cyberark/) |
 | [Delinea](https://external-secrets.io/latest/provider/delinea)                                             |   alpha   |                                                                                                                                     [@michaelsauter](https://github.com/michaelsauter/) |
 | [Pulumi ESC](https://external-secrets.io/latest/provider/pulumi)                                           |   alpha   |                                                                                                                                                  [@dirien](https://github.com/dirien) |
+| [Passbolt](https://external-secrets.io/latest/provider/passbolt)                                           |   alpha   |                                                                                                                                                   |
 
 ## Provider Feature Support
 
@@ -82,6 +83,7 @@ The following table show the support for features across different providers.
 | Conjur                    |              |              |                      |                         |        x         |             |                             |
 | Delinea                   |      x       |              |                      |                         |        x         |             |                             |
 | Pulumi ESC                |      x       |              |                      |                         |        x         |             |                             |
+| Passbolt                  |      x       |              |                      |                         |        x         |             |                             |
 
 ## Support Policy
 

+ 39 - 0
docs/provider/passbolt.md

@@ -0,0 +1,39 @@
+External Secrets Operator integrates with [Passbolt API](https://www.passbolt.com/) to sync Passbolt to secrets held on the Kubernetes cluster.
+
+
+
+### Creating a Passbolt secret store
+
+Be sure the `passbolt` provider is listed in the `Kind=SecretStore` and auth and host are set.
+The API requires a password and private key provided in a secret.
+
+```yaml
+{% include 'passbolt-secret-store.yaml' %}
+```
+
+
+### Creating an external secret
+
+To sync a Passbolt secret to a Kubernetes secret, a `Kind=ExternalSecret` is needed.
+By default the secret contains name, username, uri, password and description.
+
+To only select a single property add the `property` key.
+
+```yaml
+{% include 'passbolt-external-secret-example.yaml' %}
+```
+
+The above external secret will lead to the creation of a secret in the following form:
+
+```yaml
+{% include 'passbolt-secret-example.yaml' %}
+```
+
+
+### Finding a secret by name
+
+Instead of retrieving secrets by ID you can also use `dataFrom` to search for secrets by name.
+
+```yaml
+{% include 'passbolt-external-secret-findbyname.yaml' %}
+```

+ 19 - 0
docs/snippets/passbolt-external-secret-example.yaml

@@ -0,0 +1,19 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: passbolt-example-simple
+spec:
+  refreshInterval: "15s"
+  secretStoreRef:
+    name: passbolt
+    kind: SecretStore
+  target:
+    name: passbolt-example
+  data:
+  - secretKey: full_secret
+    remoteRef:
+      key: e22487a8-feb8-4591-95aa-14b193930cb4 # Replace with ID of exising Passbolt secret
+  - secretKey: password_only
+    remoteRef:
+      key: e22487a8-feb8-4591-95aa-14b193930cb4 # Replace with ID of exising Passbolt secret
+      property: password # You can limit the secret to only display one property

+ 15 - 0
docs/snippets/passbolt-external-secret-findbyname.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: passbolt-example
+spec:
+  refreshInterval: "15s"
+  secretStoreRef:
+    name: passbolt
+    kind: SecretStore
+  target:
+    name: passbolt-example
+  dataFrom:
+    - find:
+        name:
+          regexp: ".*"

+ 8 - 0
docs/snippets/passbolt-secret-example.yaml

@@ -0,0 +1,8 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: passbolt-example
+data:
+  full_secret: '{"name":"passbolt-secret","username":"some-username","password":"supersecretpassword","uri":"passbolt.com","description":"some description"}'
+  password_only: supersecretpassword
+type: Opaque

+ 15 - 0
docs/snippets/passbolt-secret-store.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: passbolt
+spec:
+  provider:
+    passbolt:
+      host: https://passbolt.passbolt.svc.cluster.local
+      auth:
+        passwordSecretRef:
+          key: password
+          name: passbolt-credentials
+        privateKeySecretRef:
+          key: privateKey
+          name: passbolt-credentials

+ 3 - 0
go.mod

@@ -82,6 +82,7 @@ require (
 	github.com/keeper-security/secrets-manager-go/core v1.6.2
 	github.com/lestrrat-go/jwx/v2 v2.0.21
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1
+	github.com/passbolt/go-passbolt v0.7.0
 	github.com/pulumi/esc v0.8.3
 	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.26
 	github.com/sethvargo/go-password v0.2.0
@@ -96,6 +97,8 @@ require (
 	dario.cat/mergo v1.0.0 // indirect
 	github.com/Microsoft/go-winio v0.6.1 // indirect
 	github.com/ProtonMail/go-crypto v1.0.0 // indirect
+	github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
+	github.com/ProtonMail/gopenpgp/v2 v2.7.4 // indirect
 	github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
 	github.com/agext/levenshtein v1.2.3 // indirect
 	github.com/alessio/shellescape v1.4.2 // indirect

+ 7 - 0
go.sum

@@ -123,8 +123,13 @@ github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbV
 github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
 github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk=
 github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
 github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
 github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
+github.com/ProtonMail/gopenpgp/v2 v2.7.4 h1:Vz/8+HViFFnf2A6XX8JOvZMrA6F5puwNvvF21O1mRlo=
+github.com/ProtonMail/gopenpgp/v2 v2.7.4/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
 github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
 github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
 github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
@@ -668,6 +673,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/oracle/oci-go-sdk/v65 v65.63.1 h1:dYL7sk9L1+C9LCmoq+zjPMNteuJJfk54YExq/4pV9xQ=
 github.com/oracle/oci-go-sdk/v65 v65.63.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
+github.com/passbolt/go-passbolt v0.7.0 h1:zwwTCwL3vjTTKln1hxwKuzzax4R/yvxGXSZhMh0OY5Y=
+github.com/passbolt/go-passbolt v0.7.0/go.mod h1:af3TVSJ+0A4sXeK8KgVzhV8Tej/i25biFIQjhL0FOMk=
 github.com/pgavlin/fx v0.1.6 h1:r9jEg69DhNoCd3Xh0+5mIbdbS3PqWrVWujkY76MFRTU=
 github.com/pgavlin/fx v0.1.6/go.mod h1:KWZJ6fqBBSh8GxHYqwYCf3rYE7Gp2p0N8tJp8xv9u9M=
 github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=

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

@@ -113,6 +113,7 @@ nav:
     - Cloak End 2 End Encrypted Secrets: provider/cloak.md
     - Scaleway: provider/scaleway.md
     - Delinea: provider/delinea.md
+    - Passbolt: provider/passbolt.md
     - Pulumi ESC: provider/pulumi.md
     - Onboardbase: provider/onboardbase.md
   - Examples:

+ 296 - 0
pkg/provider/passbolt/passbolt.go

@@ -0,0 +1,296 @@
+/*
+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 passbolt
+
+import (
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/url"
+	"regexp"
+
+	"github.com/passbolt/go-passbolt/api"
+	corev1 "k8s.io/api/core/v1"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
+)
+
+const (
+	errPassboltStoreMissingProvider                = "missing: spec.provider.passbolt"
+	errPassboltStoreMissingAuth                    = "missing: spec.provider.passbolt.auth"
+	errPassboltStoreMissingAuthPassword            = "missing: spec.provider.passbolt.auth.passwordSecretRef"
+	errPassboltStoreMissingAuthPrivateKey          = "missing: spec.provider.passbolt.auth.privateKeySecretRef"
+	errPassboltStoreMissingHost                    = "missing: spec.provider.passbolt.host"
+	errPassboltExternalSecretMissingFindNameRegExp = "missing: find.name.regexp"
+	errPassboltStoreHostSchemeNotHTTPS             = "host Url has to be https scheme"
+	errPassboltSecretPropertyInvalid               = "property must be one of name, username, uri, password or description"
+	errNotImplemented                              = "not implemented"
+)
+
+type ProviderPassbolt struct {
+	client Client
+}
+
+func (provider *ProviderPassbolt) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadOnly
+}
+
+type Client interface {
+	CheckSession(ctx context.Context) bool
+	Login(ctx context.Context) error
+	Logout(ctx context.Context) error
+	GetResource(ctx context.Context, resourceID string) (*api.Resource, error)
+	GetResources(ctx context.Context, opts *api.GetResourcesOptions) ([]api.Resource, error)
+	GetResourceType(ctx context.Context, typeID string) (*api.ResourceType, error)
+	DecryptMessage(message string) (string, error)
+	GetSecret(ctx context.Context, resourceID string) (*api.Secret, error)
+}
+
+func (provider *ProviderPassbolt) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	config := store.GetSpec().Provider.Passbolt
+
+	password, err := resolvers.SecretKeyRef(
+		ctx,
+		kube,
+		store.GetKind(),
+		namespace,
+		config.Auth.PasswordSecretRef,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	privateKey, err := resolvers.SecretKeyRef(
+		ctx,
+		kube,
+		store.GetKind(),
+		namespace,
+		config.Auth.PrivateKeySecretRef,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := api.NewClient(nil, "", config.Host, privateKey, password)
+	if err != nil {
+		return nil, err
+	}
+
+	provider.client = client
+	return provider, nil
+}
+
+func (provider *ProviderPassbolt) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
+	return false, fmt.Errorf(errNotImplemented)
+}
+
+func (provider *ProviderPassbolt) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	if err := assureLoggedIn(ctx, provider.client); err != nil {
+		return nil, err
+	}
+
+	secret, err := provider.getPassboltSecret(ctx, ref.Key)
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Property == "" {
+		return utils.JSONMarshal(secret)
+	}
+
+	return secret.GetProp(ref.Property)
+}
+
+func (provider *ProviderPassbolt) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (provider *ProviderPassbolt) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
+	return fmt.Errorf(errNotImplemented)
+}
+
+func (provider *ProviderPassbolt) Validate() (esv1beta1.ValidationResult, error) {
+	return esv1beta1.ValidationResultUnknown, nil
+}
+
+func (provider *ProviderPassbolt) GetSecretMap(_ context.Context, _ esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	return nil, fmt.Errorf(errNotImplemented)
+}
+
+func (provider *ProviderPassbolt) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	res := make(map[string][]byte)
+
+	if ref.Name == nil || ref.Name.RegExp == "" {
+		return res, errors.New(errPassboltExternalSecretMissingFindNameRegExp)
+	}
+
+	if err := assureLoggedIn(ctx, provider.client); err != nil {
+		return nil, err
+	}
+
+	resources, err := provider.client.GetResources(ctx, &api.GetResourcesOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	nameRegexp, err := regexp.Compile(ref.Name.RegExp)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, resource := range resources {
+		if !nameRegexp.MatchString(resource.Name) {
+			continue
+		}
+
+		secret, err := provider.getPassboltSecret(ctx, resource.ID)
+		if err != nil {
+			return nil, err
+		}
+		marshaled, err := utils.JSONMarshal(secret)
+		if err != nil {
+			return nil, err
+		}
+		res[resource.ID] = marshaled
+	}
+
+	return res, nil
+}
+
+func (provider *ProviderPassbolt) Close(ctx context.Context) error {
+	return provider.client.Logout(ctx)
+}
+
+func (provider *ProviderPassbolt) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	config := store.GetSpec().Provider.Passbolt
+	if config == nil {
+		return nil, errors.New(errPassboltStoreMissingProvider)
+	}
+
+	if config.Auth == nil {
+		return nil, errors.New(errPassboltStoreMissingAuth)
+	}
+
+	if config.Auth.PasswordSecretRef == nil || config.Auth.PasswordSecretRef.Name == "" || config.Auth.PasswordSecretRef.Key == "" {
+		return nil, errors.New(errPassboltStoreMissingAuthPassword)
+	}
+
+	if config.Auth.PrivateKeySecretRef == nil || config.Auth.PrivateKeySecretRef.Name == "" || config.Auth.PrivateKeySecretRef.Key == "" {
+		return nil, errors.New(errPassboltStoreMissingAuthPrivateKey)
+	}
+	if config.Host == "" {
+		return nil, errors.New(errPassboltStoreMissingHost)
+	}
+
+	host, err := url.Parse(config.Host)
+	if err != nil {
+		return nil, err
+	}
+
+	if host.Scheme != "https" {
+		return nil, errors.New(errPassboltStoreHostSchemeNotHTTPS)
+	}
+
+	return nil, nil
+}
+
+func init() {
+	esv1beta1.Register(&ProviderPassbolt{}, &esv1beta1.SecretStoreProvider{
+		Passbolt: &esv1beta1.PassboltProvider{},
+	})
+}
+
+type Secret struct {
+	Name        string `json:"name"`
+	Username    string `json:"username"`
+	Password    string `json:"password"`
+	URI         string `json:"uri"`
+	Description string `json:"description"`
+}
+
+func (ps Secret) GetProp(key string) ([]byte, error) {
+	switch key {
+	case "name":
+		return []byte(ps.Name), nil
+	case "username":
+		return []byte(ps.Username), nil
+	case "uri":
+		return []byte(ps.URI), nil
+	case "password":
+		return []byte(ps.Password), nil
+	case "description":
+		return []byte(ps.Description), nil
+	default:
+		return nil, errors.New(errPassboltSecretPropertyInvalid)
+	}
+}
+
+func (provider *ProviderPassbolt) getPassboltSecret(ctx context.Context, id string) (*Secret, error) {
+	resource, err := provider.client.GetResource(ctx, id)
+	if err != nil {
+		return nil, err
+	}
+
+	secret, err := provider.client.GetSecret(ctx, resource.ID)
+	if err != nil {
+		return nil, err
+	}
+	res := Secret{
+		Name:        resource.Name,
+		Username:    resource.Username,
+		URI:         resource.URI,
+		Description: resource.Description,
+	}
+
+	raw, err := provider.client.DecryptMessage(secret.Data)
+	if err != nil {
+		return nil, err
+	}
+
+	resourceType, err := provider.client.GetResourceType(ctx, resource.ResourceTypeID)
+	if err != nil {
+		return nil, err
+	}
+
+	switch resourceType.Slug {
+	case "password-string":
+		res.Password = raw
+	case "password-and-description", "password-description-totp":
+		var pwAndDesc api.SecretDataTypePasswordAndDescription
+		if err := json.Unmarshal([]byte(raw), &pwAndDesc); err != nil {
+			return nil, err
+		}
+		res.Password = pwAndDesc.Password
+		res.Description = pwAndDesc.Description
+	case "totp":
+	default:
+		return nil, fmt.Errorf("UnknownPassboltResourceType: %q", resourceType)
+	}
+
+	return &res, nil
+}
+
+func assureLoggedIn(ctx context.Context, client Client) error {
+	if client.CheckSession(ctx) {
+		return nil
+	}
+
+	return client.Login(ctx)
+}

+ 298 - 0
pkg/provider/passbolt/passbolt_test.go

@@ -0,0 +1,298 @@
+/*
+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 passbolt
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	g "github.com/onsi/gomega"
+	"github.com/passbolt/go-passbolt/api"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
+)
+
+type PassboltClientMock struct {
+}
+
+func (p *PassboltClientMock) CheckSession(_ context.Context) bool {
+	return true
+}
+func (p *PassboltClientMock) Login(_ context.Context) error {
+	return nil
+}
+func (p *PassboltClientMock) Logout(_ context.Context) error {
+	return nil
+}
+func (p *PassboltClientMock) GetResource(_ context.Context, resourceID string) (*api.Resource, error) {
+	resmap := map[string]api.Resource{
+		"some-key1": {ID: "some-key1", Name: "some-name1", URI: "some-uri1"},
+		"some-key2": {ID: "some-key2", Name: "some-name2", URI: "some-uri2"},
+	}
+
+	if res, ok := resmap[resourceID]; ok {
+		return &res, nil
+	}
+
+	return nil, errors.New("ID not found")
+}
+
+func (p *PassboltClientMock) GetResources(_ context.Context, _ *api.GetResourcesOptions) ([]api.Resource, error) {
+	res := []api.Resource{
+		{ID: "some-key1", Name: "some-name1", URI: "some-uri1"},
+		{ID: "some-key2", Name: "some-name2", URI: "some-uri2"},
+	}
+	return res, nil
+}
+
+func (p *PassboltClientMock) GetResourceType(_ context.Context, _ string) (*api.ResourceType, error) {
+	res := &api.ResourceType{Slug: "password-and-description"}
+	return res, nil
+}
+
+func (p *PassboltClientMock) DecryptMessage(message string) (string, error) {
+	return message, nil
+}
+
+func (p *PassboltClientMock) GetSecret(_ context.Context, resourceID string) (*api.Secret, error) {
+	resmap := map[string]api.Secret{
+		"some-key1": {Data: `{"password": "some-password1", "description": "some-description1"}`},
+		"some-key2": {Data: `{"password": "some-password2", "description": "some-description2"}`},
+	}
+
+	if res, ok := resmap[resourceID]; ok {
+		return &res, nil
+	}
+
+	return nil, errors.New("ID not found")
+}
+
+var clientMock = &PassboltClientMock{}
+
+func TestValidateStore(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+
+	g.RegisterTestingT(t)
+	store := &esv1beta1.SecretStore{
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Passbolt: &esv1beta1.PassboltProvider{},
+			},
+		},
+	}
+
+	// missing auth
+	_, err := p.ValidateStore(store)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errPassboltStoreMissingAuth)))
+
+	// missing password
+	store.Spec.Provider.Passbolt.Auth = &esv1beta1.PassboltAuth{
+		PrivateKeySecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "privatekey"},
+	}
+	_, err = p.ValidateStore(store)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errPassboltStoreMissingAuthPassword)))
+
+	// missing privateKey
+	store.Spec.Provider.Passbolt.Auth = &esv1beta1.PassboltAuth{
+		PasswordSecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "password"},
+	}
+	_, err = p.ValidateStore(store)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errPassboltStoreMissingAuthPrivateKey)))
+
+	store.Spec.Provider.Passbolt.Auth = &esv1beta1.PassboltAuth{
+
+		PasswordSecretRef:   &esmeta.SecretKeySelector{Key: "some-secret", Name: "password"},
+		PrivateKeySecretRef: &esmeta.SecretKeySelector{Key: "some-secret", Name: "privatekey"},
+	}
+
+	// missing host
+	_, err = p.ValidateStore(store)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errPassboltStoreMissingHost)))
+
+	// not https
+	store.Spec.Provider.Passbolt.Host = "http://passbolt.test"
+	_, err = p.ValidateStore(store)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errPassboltStoreHostSchemeNotHTTPS)))
+
+	// spec ok
+	store.Spec.Provider.Passbolt.Host = "https://passbolt.test"
+	_, err = p.ValidateStore(store)
+	g.Expect(err).To(g.BeNil())
+}
+
+func TestClose(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+	g.RegisterTestingT(t)
+	err := p.Close(context.TODO())
+	g.Expect(err).To(g.BeNil())
+}
+
+func TestGetAllSecrets(t *testing.T) {
+	cases := []struct {
+		desc        string
+		ref         esv1beta1.ExternalSecretFind
+		expected    map[string][]byte
+		expectedErr string
+	}{
+		{
+			desc: "no matches",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{
+					RegExp: "nonexistant",
+				},
+			},
+			expected: map[string][]byte{},
+		},
+		{
+			desc: "matches",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{
+					RegExp: "some-name.*",
+				},
+			},
+			expected: map[string][]byte{
+				"some-key1": []byte(`{"name":"some-name1","username":"","password":"some-password1","uri":"some-uri1","description":"some-description1"}`),
+				"some-key2": []byte(`{"name":"some-name2","username":"","password":"some-password2","uri":"some-uri2","description":"some-description2"}`),
+			},
+		},
+		{
+			desc:        "missing find.name",
+			ref:         esv1beta1.ExternalSecretFind{},
+			expectedErr: errPassboltExternalSecretMissingFindNameRegExp,
+		},
+		{
+			desc: "empty find.name.regexp",
+			ref: esv1beta1.ExternalSecretFind{
+				Name: &esv1beta1.FindName{
+					RegExp: "",
+				},
+			},
+			expectedErr: errPassboltExternalSecretMissingFindNameRegExp,
+		},
+	}
+
+	for _, tc := range cases {
+		t.Run(tc.desc, func(t *testing.T) {
+			ctx := context.Background()
+			p := ProviderPassbolt{client: clientMock}
+
+			got, err := p.GetAllSecrets(ctx, tc.ref)
+			if err != nil {
+				if tc.expectedErr == "" {
+					t.Fatalf("failed to call GetAllSecrets: %v", err)
+				}
+
+				if !strings.Contains(err.Error(), tc.expectedErr) {
+					t.Fatalf("%q expected to contain substring %q", err.Error(), tc.expectedErr)
+				}
+
+				return
+			}
+
+			if tc.expectedErr != "" {
+				t.Fatal("expected to receive an error but got nil")
+			}
+
+			if diff := cmp.Diff(tc.expected, got); diff != "" {
+				t.Fatalf("(-got, +want)\n%s", diff)
+			}
+		})
+	}
+}
+
+func TestGetSecret(t *testing.T) {
+	g.RegisterTestingT(t)
+	tbl := []struct {
+		name     string
+		request  esv1beta1.ExternalSecretDataRemoteRef
+		expValue string
+		expErr   string
+	}{
+		{
+			name: "return err when not found",
+			request: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "nonexistent",
+			},
+			expErr: "ID not found",
+		},
+		{
+			name: "get property from secret",
+			request: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "some-key1",
+				Property: "password",
+			},
+			expValue: "some-password1",
+		},
+		{
+			name: "get full secret",
+			request: esv1beta1.ExternalSecretDataRemoteRef{
+				Key: "some-key1",
+			},
+			expValue: `{"name":"some-name1","username":"","password":"some-password1","uri":"some-uri1","description":"some-description1"}`,
+		},
+		{
+			name: "return err when using invalid property",
+			request: esv1beta1.ExternalSecretDataRemoteRef{
+				Key:      "some-key1",
+				Property: "invalid",
+			},
+			expErr: errPassboltSecretPropertyInvalid,
+		},
+	}
+
+	for _, row := range tbl {
+		t.Run(row.name, func(_ *testing.T) {
+			p := &ProviderPassbolt{client: clientMock}
+
+			out, err := p.GetSecret(context.Background(), row.request)
+			if row.expErr != "" {
+				g.Expect(err).To(g.MatchError(row.expErr))
+			} else {
+				g.Expect(err).ToNot(g.HaveOccurred())
+			}
+			g.Expect(string(out)).To(g.Equal(row.expValue))
+		})
+	}
+}
+
+func TestSecretExists(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+	g.RegisterTestingT(t)
+	_, err := p.SecretExists(context.TODO(), nil)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errNotImplemented)))
+}
+func TestPushSecret(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+	g.RegisterTestingT(t)
+	err := p.PushSecret(context.TODO(), nil, nil)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errNotImplemented)))
+}
+func TestDeleteSecret(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+	g.RegisterTestingT(t)
+	err := p.DeleteSecret(context.TODO(), nil)
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errNotImplemented)))
+}
+func TestGetSecretMap(t *testing.T) {
+	p := &ProviderPassbolt{client: clientMock}
+	g.RegisterTestingT(t)
+	_, err := p.GetSecretMap(context.TODO(), esv1beta1.ExternalSecretDataRemoteRef{})
+	g.Expect(err).To(g.BeEquivalentTo(fmt.Errorf(errNotImplemented)))
+}

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

@@ -36,6 +36,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onboardbase"
 	_ "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/passbolt"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/passworddepot"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/pulumi"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/scaleway"