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 bitwarden secret manager support (#3603)

This commit is contained in:
Gergely Brautigam 2024-06-28 06:04:25 +02:00 committed by GitHub
parent 907e8ebc82
commit 095537e6ad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 2542 additions and 4 deletions

View file

@ -146,6 +146,7 @@ run: generate ## Run app locally (without a k8s cluster)
manifests: helm.generate ## Generate manifests from helm chart
mkdir -p $(OUTPUT_DIR)/deploy/manifests
helm dependency build $(HELM_DIR)
helm template external-secrets $(HELM_DIR) -f deploy/manifests/helm-values.yaml > $(OUTPUT_DIR)/deploy/manifests/external-secrets.yaml
crds.install: generate ## Install CRDs into a cluster. This is for convenience

View file

@ -0,0 +1,47 @@
/*
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"
)
// BitwardenSecretsManagerProvider configures a store to sync secrets with a Bitwarden Secrets Manager instance.
type BitwardenSecretsManagerProvider struct {
APIURL string `json:"apiURL,omitempty"`
IdentityURL string `json:"identityURL,omitempty"`
BitwardenServerSDKURL string `json:"bitwardenServerSDKURL,omitempty"`
// Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
// can be performed.
// +required
CABundle string `json:"caBundle"`
// OrganizationID determines which organization this secret store manages.
OrganizationID string `json:"organizationID"`
// ProjectID determines which project this secret store manages.
ProjectID string `json:"projectID"`
// Auth configures how secret-manager authenticates with a bitwarden machine account instance.
// Make sure that the token being used has permissions on the given secret.
Auth BitwardenSecretsManagerAuth `json:"auth"`
}
// BitwardenSecretsManagerAuth contains the ref to the secret that contains the machine account token.
type BitwardenSecretsManagerAuth struct {
SecretRef BitwardenSecretsManagerSecretRef `json:"secretRef"`
}
// BitwardenSecretsManagerSecretRef contains the credential ref to the bitwarden instance.
type BitwardenSecretsManagerSecretRef struct {
// AccessToken used for the bitwarden instance.
// +required
Credentials esmeta.SecretKeySelector `json:"credentials"`
}

View file

@ -74,6 +74,10 @@ type SecretStoreProvider struct {
// +optional
Akeyless *AkeylessProvider `json:"akeyless,omitempty"`
// BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider
// +optional
BitwardenSecretsManager *BitwardenSecretsManagerProvider `json:"bitwardensecretsmanager,omitempty"`
// Vault configures this store to sync secrets using Hashi provider
// +optional
Vault *VaultProvider `json:"vault,omitempty"`

View file

@ -391,6 +391,54 @@ func (in *AzureKVProvider) DeepCopy() *AzureKVProvider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BitwardenSecretsManagerAuth) DeepCopyInto(out *BitwardenSecretsManagerAuth) {
*out = *in
in.SecretRef.DeepCopyInto(&out.SecretRef)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitwardenSecretsManagerAuth.
func (in *BitwardenSecretsManagerAuth) DeepCopy() *BitwardenSecretsManagerAuth {
if in == nil {
return nil
}
out := new(BitwardenSecretsManagerAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BitwardenSecretsManagerProvider) DeepCopyInto(out *BitwardenSecretsManagerProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitwardenSecretsManagerProvider.
func (in *BitwardenSecretsManagerProvider) DeepCopy() *BitwardenSecretsManagerProvider {
if in == nil {
return nil
}
out := new(BitwardenSecretsManagerProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BitwardenSecretsManagerSecretRef) DeepCopyInto(out *BitwardenSecretsManagerSecretRef) {
*out = *in
in.Credentials.DeepCopyInto(&out.Credentials)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BitwardenSecretsManagerSecretRef.
func (in *BitwardenSecretsManagerSecretRef) DeepCopy() *BitwardenSecretsManagerSecretRef {
if in == nil {
return nil
}
out := new(BitwardenSecretsManagerSecretRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CAProvider) DeepCopyInto(out *CAProvider) {
*out = *in
@ -2290,6 +2338,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(AkeylessProvider)
(*in).DeepCopyInto(*out)
}
if in.BitwardenSecretsManager != nil {
in, out := &in.BitwardenSecretsManager, &out.BitwardenSecretsManager
*out = new(BitwardenSecretsManagerProvider)
(*in).DeepCopyInto(*out)
}
if in.Vault != nil {
in, out := &in.Vault, &out.Vault
*out = new(VaultProvider)

View file

@ -2294,6 +2294,68 @@ spec:
required:
- vaultUrl
type: object
bitwardensecretsmanager:
description: BitwardenSecretsManager configures this store to
sync secrets using BitwardenSecretsManager provider
properties:
apiURL:
type: string
auth:
description: |-
Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.
properties:
secretRef:
description: BitwardenSecretsManagerSecretRef contains
the credential ref to the bitwarden instance.
properties:
credentials:
description: AccessToken used for the bitwarden instance.
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:
- credentials
type: object
required:
- secretRef
type: object
bitwardenServerSDKURL:
type: string
caBundle:
description: |-
Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.
type: string
identityURL:
type: string
organizationID:
description: OrganizationID determines which organization
this secret store manages.
type: string
projectID:
description: ProjectID determines which project this secret
store manages.
type: string
required:
- auth
- caBundle
- organizationID
- projectID
type: object
chef:
description: Chef configures this store to sync secrets with chef
server

View file

@ -2294,6 +2294,68 @@ spec:
required:
- vaultUrl
type: object
bitwardensecretsmanager:
description: BitwardenSecretsManager configures this store to
sync secrets using BitwardenSecretsManager provider
properties:
apiURL:
type: string
auth:
description: |-
Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.
properties:
secretRef:
description: BitwardenSecretsManagerSecretRef contains
the credential ref to the bitwarden instance.
properties:
credentials:
description: AccessToken used for the bitwarden instance.
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:
- credentials
type: object
required:
- secretRef
type: object
bitwardenServerSDKURL:
type: string
caBundle:
description: |-
Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.
type: string
identityURL:
type: string
organizationID:
description: OrganizationID determines which organization
this secret store manages.
type: string
projectID:
description: ProjectID determines which project this secret
store manages.
type: string
required:
- auth
- caBundle
- organizationID
- projectID
type: object
chef:
description: Chef configures this store to sync secrets with chef
server

View file

@ -0,0 +1,6 @@
dependencies:
- name: bitwarden-sdk-server
repository: oci://ghcr.io/external-secrets/charts
version: v0.1.4
digest: sha256:f60d5e4c6ad432fc7efdb0dad33774afaa88e02bd82eb9d5224372828f7d52be
generated: "2024-06-20T10:01:52.49841+02:00"

View file

@ -13,3 +13,9 @@ icon: https://raw.githubusercontent.com/external-secrets/external-secrets/main/a
maintainers:
- name: mcavoyk
email: kellinmcavoy@gmail.com
dependencies:
- name: bitwarden-sdk-server
version: v0.1.4
repository: oci://ghcr.io/external-secrets/charts
condition: bitwarden-sdk-server.enabled

View file

@ -35,6 +35,7 @@ The command removes all the Kubernetes components associated with the chart and
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| affinity | object | `{}` | |
| bitwarden-sdk-server.enabled | bool | `false` | |
| certController.affinity | object | `{}` | |
| certController.create | bool | `true` | Specifies whether a certificate controller deployment be created. |
| certController.deploymentAnnotations | object | `{}` | Annotations to add to Deployment |
@ -108,7 +109,7 @@ The command removes all the Kubernetes components associated with the chart and
| global.tolerations | list | `[]` | |
| global.topologySpreadConstraints | list | `[]` | |
| hostNetwork | bool | `false` | Run the controller on the host network |
| image.flavour | string | `""` | The flavour of tag you want to use There are different image flavours available, like distroless and ubi. Please see GitHub release notes for image tags for these flavors. By default the distroless image is used. |
| image.flavour | string | `""` | The flavour of tag you want to use There are different image flavours available, like distroless and ubi. Please see GitHub release notes for image tags for these flavors. By default, the distroless image is used. |
| image.pullPolicy | string | `"IfNotPresent"` | |
| image.repository | string | `"ghcr.io/external-secrets/external-secrets"` | |
| image.tag | string | `""` | The image tag to use. The default is the chart appVersion. |

View file

@ -2158,6 +2158,63 @@ should match snapshot of default values:
required:
- vaultUrl
type: object
bitwardensecretsmanager:
description: BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider
properties:
apiURL:
type: string
auth:
description: |-
Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.
properties:
secretRef:
description: BitwardenSecretsManagerSecretRef contains the credential ref to the bitwarden instance.
properties:
credentials:
description: AccessToken used for the bitwarden instance.
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:
- credentials
type: object
required:
- secretRef
type: object
bitwardenServerSDKURL:
type: string
caBundle:
description: |-
Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.
type: string
identityURL:
type: string
organizationID:
description: OrganizationID determines which organization this secret store manages.
type: string
projectID:
description: ProjectID determines which project this secret store manages.
type: string
required:
- auth
- caBundle
- organizationID
- projectID
type: object
chef:
description: Chef configures this store to sync secrets with chef server
properties:

View file

@ -14,6 +14,9 @@ global:
replicaCount: 1
bitwarden-sdk-server:
enabled: false
# -- Specifies the amount of historic ReplicaSets k8s should keep (see https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#clean-up-policy)
revisionHistoryLimit: 10
@ -25,7 +28,7 @@ image:
# -- The flavour of tag you want to use
# There are different image flavours available, like distroless and ubi.
# Please see GitHub release notes for image tags for these flavors.
# By default the distroless image is used.
# By default, the distroless image is used.
flavour: ""
# -- If set, install and upgrade CRDs through helm chart.

View file

@ -2804,6 +2804,63 @@ spec:
required:
- vaultUrl
type: object
bitwardensecretsmanager:
description: BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider
properties:
apiURL:
type: string
auth:
description: |-
Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.
properties:
secretRef:
description: BitwardenSecretsManagerSecretRef contains the credential ref to the bitwarden instance.
properties:
credentials:
description: AccessToken used for the bitwarden instance.
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:
- credentials
type: object
required:
- secretRef
type: object
bitwardenServerSDKURL:
type: string
caBundle:
description: |-
Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.
type: string
identityURL:
type: string
organizationID:
description: OrganizationID determines which organization this secret store manages.
type: string
projectID:
description: ProjectID determines which project this secret store manages.
type: string
required:
- auth
- caBundle
- organizationID
- projectID
type: object
chef:
description: Chef configures this store to sync secrets with chef server
properties:
@ -8295,6 +8352,63 @@ spec:
required:
- vaultUrl
type: object
bitwardensecretsmanager:
description: BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider
properties:
apiURL:
type: string
auth:
description: |-
Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.
properties:
secretRef:
description: BitwardenSecretsManagerSecretRef contains the credential ref to the bitwarden instance.
properties:
credentials:
description: AccessToken used for the bitwarden instance.
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:
- credentials
type: object
required:
- secretRef
type: object
bitwardenServerSDKURL:
type: string
caBundle:
description: |-
Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.
type: string
identityURL:
type: string
organizationID:
description: OrganizationID determines which organization this secret store manages.
type: string
projectID:
description: ProjectID determines which project this secret store manages.
type: string
required:
- auth
- caBundle
- organizationID
- projectID
type: object
chef:
description: Chef configures this store to sync secrets with chef server
properties:

View file

@ -1001,6 +1001,166 @@ string
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.BitwardenSecretsManagerAuth">BitwardenSecretsManagerAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.BitwardenSecretsManagerProvider">BitwardenSecretsManagerProvider</a>)
</p>
<p>
<p>BitwardenSecretsManagerAuth contains the ref to the secret that contains the machine account token.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>secretRef</code></br>
<em>
<a href="#external-secrets.io/v1beta1.BitwardenSecretsManagerSecretRef">
BitwardenSecretsManagerSecretRef
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.BitwardenSecretsManagerProvider">BitwardenSecretsManagerProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>BitwardenSecretsManagerProvider configures a store to sync secrets with a Bitwarden Secrets Manager instance.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiURL</code></br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>identityURL</code></br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>bitwardenServerSDKURL</code></br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>caBundle</code></br>
<em>
string
</em>
</td>
<td>
<p>Base64 encoded certificate for the bitwarden server sdk. The sdk MUST run with HTTPS to make sure no MITM attack
can be performed.</p>
</td>
</tr>
<tr>
<td>
<code>organizationID</code></br>
<em>
string
</em>
</td>
<td>
<p>OrganizationID determines which organization this secret store manages.</p>
</td>
</tr>
<tr>
<td>
<code>projectID</code></br>
<em>
string
</em>
</td>
<td>
<p>ProjectID determines which project this secret store manages.</p>
</td>
</tr>
<tr>
<td>
<code>auth</code></br>
<em>
<a href="#external-secrets.io/v1beta1.BitwardenSecretsManagerAuth">
BitwardenSecretsManagerAuth
</a>
</em>
</td>
<td>
<p>Auth configures how secret-manager authenticates with a bitwarden machine account instance.
Make sure that the token being used has permissions on the given secret.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.BitwardenSecretsManagerSecretRef">BitwardenSecretsManagerSecretRef
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.BitwardenSecretsManagerAuth">BitwardenSecretsManagerAuth</a>)
</p>
<p>
<p>BitwardenSecretsManagerSecretRef contains the credential ref to the bitwarden instance.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>credentials</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>
<p>AccessToken used for the bitwarden instance.</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.CAProvider">CAProvider
</h3>
<p>
@ -5975,6 +6135,20 @@ AkeylessProvider
</tr>
<tr>
<td>
<code>bitwardensecretsmanager</code></br>
<em>
<a href="#external-secrets.io/v1beta1.BitwardenSecretsManagerProvider">
BitwardenSecretsManagerProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>BitwardenSecretsManager configures this store to sync secrets using BitwardenSecretsManager provider</p>
</td>
</tr>
<tr>
<td>
<code>vault</code></br>
<em>
<a href="#external-secrets.io/v1beta1.VaultProvider">

View file

@ -57,13 +57,14 @@ The following table describes the stability level of each provider and who's res
| [Passbolt](https://external-secrets.io/latest/provider/passbolt) | alpha | |
| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
| [Device42](https://external-secrets.io/latest/provider/device42) | alpha | |
| [Bitwarden Secrets Manager](https://external-secrets.io/latest/provider/bitwarden-secrets-manager) | alpha | |
## Provider Feature Support
The following table show the support for features across different providers.
| Provider | find by name | find by tags | metadataPolicy Fetch | referent authentication | store validation | push secret | DeletionPolicy Merge/Delete |
| ------------------------- | :----------: | :----------: | :------------------: | :---------------------: | :--------------: | :---------: | :-------------------------: |
|---------------------------|:------------:| :----------: | :------------------: | :---------------------: | :--------------: |:-----------:|:---------------------------:|
| AWS Secrets Manager | x | x | x | x | x | x | x |
| AWS Parameter Store | x | x | x | x | x | x | x |
| Hashicorp Vault | x | x | x | x | x | x | x |
@ -88,6 +89,7 @@ The following table show the support for features across different providers.
| Passbolt | x | | | | x | | |
| Infisical | x | | | x | x | | |
| Device42 | | | | | x | | |
| Bitwarden Secrets Manager | x | | | | x | x | x |
## Support Policy

View file

@ -0,0 +1,135 @@
## Bitwarden Secrets Manager Provider
This section describes how to set up the Bitwarden Secrets Manager provider for External Secrets Operator (ESO).
### Prerequisites
In order for the bitwarden provider to work, we need a second service. This service is the [Bitwarden SDK Server](https://github.com/external-secrets/bitwarden-sdk-server).
The Bitwarden SDK is Rust based and requires CGO enabled. In order to not restrict the capabilities of ESO, and the image
size ( the bitwarden Rust SDK libraries are over 150MB in size ) it has been decided to create a soft wrapper
around the SDK that runs as a separate service providing ESO with a light REST API to pull secrets through.
#### Bitwarden SDK server
The server itself can be installed together with ESO. The ESO Helm Chart packages this service as a dependency.
The Bitwarden SDK Server's full name is hardcoded to bitwarden-sdk-server. This is so that the exposed service URL
gets a determinable endpoint.
In order to install the service install ESO with the following helm directive:
```
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace \
--set bitwarden-sdk-server.enabled=true
```
##### Certificate
The Bitwarden SDK Server _NEEDS_ to run as an HTTPS service. That means that any installation that once to with Bitwarden
provider will need to generate a certificate. The best approach for that is to use cert-manager. It's easy to set up
and can generate a certificate that the store can use to connect with the server.
For a sample set up look at the bitwarden sdk server's test setup. It contains a self-signed certificate issuer for
cert-manager.
### External secret store
With that out of the way, let's take a look at how a secret store would look like.
```yaml
{% include 'bitwarden-secrets-manager-secret-store.yaml' %}
```
The api url and identity url are optional. The secret should contain the token for the Machine account for bitwarden.
!!! note inline end
Make sure that the machine account has Read-Write access to the Project that the secrets are in.
!!! note inline end
A secret store is organization/project dependent. Meaning a 1 store == 1 organization/project. This is so that we ensure
that no other project's secrets can be modified accidentally _or_ intentionally.
### External Secrets
There are two ways to fetch secrets from the provider.
#### Find by UUID
In order to fetch a secret by using its UUID simply provide that as remote key in the external secrets like this:
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: bitwarden
spec:
refreshInterval: 10s
secretStoreRef:
# This name must match the metadata.name in the `SecretStore`
name: bitwarden-secretsmanager
kind: SecretStore
data:
- secretKey: test
remoteRef:
key: "339062b8-a5a1-4303-bf1d-b1920146a622"
```
#### Find by Name
To find a secret using its name, we need a bit more information. Mainly, these are the rules to find a secret:
- if name is a UUID get the secret
- if name is NOT a UUID Property is mandatory that defines the projectID to look for
- if name + projectID + organizationID matches, we return that secret
- if more than one name exists for the same projectID within the same organization we error
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: bitwarden
spec:
refreshInterval: 10s
secretStoreRef:
# This name must match the metadata.name in the `SecretStore`
name: bitwarden-secretsmanager
kind: SecretStore
data:
- secretKey: test
remoteRef:
key: "secret-name"
```
### Push Secret
Pushing a secret is also implemented. Pushing a secret requires even more restrictions because Bitwarden Secrets Manager
allows creating the same secret with the same key multiple times. In order to avoid overwriting, or potentially, returning
the wrong secret, we restrict push secret with the following rules:
- name, projectID, organizationID and value AND NOTE equal, we won't push it again.
- name, projectID, organizationID and ONLY the value does not equal ( INCLUDING THE NOTE ) we update
- any of the above isn't true, we create the secret ( this means that it will create a secret in a separate project )
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: pushsecret-bitwarden # Customisable
spec:
refreshInterval: 10s # Refresh interval for which push secret will reconcile
secretStoreRefs: # A list of secret stores to push secrets to
- name: bitwarden-secretsmanager
kind: SecretStore
selector:
secret:
name: my-secret # Source Kubernetes secret to be pushed
data:
- match:
secretKey: key # Source Kubernetes secret key to be pushed
remoteRef:
remoteKey: remote-key-name # Remote reference (where the secret is going to be pushed)
metadata:
note: "Note of the secret to add."
```

View file

@ -0,0 +1,18 @@
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: bitwarden-secretsmanager
spec:
provider:
bitwardensecretsmanager:
apiURL: https://vault.bitwarden.com
identityURL: https://identity.bitwarden.com
auth:
secretRef:
credentials:
key: token
name: bitwarden-access-token
bitwardenServerSDKURL: https://bitwarden-sdk-server.default.svc.cluster.local:9998
caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ5akNDQXQ2Z0F3SUJBZ0lRS08vM1J1dXR4YWdOeThCdUcyUTJYREFOQmdrcWhraUc5dzBCQVFzRkFEQkQKTVJ3d0dnWURWUVFLRXhObGVIUmxjbTVoYkMxelpXTnlaWFJ6TG1sdk1TTXdJUVlEVlFRREV4cGpaWEowTFcxaApibUZuWlhJdFltbDBkMkZ5WkdWdUxYUnNjekFlRncweU5EQTJNVGt4TXpJd01EUmFGdzB5TkRBNU1UY3hNekl3Ck1EUmFNRU14SERBYUJnTlZCQW9URTJWNGRHVnlibUZzTFhObFkzSmxkSE11YVc4eEl6QWhCZ05WQkFNVEdtTmwKY25RdGJXRnVZV2RsY2kxaWFYUjNZWEprWlc0dGRHeHpNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QQpNSUlCQ2dLQ0FRRUExdlFxaTNCL0NVU01FaUx1b3NkTVdZV25QcWJmQ20xbnZsMWhoUWxjOW1ocDFnSmxDbndjCmE0MmxuTkx0TjNTUmdrZWFNYXppV1RyaDQ5SGdUeTNVQ2xoNDh5RXFvTmJDRUlaL2xxOHNoVzRMd2g0RTdNT08KOVJJMDY2a3JCYllYakZuam1ETjdJV1NLOVVwZjIrOUpLTi9PM3ZWTktLMGZhOERxRkppL3h3VUsyOGRNc05tZAo2NnkreW52TzRFRU51Wm9IRUFieWdrOTQ2cm9yNnNmUkxHZ3ZVYXg5cmd4dEh5TkZqcGkrbjhCUDRlQkRZeGI4CkVsQy93Q0Rza2NBNFF3TXphU3NFbDBwL3gwQm9nTS9nbWJWelNVemhBL2NGdXpMRVJmV0tuanJrbmpoenNFWncKRWlzUmZ6K3MyVnUvcm5YK3pabTBoWTFvSDZYY29mVkhOUUlEQVFBQm80SGxNSUhpTUE0R0ExVWREd0VCL3dRRQpBd0lDcERBUEJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJTeUplK1lnUWZQbWFFOEZKSHowbzZ0CjQzeGh4VENCbndZRFZSMFJCSUdYTUlHVWdqOWxlSFJsY201aGJDMXpaV055WlhSekxXSnBkSGRoY21SbGJpMXoKWkdzdGMyVnlkbVZ5TG1SbFptRjFiSFF1YzNaakxtTnNkWE4wWlhJdWJHOWpZV3lDTG1KcGRIZGhjbVJsYmkxegpaR3N0YzJWeWRtVnlMbVJsWm1GMWJIUXVjM1pqTG1Oc2RYTjBaWEl1Ykc5allXeUNDV3h2WTJGc2FHOXpkSWNFCmZ3QUFBWWNRQUFBQUFBQUFBQUFBQUFBQUFBQUFBVEFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBdllYUW5ETEgKczc3N3NJN3cwN2NWMVIrTmZvbGRYblp5ejVtQVpDZkc4T2djZDRGdjRYV3lrRG94MzlkUWo0dTJnOWlVcUNwawp2QzJsbUR1UjNrS2kzbjgySTYyQ1BDN1JmZFd3M2hqaFJOV1NKbVBGeGF6NHkrbnMvMDZ3RFBlMmZwRXpPMXIzCmwxTFdZMHBySVlMME1EYTI1c3BUdlZPdWxyeWlnUnJRRGNEbS9hZ3krSEs4RHB3dWlTTEpsdFM0Q1JVa25mb3kKS00rL213VTd4RzNrSnN5ekR0T2dOZDhZeG1lRU44Q05WSk9JalltRk9OWTJrYU51S2ZnMU1aaXArcllPTEFqUgpJdUNxOFhSSTVST2gxOFJKdVlXcVZ6MUkwbXE4aVgwYlo2WG5WRjliZ0ViQ2d3bXZOWkZha3Z4RVhkWmR2N3VmCkYvRm9PTUFlNTY3L0RBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
organizationID: 7c0d21ec-10d9-4972-bdf8-ec52df99cc86
projectID: 9c713cd6-728c-437a-a783-252b0773a0bb

2
go.mod
View file

@ -90,6 +90,7 @@ require (
github.com/sethvargo/go-password v0.3.0
github.com/spf13/pflag v1.0.5
github.com/tidwall/sjson v1.2.5
k8s.io/kube-openapi v0.0.0-20240620174524-b456828f718b
sigs.k8s.io/yaml v1.4.0
software.sslmate.com/src/go-pkcs12 v0.4.0
)
@ -195,7 +196,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20240617180043-68d350f18fd4 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240617180043-68d350f18fd4 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
k8s.io/kube-openapi v0.0.0-20240620174524-b456828f718b // indirect
lukechampine.com/frand v1.4.2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

View file

@ -93,6 +93,7 @@ nav:
- AWS Secrets Manager: provider/aws-secrets-manager.md
- AWS Parameter Store: provider/aws-parameter-store.md
- Azure Key Vault: provider/azure-key-vault.md
- Bitwarden Secrets Manager: provider/bitwarden-secrets-manager.md
- Chef: provider/chef.md
- CyberArk Conjur: provider/conjur.md
- Device42: provider/device42.md

View file

@ -0,0 +1,254 @@
/*
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 bitwarden
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
// Defined Header Keys.
const (
WardenHeaderAccessToken = "Warden-Access-Token"
WardenHeaderAPIURL = "Warden-Api-Url"
WardenHeaderIdentityURL = "Warden-Identity-Url"
)
type SecretResponse struct {
CreationDate string `json:"creationDate"`
ID string `json:"id"`
Key string `json:"key"`
Note string `json:"note"`
OrganizationID string `json:"organizationId"`
ProjectID *string `json:"projectId,omitempty"`
RevisionDate string `json:"revisionDate"`
Value string `json:"value"`
}
type SecretsDeleteResponse struct {
Data []SecretDeleteResponse `json:"data"`
}
type SecretDeleteResponse struct {
Error *string `json:"error,omitempty"`
ID string `json:"id"`
}
type SecretIdentifiersResponse struct {
Data []SecretIdentifierResponse `json:"data"`
}
type SecretIdentifierResponse struct {
ID string `json:"id"`
Key string `json:"key"`
OrganizationID string `json:"organizationId"`
}
type SecretCreateRequest struct {
Key string `json:"key"`
Note string `json:"note"`
// Organization where the secret will be created
OrganizationID string `json:"organizationId"`
// IDs of the projects that this secret will belong to
ProjectIDS []string `json:"projectIds,omitempty"`
Value string `json:"value"`
}
type SecretPutRequest struct {
ID string `json:"id"`
Key string `json:"key"`
Note string `json:"note"`
// Organization where the secret will be created
OrganizationID string `json:"organizationId"`
// IDs of the projects that this secret will belong to
ProjectIDS []string `json:"projectIds,omitempty"`
Value string `json:"value"`
}
// Client for the bitwarden SDK.
type Client interface {
GetSecret(ctx context.Context, id string) (*SecretResponse, error)
DeleteSecret(ctx context.Context, ids []string) (*SecretsDeleteResponse, error)
CreateSecret(ctx context.Context, secret SecretCreateRequest) (*SecretResponse, error)
UpdateSecret(ctx context.Context, secret SecretPutRequest) (*SecretResponse, error)
ListSecrets(ctx context.Context, organizationID string) (*SecretIdentifiersResponse, error)
}
// SdkClient creates a client to talk to the bitwarden SDK server.
type SdkClient struct {
apiURL string
identityURL string
token string
bitwardenSdkServerURL string
client *http.Client
}
func NewSdkClient(apiURL, identityURL, bitwardenURL, token string, caBundle []byte) (*SdkClient, error) {
client, err := newHTTPSClient(caBundle)
if err != nil {
return nil, fmt.Errorf("error creating https client: %w", err)
}
return &SdkClient{
apiURL: apiURL,
identityURL: identityURL,
token: token,
client: client,
bitwardenSdkServerURL: bitwardenURL,
}, nil
}
func (s *SdkClient) GetSecret(ctx context.Context, id string) (*SecretResponse, error) {
body := struct {
ID string `json:"id"`
}{
ID: id,
}
secretResp := &SecretResponse{}
if err := s.performHTTPRequestOperation(ctx, params{
method: http.MethodGet,
url: s.bitwardenSdkServerURL + "/rest/api/1/secret",
body: body,
result: &secretResp,
}); err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
return secretResp, nil
}
func (s *SdkClient) DeleteSecret(ctx context.Context, ids []string) (*SecretsDeleteResponse, error) {
body := struct {
IDs []string `json:"ids"`
}{
IDs: ids,
}
secretResp := &SecretsDeleteResponse{}
if err := s.performHTTPRequestOperation(ctx, params{
method: http.MethodDelete,
url: s.bitwardenSdkServerURL + "/rest/api/1/secret",
body: body,
result: &secretResp,
}); err != nil {
return nil, fmt.Errorf("failed to delete secret: %w", err)
}
return secretResp, nil
}
func (s *SdkClient) CreateSecret(ctx context.Context, createReq SecretCreateRequest) (*SecretResponse, error) {
secretResp := &SecretResponse{}
if err := s.performHTTPRequestOperation(ctx, params{
method: http.MethodPost,
url: s.bitwardenSdkServerURL + "/rest/api/1/secret",
body: createReq,
result: &secretResp,
}); err != nil {
return nil, fmt.Errorf("failed to create secret: %w", err)
}
return secretResp, nil
}
func (s *SdkClient) UpdateSecret(ctx context.Context, putReq SecretPutRequest) (*SecretResponse, error) {
secretResp := &SecretResponse{}
if err := s.performHTTPRequestOperation(ctx, params{
method: http.MethodPut,
url: s.bitwardenSdkServerURL + "/rest/api/1/secret",
body: putReq,
result: &secretResp,
}); err != nil {
return nil, fmt.Errorf("failed to update secret: %w", err)
}
return secretResp, nil
}
func (s *SdkClient) ListSecrets(ctx context.Context, organizationID string) (*SecretIdentifiersResponse, error) {
body := struct {
ID string `json:"organizationID"`
}{
ID: organizationID,
}
secretResp := &SecretIdentifiersResponse{}
if err := s.performHTTPRequestOperation(ctx, params{
method: http.MethodGet,
url: s.bitwardenSdkServerURL + "/rest/api/1/secrets",
body: body,
result: &secretResp,
}); err != nil {
return nil, fmt.Errorf("failed to list secrets: %w", err)
}
return secretResp, nil
}
func (s *SdkClient) constructSdkRequest(ctx context.Context, method, url string, body []byte) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("failed to construct request: %w", err)
}
req.Header.Set(WardenHeaderAccessToken, s.token)
req.Header.Set(WardenHeaderAPIURL, s.apiURL)
req.Header.Set(WardenHeaderIdentityURL, s.identityURL)
return req, nil
}
type params struct {
method string
url string
body any
result any
}
func (s *SdkClient) performHTTPRequestOperation(ctx context.Context, params params) error {
data, err := json.Marshal(params.body)
if err != nil {
return fmt.Errorf("failed to marshal body: %w", err)
}
req, err := s.constructSdkRequest(ctx, params.method, params.url, data)
if err != nil {
return fmt.Errorf("failed to construct request: %w", err)
}
resp, err := s.client.Do(req)
if err != nil {
return fmt.Errorf("failed to do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
content, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to perform http request, got response: %s with status code %d", string(content), resp.StatusCode)
}
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&params.result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
return nil
}

View file

@ -0,0 +1,167 @@
/*
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 bitwarden
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"testing"
)
// The rest of the tests much look the same, it would be nice if I could find a way
// to nicely unify the tests for all of them.
func TestSdkClient_CreateSecret(t *testing.T) {
type fields struct {
apiURL func(c *httptest.Server) string
identityURL func(c *httptest.Server) string
bitwardenSdkServerURL func(c *httptest.Server) string
token string
testServer func(response any) *httptest.Server
response any
}
type args struct {
ctx context.Context
createReq SecretCreateRequest
}
tests := []struct {
name string
fields fields
args args
want *SecretResponse
wantErr bool
}{
{
name: "create secret is successful",
fields: fields{
testServer: func(response any) *httptest.Server {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data, err := json.Marshal(response)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write(data)
}))
return testServer
},
apiURL: func(c *httptest.Server) string {
return c.URL
},
identityURL: func(c *httptest.Server) string {
return c.URL
},
bitwardenSdkServerURL: func(c *httptest.Server) string {
return c.URL
},
token: "token",
response: &SecretResponse{
ID: "id",
Key: "key",
Note: "note",
OrganizationID: "orgID",
RevisionDate: "2024-04-04",
Value: "value",
},
},
args: args{
ctx: context.Background(),
createReq: SecretCreateRequest{
Key: "key",
Note: "note",
OrganizationID: "orgID",
ProjectIDS: []string{projectID},
Value: "value",
},
},
want: &SecretResponse{
ID: "id",
Key: "key",
Note: "note",
OrganizationID: "orgID",
RevisionDate: "2024-04-04",
Value: "value",
},
},
{
name: "create secret fails",
fields: fields{
testServer: func(response any) *httptest.Server {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "nope", http.StatusInternalServerError)
}))
return testServer
},
apiURL: func(c *httptest.Server) string {
return c.URL
},
identityURL: func(c *httptest.Server) string {
return c.URL
},
bitwardenSdkServerURL: func(c *httptest.Server) string {
return c.URL
},
token: "token",
response: &SecretResponse{
ID: "id",
Key: "key",
Note: "note",
OrganizationID: "orgID",
RevisionDate: "2024-04-04",
Value: "value",
},
},
args: args{
ctx: context.Background(),
createReq: SecretCreateRequest{
Key: "key",
Note: "note",
OrganizationID: "orgID",
ProjectIDS: []string{projectID},
Value: "value",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
server := tt.fields.testServer(tt.fields.response)
defer server.Close()
s := &SdkClient{
apiURL: tt.fields.apiURL(server),
identityURL: tt.fields.identityURL(server),
bitwardenSdkServerURL: tt.fields.bitwardenSdkServerURL(server),
token: tt.fields.token,
client: server.Client(),
}
got, err := s.CreateSecret(tt.args.ctx, tt.args.createReq)
if (err != nil) != tt.wantErr {
t.Errorf("CreateSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("CreateSecret() got = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,301 @@
/*
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 bitwarden
import (
"context"
"errors"
"fmt"
corev1 "k8s.io/api/core/v1"
"k8s.io/kube-openapi/pkg/validation/strfmt"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/utils"
)
var (
errBadCertBundle = "caBundle failed to base64 decode: %w"
)
const (
// NoteMetadataKey defines the note for the pushed secret.
NoteMetadataKey = "note"
)
// PushSecret will write a single secret into the provider.
// Note: We will refuse to overwrite ANY secrets, because we can never be completely
// sure if it's the same secret we are trying to push. We only have the Name and the value
// could be different. Therefore, we will always create a new secret. Except if, the value
// the key, the note, and organization ID all match.
// We only allow to push to a single project, because GET returns a single project ID
// the secret belongs to even though technically Create allows multiple projects. This is
// to ensure that we push to the same project always, and so we can determine reliably that
// we don't need to push again.
func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
spec := p.store.GetSpec()
if spec == nil || spec.Provider == nil {
return fmt.Errorf("store does not have a provider")
}
if data.GetSecretKey() == "" {
return fmt.Errorf("pushing the whole secret is not yet implemented")
}
if data.GetRemoteKey() == "" {
return fmt.Errorf("remote key must be defined")
}
value, ok := secret.Data[data.GetSecretKey()]
if !ok {
return fmt.Errorf("failed to find secret key in secret with key: %s", data.GetSecretKey())
}
note, err := utils.FetchValueFromMetadata(NoteMetadataKey, data.GetMetadata(), "")
if err != nil {
return fmt.Errorf("failed to fetch note from metadata: %w", err)
}
// ListAll Secrets for an organization. If the key matches our key, we GetSecret that and do a compare.
remoteSecrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
if err != nil {
return fmt.Errorf("failed to get all secrets: %w", err)
}
for _, d := range remoteSecrets.Data {
if d.Key != data.GetRemoteKey() {
continue
}
sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
if err != nil {
return fmt.Errorf("failed to get secret: %w", err)
}
// If all pushed data matches, we won't push this secret.
if sec.Key == data.GetRemoteKey() &&
sec.Value == string(value) &&
sec.Note == note &&
sec.ProjectID != nil &&
*sec.ProjectID == spec.Provider.BitwardenSecretsManager.ProjectID {
// we have a complete match, skip pushing.
return nil
} else if sec.Key == data.GetRemoteKey() &&
sec.Value != string(value) &&
sec.Note == note &&
sec.ProjectID != nil &&
*sec.ProjectID == spec.Provider.BitwardenSecretsManager.ProjectID {
// only the value is different, update the existing secret.
_, err = p.bitwardenSdkClient.UpdateSecret(ctx, SecretPutRequest{
ID: sec.ID,
Key: data.GetRemoteKey(),
Note: note,
OrganizationID: spec.Provider.BitwardenSecretsManager.OrganizationID,
ProjectIDS: []string{spec.Provider.BitwardenSecretsManager.ProjectID},
Value: string(value),
})
return err
}
}
// no matching secret found, let's create it
_, err = p.bitwardenSdkClient.CreateSecret(ctx, SecretCreateRequest{
Key: data.GetRemoteKey(),
Note: note,
OrganizationID: spec.Provider.BitwardenSecretsManager.OrganizationID,
ProjectIDS: []string{spec.Provider.BitwardenSecretsManager.ProjectID},
Value: string(value),
})
return err
}
// GetSecret returns a single secret from the provider.
func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
if strfmt.IsUUID(ref.Key) {
resp, err := p.bitwardenSdkClient.GetSecret(ctx, ref.Key)
if err != nil {
return nil, fmt.Errorf("error getting secret: %w", err)
}
return []byte(resp.Value), nil
}
spec := p.store.GetSpec()
if spec == nil || spec.Provider == nil {
return nil, fmt.Errorf("store does not have a provider")
}
secret, err := p.findSecretByRef(ctx, ref.Key, spec.Provider.BitwardenSecretsManager.ProjectID)
if err != nil {
return nil, fmt.Errorf("error getting secret: %w", err)
}
// we found our secret, return the value for it
return []byte(secret.Value), nil
}
func (p *Provider) DeleteSecret(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) error {
if strfmt.IsUUID(ref.GetRemoteKey()) {
return p.deleteSecret(ctx, ref.GetRemoteKey())
}
spec := p.store.GetSpec()
if spec == nil || spec.Provider == nil {
return fmt.Errorf("store does not have a provider")
}
secret, err := p.findSecretByRef(ctx, ref.GetRemoteKey(), spec.Provider.BitwardenSecretsManager.ProjectID)
if err != nil {
return fmt.Errorf("error getting secret: %w", err)
}
return p.deleteSecret(ctx, secret.ID)
}
func (p *Provider) deleteSecret(ctx context.Context, id string) error {
resp, err := p.bitwardenSdkClient.DeleteSecret(ctx, []string{id})
if err != nil {
return fmt.Errorf("error deleting secret: %w", err)
}
var errs error
for _, data := range resp.Data {
if data.Error != nil {
errs = errors.Join(errs, fmt.Errorf("error deleting secret with id %s: %s", data.ID, *data.Error))
}
}
if errs != nil {
return fmt.Errorf("there were one or more errors deleting secrets: %w", errs)
}
return nil
}
func (p *Provider) SecretExists(ctx context.Context, ref esv1beta1.PushSecretRemoteRef) (bool, error) {
if strfmt.IsUUID(ref.GetRemoteKey()) {
_, err := p.bitwardenSdkClient.GetSecret(ctx, ref.GetRemoteKey())
if err != nil {
return false, fmt.Errorf("error getting secret: %w", err)
}
return true, nil
}
spec := p.store.GetSpec()
if spec == nil || spec.Provider == nil {
return false, fmt.Errorf("store does not have a provider")
}
if _, err := p.findSecretByRef(ctx, ref.GetRemoteKey(), spec.Provider.BitwardenSecretsManager.ProjectID); err != nil {
return false, fmt.Errorf("error getting secret: %w", err)
}
return true, nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return nil, fmt.Errorf("GetSecretMap() not implemented")
}
// GetAllSecrets gets multiple secrets from the provider and loads into a kubernetes secret.
// First load all secrets from secretStore path configuration
// Then, gets secrets from a matching name or matching custom_metadata.
func (p *Provider) GetAllSecrets(ctx context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
spec := p.store.GetSpec()
if spec == nil {
return nil, fmt.Errorf("store does not have a provider")
}
secrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
if err != nil {
return nil, fmt.Errorf("failed to get all secrets: %w", err)
}
result := map[string][]byte{}
for _, d := range secrets.Data {
sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
if err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
result[d.ID] = []byte(sec.Value)
}
return result, nil
}
// Validate validates the provider.
func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}
// Close closes the provider.
func (p *Provider) Close(_ context.Context) error {
return nil
}
// getCABundle try retrieve the CA bundle from the provider CABundle.
func (p *Provider) getCABundle(provider *esv1beta1.BitwardenSecretsManagerProvider) ([]byte, error) {
certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
if decodeErr != nil {
return nil, fmt.Errorf(errBadCertBundle, decodeErr)
}
return certBytes, nil
}
func (p *Provider) findSecretByRef(ctx context.Context, key, projectID string) (*SecretResponse, error) {
spec := p.store.GetSpec()
if spec == nil || spec.Provider == nil {
return nil, fmt.Errorf("store does not have a provider")
}
// ListAll Secrets for an organization. If the key matches our key, we GetSecret that and do a compare.
secrets, err := p.bitwardenSdkClient.ListSecrets(ctx, spec.Provider.BitwardenSecretsManager.OrganizationID)
if err != nil {
return nil, fmt.Errorf("failed to get all secrets: %w", err)
}
var remoteSecret *SecretResponse
for _, d := range secrets.Data {
if d.Key != key {
continue
}
sec, err := p.bitwardenSdkClient.GetSecret(ctx, d.ID)
if err != nil {
return nil, fmt.Errorf("failed to get secret: %w", err)
}
if sec.ProjectID != nil && *sec.ProjectID == projectID {
if remoteSecret != nil {
return nil, fmt.Errorf("more than one secret found for project %s with key %s", projectID, key)
}
// We don't break here because we WANT TO MAKE SURE that there is ONLY ONE
// such secret.
remoteSecret = sec
}
}
if remoteSecret == nil {
return nil, fmt.Errorf("no secret found for project id %s and name %s", projectID, key)
}
return remoteSecret, nil
}

View file

@ -0,0 +1,825 @@
/*
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 bitwarden
import (
"context"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
var projectID = "e8fc8f9c-2208-446e-9e89-9bc358f39b47"
func TestProviderDeleteSecret(t *testing.T) {
type fields struct {
kube client.Client
namespace string
store v1beta1.GenericStore
mock func(c *FakeClient)
assertMock func(t *testing.T, c *FakeClient)
}
type args struct {
ctx context.Context
ref v1beta1.PushSecretRemoteRef
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "delete secret is successfully with UUID",
fields: fields{
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.DeleteSecretReturnsOnCallN(0, &SecretsDeleteResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 1, c.deleteSecretCalledN)
},
},
args: args{
ctx: context.TODO(),
ref: v1alpha1.PushSecretRemoteRef{
RemoteKey: "d8f29773-3019-4973-9bbc-66327d077fe2",
},
},
},
{
name: "delete secret by name",
fields: fields{
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "key",
Note: "note",
OrganizationID: "org",
Value: "value",
ProjectID: &projectID,
})
c.DeleteSecretReturnsOnCallN(0, &SecretsDeleteResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 1, c.deleteSecretCalledN)
},
},
args: args{
ctx: context.TODO(),
ref: v1alpha1.PushSecretRemoteRef{
RemoteKey: "d8f29773-3019-4973-9bbc-66327d077fe2",
},
},
},
{
name: "delete secret by name will not delete if something doesn't match",
fields: fields{
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
projectID := "another-project"
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
Note: "note",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectID,
})
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 0, c.deleteSecretCalledN)
},
},
wantErr: true, // no secret found
args: args{
ctx: context.TODO(),
ref: v1alpha1.PushSecretRemoteRef{
RemoteKey: "this-is-a-name",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &FakeClient{}
tt.fields.mock(fakeClient)
p := &Provider{
kube: tt.fields.kube,
namespace: tt.fields.namespace,
store: tt.fields.store,
bitwardenSdkClient: fakeClient,
}
if err := p.DeleteSecret(tt.args.ctx, tt.args.ref); (err != nil) != tt.wantErr {
t.Errorf("DeleteSecret() error = %v, wantErr %v", err, tt.wantErr)
}
tt.fields.assertMock(t, fakeClient)
})
}
}
func TestProviderGetAllSecrets(t *testing.T) {
type fields struct {
kube client.Client
namespace string
store v1beta1.GenericStore
mock func(c *FakeClient)
}
type args struct {
ctx context.Context
ref v1beta1.ExternalSecretFind
}
tests := []struct {
name string
fields fields
args args
want map[string][]byte
wantErr bool
}{
{
name: "get all secrets",
fields: fields{
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "key1",
OrganizationID: "orgid",
},
{
ID: "7c0d21ec-10d9-4972-bdf8-ec52df99cc86",
Key: "key2",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "key1",
Value: "value1",
})
c.GetSecretReturnsOnCallN(1, &SecretResponse{
ID: "7c0d21ec-10d9-4972-bdf8-ec52df99cc86",
Key: "key2",
Value: "value2",
})
},
},
args: args{
ctx: context.TODO(),
ref: v1beta1.ExternalSecretFind{},
},
want: map[string][]byte{
"d8f29773-3019-4973-9bbc-66327d077fe2": []byte("value1"),
"7c0d21ec-10d9-4972-bdf8-ec52df99cc86": []byte("value2"),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &FakeClient{}
tt.fields.mock(fakeClient)
p := &Provider{
kube: tt.fields.kube,
namespace: tt.fields.namespace,
store: tt.fields.store,
bitwardenSdkClient: fakeClient,
}
got, err := p.GetAllSecrets(tt.args.ctx, tt.args.ref)
if (err != nil) != tt.wantErr {
t.Errorf("GetAllSecrets() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetAllSecrets() got = %v, want %v", got, tt.want)
}
})
}
}
func TestProviderGetSecret(t *testing.T) {
type fields struct {
kube func() client.Client
namespace string
store v1beta1.GenericStore
mock func(c *FakeClient)
}
type args struct {
ctx context.Context
ref v1beta1.ExternalSecretDataRemoteRef
}
tests := []struct {
name string
fields fields
args args
want []byte
wantErr bool
}{
{
name: "get secret with UUID",
fields: fields{
kube: func() client.Client {
return fake.NewFakeClient()
},
namespace: "default",
store: &v1beta1.SecretStore{},
mock: func(c *FakeClient) {
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "id",
Key: "key",
Note: "note",
OrganizationID: "org",
Value: "value",
})
},
},
args: args{
ctx: context.Background(),
ref: v1beta1.ExternalSecretDataRemoteRef{
Key: "d8f29773-3019-4973-9bbc-66327d077fe2",
},
},
want: []byte("value"),
},
{
name: "get secret by name",
fields: fields{
kube: func() client.Client {
return fake.NewFakeClient()
},
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "key",
Note: "note",
OrganizationID: "org",
Value: "value",
ProjectID: &projectID,
})
},
},
args: args{
ctx: context.Background(),
ref: v1beta1.ExternalSecretDataRemoteRef{
Key: "this-is-a-name",
},
},
want: []byte("value"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &FakeClient{}
tt.fields.mock(fakeClient)
p := &Provider{
kube: tt.fields.kube(),
namespace: tt.fields.namespace,
store: tt.fields.store,
bitwardenSdkClient: fakeClient,
}
got, err := p.GetSecret(tt.args.ctx, tt.args.ref)
if (err != nil) != tt.wantErr {
t.Errorf("GetSecret() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetSecret() got = %v, want %v", got, tt.want)
}
})
}
}
func TestProviderPushSecret(t *testing.T) {
type fields struct {
kube func() client.Client
namespace string
store v1beta1.GenericStore
mock func(c *FakeClient)
assertMock func(t *testing.T, c *FakeClient)
}
type args struct {
ctx context.Context
secret *corev1.Secret
data v1beta1.PushSecretData
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "push secret is successful for a none existent remote secret",
args: args{
ctx: context.Background(),
secret: &corev1.Secret{
Data: map[string][]byte{
"key": []byte("value"),
},
},
data: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "this-is-a-name",
},
},
},
},
fields: fields{
kube: func() client.Client {
return fake.NewFakeClient()
},
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "no-match", // if this is this-is-a-name it would match
Note: "",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectID,
})
c.CreateSecretReturnsOnCallN(0, &SecretResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
cargs := c.createSecretCallArguments[0]
assert.Equal(t, cargs, SecretCreateRequest{
Key: "this-is-a-name",
Note: "",
OrganizationID: "orgid",
ProjectIDS: []string{projectID},
Value: "value",
})
},
},
},
{
name: "push secret is successful for a existing remote secret but only the value differs will call update",
args: args{
ctx: context.Background(),
secret: &corev1.Secret{
Data: map[string][]byte{
"key": []byte("new-value"),
},
},
data: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "this-is-a-name",
},
},
},
},
fields: fields{
kube: func() client.Client {
return fake.NewFakeClient()
},
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
Note: "",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectID,
})
c.UpdateSecretReturnsOnCallN(0, &SecretResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
pargs := c.updateSecretCallArguments[0]
assert.Equal(t, pargs, SecretPutRequest{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
Note: "",
OrganizationID: "orgid",
ProjectIDS: []string{projectID},
Value: "new-value",
})
},
},
},
{
name: "push secret will not push if the same secret already exists",
args: args{
ctx: context.Background(),
secret: &corev1.Secret{
Data: map[string][]byte{
"key": []byte("value"),
},
},
data: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
SecretKey: "key",
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "this-is-a-name",
},
},
},
},
fields: fields{
kube: func() client.Client {
return fake.NewFakeClient()
},
namespace: "default",
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "this-is-a-name",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectID,
})
c.UpdateSecretReturnsOnCallN(0, &SecretResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 0, c.createSecretCalledN)
assert.Equal(t, 0, c.updateSecretCalledN)
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &FakeClient{}
tt.fields.mock(fakeClient)
p := &Provider{
kube: tt.fields.kube(),
namespace: tt.fields.namespace,
store: tt.fields.store,
bitwardenSdkClient: fakeClient,
}
if err := p.PushSecret(tt.args.ctx, tt.args.secret, tt.args.data); (err != nil) != tt.wantErr {
t.Errorf("PushSecret() error = %v, wantErr %v", err, tt.wantErr)
}
tt.fields.assertMock(t, fakeClient)
})
}
}
func TestProviderSecretExists(t *testing.T) {
type fields struct {
kube client.Client
namespace string
store v1beta1.GenericStore
mock func(c *FakeClient)
assertMock func(t *testing.T, c *FakeClient)
}
type args struct {
ctx context.Context
ref v1alpha1.PushSecretData
}
tests := []struct {
name string
fields fields
args args
want bool
wantErr bool
}{
{
name: "secret exists",
fields: fields{
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.GetSecretReturnsOnCallN(0, &SecretResponse{})
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 0, c.listSecretsCalledN)
},
},
args: args{
ctx: nil,
ref: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "d8f29773-3019-4973-9bbc-66327d077fe2",
},
},
},
},
want: true,
},
{
name: "secret exists by name",
fields: fields{
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "name",
OrganizationID: "orgid",
},
},
})
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "name",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectID,
})
},
},
args: args{
ctx: nil,
ref: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "name",
},
},
},
},
want: true,
},
{
name: "secret not found by name",
fields: fields{
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
c.ListSecretReturnsOnCallN(0, &SecretIdentifiersResponse{
Data: []SecretIdentifierResponse{
{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "name",
OrganizationID: "orgid",
},
},
})
projectIDDifferent := "different-project"
c.GetSecretReturnsOnCallN(0, &SecretResponse{
ID: "d8f29773-3019-4973-9bbc-66327d077fe2",
Key: "name",
OrganizationID: "orgid",
Value: "value",
ProjectID: &projectIDDifferent,
})
},
},
args: args{
ctx: nil,
ref: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "name",
},
},
},
},
want: false,
wantErr: true, // secret not found
},
{
name: "invalid name format should error",
fields: fields{
store: &v1beta1.SecretStore{
Spec: v1beta1.SecretStoreSpec{
Provider: &v1beta1.SecretStoreProvider{
BitwardenSecretsManager: &v1beta1.BitwardenSecretsManagerProvider{
OrganizationID: "orgid",
ProjectID: projectID,
},
},
},
},
mock: func(c *FakeClient) {
},
assertMock: func(t *testing.T, c *FakeClient) {
assert.Equal(t, 0, c.listSecretsCalledN)
},
},
args: args{
ctx: nil,
ref: v1alpha1.PushSecretData{
Match: v1alpha1.PushSecretMatch{
RemoteRef: v1alpha1.PushSecretRemoteRef{
RemoteKey: "name",
},
},
},
},
want: false,
wantErr: true, // invalid remote key format
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := &FakeClient{}
tt.fields.mock(fakeClient)
p := &Provider{
kube: tt.fields.kube,
namespace: tt.fields.namespace,
store: tt.fields.store,
bitwardenSdkClient: fakeClient,
}
got, err := p.SecretExists(tt.args.ctx, tt.args.ref)
if (err != nil) != tt.wantErr {
t.Errorf("SecretExists() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("SecretExists() got = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,137 @@
/*
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 bitwarden
import (
"context"
"fmt"
)
type FakeClient struct {
getSecretCallArguments []string
getSecretReturnsOnCall map[int]*SecretResponse
getSecretCalledN int
deleteSecretCallArguments [][]string
deleteSecretReturnsOnCall map[int]*SecretsDeleteResponse
deleteSecretCalledN int
createSecretCallArguments []SecretCreateRequest
createSecretReturnsOnCall map[int]*SecretResponse
createSecretCalledN int
updateSecretCallArguments []SecretPutRequest
updateSecretReturnsOnCall map[int]*SecretResponse
updateSecretCalledN int
listSecretsCallArguments []string
listSecretsReturnsOnCall map[int]*SecretIdentifiersResponse
listSecretsCalledN int
}
func (c *FakeClient) GetSecretReturnsOnCallN(call int, ret *SecretResponse) {
if c.getSecretReturnsOnCall == nil {
c.getSecretReturnsOnCall = make(map[int]*SecretResponse)
}
c.getSecretReturnsOnCall[call] = ret
}
func (c *FakeClient) GetSecret(ctx context.Context, id string) (*SecretResponse, error) {
ret, ok := c.getSecretReturnsOnCall[c.getSecretCalledN]
if !ok {
return nil, fmt.Errorf("get secret no canned responses set for call %d", c.getSecretCalledN)
}
c.getSecretCallArguments = append(c.getSecretCallArguments, id)
c.getSecretCalledN++
return ret, nil
}
func (c *FakeClient) DeleteSecretReturnsOnCallN(call int, ret *SecretsDeleteResponse) {
if c.deleteSecretReturnsOnCall == nil {
c.deleteSecretReturnsOnCall = make(map[int]*SecretsDeleteResponse)
}
c.deleteSecretReturnsOnCall[call] = ret
}
func (c *FakeClient) DeleteSecret(ctx context.Context, ids []string) (*SecretsDeleteResponse, error) {
ret, ok := c.deleteSecretReturnsOnCall[c.deleteSecretCalledN]
if !ok {
return nil, fmt.Errorf("delete secret no canned responses set for call %d", c.deleteSecretCalledN)
}
c.deleteSecretCalledN++
c.deleteSecretCallArguments = append(c.deleteSecretCallArguments, ids)
return ret, nil
}
func (c *FakeClient) CreateSecretReturnsOnCallN(call int, ret *SecretResponse) {
if c.createSecretReturnsOnCall == nil {
c.createSecretReturnsOnCall = make(map[int]*SecretResponse)
}
c.createSecretReturnsOnCall[call] = ret
}
func (c *FakeClient) CreateSecret(ctx context.Context, secret SecretCreateRequest) (*SecretResponse, error) {
ret, ok := c.createSecretReturnsOnCall[c.createSecretCalledN]
if !ok {
return nil, fmt.Errorf("create secret no canned responses set for call %d", c.createSecretCalledN)
}
c.createSecretCalledN++
c.createSecretCallArguments = append(c.createSecretCallArguments, secret)
return ret, nil
}
func (c *FakeClient) UpdateSecretReturnsOnCallN(call int, ret *SecretResponse) {
if c.updateSecretReturnsOnCall == nil {
c.updateSecretReturnsOnCall = make(map[int]*SecretResponse)
}
c.updateSecretReturnsOnCall[call] = ret
}
func (c *FakeClient) UpdateSecret(ctx context.Context, secret SecretPutRequest) (*SecretResponse, error) {
ret, ok := c.updateSecretReturnsOnCall[c.updateSecretCalledN]
if !ok {
return nil, fmt.Errorf("secret update no canned responses set for call %d", c.updateSecretCalledN)
}
c.updateSecretCalledN++
c.updateSecretCallArguments = append(c.updateSecretCallArguments, secret)
return ret, nil
}
func (c *FakeClient) ListSecretReturnsOnCallN(call int, ret *SecretIdentifiersResponse) {
if c.listSecretsReturnsOnCall == nil {
c.listSecretsReturnsOnCall = make(map[int]*SecretIdentifiersResponse)
}
c.listSecretsReturnsOnCall[call] = ret
}
func (c *FakeClient) ListSecrets(ctx context.Context, organizationID string) (*SecretIdentifiersResponse, error) {
ret, ok := c.listSecretsReturnsOnCall[c.listSecretsCalledN]
if !ok {
return nil, fmt.Errorf("secret list no canned responses set for call %d", c.listSecretsCalledN)
}
c.listSecretsCalledN++
c.listSecretsCallArguments = append(c.listSecretsCallArguments, organizationID)
return ret, nil
}

View file

@ -0,0 +1,107 @@
/*
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 bitwarden
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"time"
"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/resolvers"
)
type Provider struct {
kube client.Client
namespace string
store esv1beta1.GenericStore
bitwardenSdkClient Client
}
func init() {
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{BitwardenSecretsManager: &esv1beta1.BitwardenSecretsManagerProvider{}})
}
// NewClient creates a new Bitwarden Secret Manager client.
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.BitwardenSecretsManager == nil {
return nil, fmt.Errorf("no store type or wrong store type")
}
token, err := resolvers.SecretKeyRef(
ctx,
kube,
store.GetKind(),
namespace,
&storeSpec.Provider.BitwardenSecretsManager.Auth.SecretRef.Credentials,
)
if err != nil {
return nil, fmt.Errorf("could not resolve auth credentials: %w", err)
}
bundle, err := p.getCABundle(storeSpec.Provider.BitwardenSecretsManager)
if err != nil {
return nil, fmt.Errorf("could not resolve caBundle: %w", err)
}
sdkClient, err := NewSdkClient(
storeSpec.Provider.BitwardenSecretsManager.APIURL,
storeSpec.Provider.BitwardenSecretsManager.IdentityURL,
storeSpec.Provider.BitwardenSecretsManager.BitwardenServerSDKURL,
token,
bundle,
)
if err != nil {
return nil, fmt.Errorf("could not create SdkClient: %w", err)
}
return &Provider{
kube: kube,
namespace: namespace,
store: store,
bitwardenSdkClient: sdkClient,
}, nil
}
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadWrite
}
// ValidateStore validates the store.
func (p *Provider) ValidateStore(_ esv1beta1.GenericStore) (admission.Warnings, error) {
return nil, nil
}
// newHTTPSClient creates a new HTTPS client with the given cert.
func newHTTPSClient(cert []byte) (*http.Client, error) {
pool := x509.NewCertPool()
ok := pool.AppendCertsFromPEM(cert)
if !ok {
return nil, fmt.Errorf("can't append Conjur SSL cert")
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12},
}
return &http.Client{Transport: tr, Timeout: time.Second * 10}, nil
}

View file

@ -21,6 +21,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/alibaba"
_ "github.com/external-secrets/external-secrets/pkg/provider/aws"
_ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"
_ "github.com/external-secrets/external-secrets/pkg/provider/bitwarden"
_ "github.com/external-secrets/external-secrets/pkg/provider/chef"
_ "github.com/external-secrets/external-secrets/pkg/provider/conjur"
_ "github.com/external-secrets/external-secrets/pkg/provider/delinea"