1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

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>
This commit is contained in:
Thorben Below 2024-04-18 09:58:25 +02:00 committed by GitHub
parent e0bdcd0d97
commit 432c6bf9ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1109 additions and 0 deletions

View file

@ -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"`
}

View file

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

View file

@ -1920,6 +1920,51 @@ func (in *OracleSecretRef) DeepCopy() *OracleSecretRef {
return out
}
// 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
@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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:

View file

@ -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

View file

@ -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
docs/provider/passbolt.md Normal file
View file

@ -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' %}
```

View file

@ -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

View file

@ -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: ".*"

View file

@ -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

View file

@ -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
go.mod
View file

@ -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
go.sum
View file

@ -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=

View file

@ -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:

View file

@ -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)
}

View file

@ -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)))
}

View file

@ -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"