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

Infisical provider (#3477)

* feat: added crds for infisical provider

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: implemented infisical provider logic

Signed-off-by: = <akhilmhdh@gmail.com>

* fix: resolved broken doc building due to vault doc error

Signed-off-by: = <akhilmhdh@gmail.com>

* docs: added doc for infisical provider

Signed-off-by: = <akhilmhdh@gmail.com>

* docs: fixed a warning in mkdocs on link

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: resolved all lint issues

Signed-off-by: = <akhilmhdh@gmail.com>

* doc: removed k8s auth release banner from infisical doc

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: added support for property to infisical provider

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: removed auth type and made implicit ordering of authentication based on feedback

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: support for referent authentication

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: added error for tag not supported in find

Signed-off-by: = <akhilmhdh@gmail.com>

* fix: resolved failing build

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: updated doc and added stability matrix for infisical

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: switched to less error prone use and revoke token strategy and added validate interface logic

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: code lint issue fixes

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: resolved review comments for infisical client

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: improved test cases and resolved sonar issues

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: resolved sonar suggestions

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: resolved sonar suggestions for test const ids

Signed-off-by: = <akhilmhdh@gmail.com>

* feat: store changes to assertError

Signed-off-by: = <akhilmhdh@gmail.com>

---------

Signed-off-by: = <akhilmhdh@gmail.com>
This commit is contained in:
Akhil Mohan 2024-06-12 01:57:31 +05:30 committed by GitHub
parent 253fee4c3b
commit ace1ff595f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1818 additions and 72 deletions

View file

@ -0,0 +1,53 @@
/*
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"
)
type UniversalAuthCredentials struct {
// +kubebuilder:validation:Required
ClientID esmeta.SecretKeySelector `json:"clientId"`
// +kubebuilder:validation:Required
ClientSecret esmeta.SecretKeySelector `json:"clientSecret"`
}
type InfisicalAuth struct {
// +optional
UniversalAuthCredentials *UniversalAuthCredentials `json:"universalAuthCredentials,omitempty"`
}
type MachineIdentityScopeInWorkspace struct {
// +kubebuilder:default="/"
// +optional
SecretsPath string `json:"secretsPath,omitempty"`
// +kubebuilder:validation:Required
EnvironmentSlug string `json:"environmentSlug"`
// +kubebuilder:validation:Required
ProjectSlug string `json:"projectSlug"`
}
// InfisicalProvider configures a store to sync secrets using the Infisical provider.
type InfisicalProvider struct {
// Auth configures how the Operator authenticates with the Infisical API
// +kubebuilder:validation:Required
Auth InfisicalAuth `json:"auth"`
// +kubebuilder:validation:Required
SecretsScope MachineIdentityScopeInWorkspace `json:"secretsScope"`
// +kubebuilder:default="https://app.infisical.com/api"
// +optional
HostAPI string `json:"hostAPI,omitempty"`
}

View file

@ -163,6 +163,10 @@ type SecretStoreProvider struct {
// +optional
Passbolt *PassboltProvider `json:"passbolt,omitempty"`
// Infisical configures this store to sync secrets using the Infisical provider
// +optional
Infisical *InfisicalProvider `json:"infisical,omitempty"`
}
type CAProviderType string

View file

@ -1669,6 +1669,43 @@ func (in *IBMProvider) DeepCopy() *IBMProvider {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalAuth) DeepCopyInto(out *InfisicalAuth) {
*out = *in
if in.UniversalAuthCredentials != nil {
in, out := &in.UniversalAuthCredentials, &out.UniversalAuthCredentials
*out = new(UniversalAuthCredentials)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalAuth.
func (in *InfisicalAuth) DeepCopy() *InfisicalAuth {
if in == nil {
return nil
}
out := new(InfisicalAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalProvider) DeepCopyInto(out *InfisicalProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
out.SecretsScope = in.SecretsScope
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalProvider.
func (in *InfisicalProvider) DeepCopy() *InfisicalProvider {
if in == nil {
return nil
}
out := new(InfisicalProvider)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KeeperSecurityProvider) DeepCopyInto(out *KeeperSecurityProvider) {
*out = *in
@ -1757,6 +1794,21 @@ func (in *KubernetesServer) DeepCopy() *KubernetesServer {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MachineIdentityScopeInWorkspace) DeepCopyInto(out *MachineIdentityScopeInWorkspace) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineIdentityScopeInWorkspace.
func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWorkspace {
if in == nil {
return nil
}
out := new(MachineIdentityScopeInWorkspace)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *NoSecretError) DeepCopyInto(out *NoSecretError) {
*out = *in
@ -2305,6 +2357,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(PassboltProvider)
(*in).DeepCopyInto(*out)
}
if in.Infisical != nil {
in, out := &in.Infisical, &out.Infisical
*out = new(InfisicalProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@ -2616,6 +2673,23 @@ func (in *TokenAuth) DeepCopy() *TokenAuth {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UniversalAuthCredentials) DeepCopyInto(out *UniversalAuthCredentials) {
*out = *in
in.ClientID.DeepCopyInto(&out.ClientID)
in.ClientSecret.DeepCopyInto(&out.ClientSecret)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UniversalAuthCredentials.
func (in *UniversalAuthCredentials) DeepCopy() *UniversalAuthCredentials {
if in == nil {
return nil
}
out := new(UniversalAuthCredentials)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
*out = *in

View file

@ -2883,6 +2883,81 @@ spec:
required:
- auth
type: object
infisical:
description: Infisical configures this store to sync secrets using
the Infisical provider
properties:
auth:
description: Auth configures how the Operator authenticates
with the Infisical API
properties:
universalAuthCredentials:
properties:
clientId:
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
clientSecret:
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:
- clientId
- clientSecret
type: object
type: object
hostAPI:
default: https://app.infisical.com/api
type: string
secretsScope:
properties:
environmentSlug:
type: string
projectSlug:
type: string
secretsPath:
default: /
type: string
required:
- environmentSlug
- projectSlug
type: object
required:
- auth
- secretsScope
type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets
using the KeeperSecurity provider

View file

@ -2883,6 +2883,81 @@ spec:
required:
- auth
type: object
infisical:
description: Infisical configures this store to sync secrets using
the Infisical provider
properties:
auth:
description: Auth configures how the Operator authenticates
with the Infisical API
properties:
universalAuthCredentials:
properties:
clientId:
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
clientSecret:
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:
- clientId
- clientSecret
type: object
type: object
hostAPI:
default: https://app.infisical.com/api
type: string
secretsScope:
properties:
environmentSlug:
type: string
projectSlug:
type: string
secretsPath:
default: /
type: string
required:
- environmentSlug
- projectSlug
type: object
required:
- auth
- secretsScope
type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets
using the KeeperSecurity provider

View file

@ -3337,6 +3337,77 @@ spec:
required:
- auth
type: object
infisical:
description: Infisical configures this store to sync secrets using the Infisical provider
properties:
auth:
description: Auth configures how the Operator authenticates with the Infisical API
properties:
universalAuthCredentials:
properties:
clientId:
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
clientSecret:
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:
- clientId
- clientSecret
type: object
type: object
hostAPI:
default: https://app.infisical.com/api
type: string
secretsScope:
properties:
environmentSlug:
type: string
projectSlug:
type: string
secretsPath:
default: /
type: string
required:
- environmentSlug
- projectSlug
type: object
required:
- auth
- secretsScope
type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
properties:
@ -8712,6 +8783,77 @@ spec:
required:
- auth
type: object
infisical:
description: Infisical configures this store to sync secrets using the Infisical provider
properties:
auth:
description: Auth configures how the Operator authenticates with the Infisical API
properties:
universalAuthCredentials:
properties:
clientId:
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
clientSecret:
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:
- clientId
- clientSecret
type: object
type: object
hostAPI:
default: https://app.infisical.com/api
type: string
secretsScope:
properties:
environmentSlug:
type: string
projectSlug:
type: string
secretsPath:
default: /
type: string
required:
- environmentSlug
- projectSlug
type: object
required:
- auth
- secretsScope
type: object
keepersecurity:
description: KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
properties:

View file

@ -4372,6 +4372,92 @@ string
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.InfisicalAuth">InfisicalAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>universalAuthCredentials</code></br>
<em>
<a href="#external-secrets.io/v1beta1.UniversalAuthCredentials">
UniversalAuthCredentials
</a>
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>InfisicalProvider configures a store to sync secrets using the Infisical provider.</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.InfisicalAuth">
InfisicalAuth
</a>
</em>
</td>
<td>
<p>Auth configures how the Operator authenticates with the Infisical API</p>
</td>
</tr>
<tr>
<td>
<code>secretsScope</code></br>
<em>
<a href="#external-secrets.io/v1beta1.MachineIdentityScopeInWorkspace">
MachineIdentityScopeInWorkspace
</a>
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>hostAPI</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.KeeperSecurityProvider">KeeperSecurityProvider
</h3>
<p>
@ -4586,6 +4672,55 @@ CAProvider
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.MachineIdentityScopeInWorkspace">MachineIdentityScopeInWorkspace
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.InfisicalProvider">InfisicalProvider</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>secretsPath</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>environmentSlug</code></br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
<tr>
<td>
<code>projectSlug</code></br>
<em>
string
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.NoSecretError">NoSecretError
</h3>
<p>
@ -6055,6 +6190,20 @@ PassboltProvider
<em>(Optional)</em>
</td>
</tr>
<tr>
<td>
<code>infisical</code></br>
<em>
<a href="#external-secrets.io/v1beta1.InfisicalProvider">
InfisicalProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Infisical configures this store to sync secrets using the Infisical provider</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.SecretStoreRef">SecretStoreRef
@ -6932,6 +7081,48 @@ External Secrets meta/v1.SecretKeySelector
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1beta1.UniversalAuthCredentials">UniversalAuthCredentials
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1beta1.InfisicalAuth">InfisicalAuth</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>clientId</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>clientSecret</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.ValidationResult">ValidationResult
(<code>byte</code> alias)</p></h3>
<p>

View file

@ -55,6 +55,7 @@ The following table describes the stability level of each provider and who's res
| [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 | |
| [Infisical](https://external-secrets.io/latest/provider/infisical) | alpha | [@akhilmhdh](https://github.com/akhilmhdh) |
## Provider Feature Support
@ -84,6 +85,7 @@ The following table show the support for features across different providers.
| Delinea | x | | | | x | | |
| Pulumi ESC | x | | | | x | | |
| Passbolt | x | | | | x | | |
| Infisical | x | | | x | x | | |
## Support Policy

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View file

@ -364,7 +364,7 @@ set of AWS Programmatic access credentials stored in a `Kind=Secret` and referen
### Mutual authentication (mTLS)
Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#TLS-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
Under specific compliance requirements, the Vault server can be set up to enforce mutual authentication from clients across all APIs by configuring the server with `tls_require_and_verify_client_cert = true`. This configuration differs fundamentally from the [TLS certificates auth method](#tls-certificates-authentication). While the TLS certificates auth method allows the issuance of a Vault token through the `/v1/auth/cert/login` API, the mTLS configuration solely focuses on TLS transport layer authentication and lacks any authorization-related capabilities. It's important to note that the Vault token must still be included in the request, following any of the supported authentication methods mentioned earlier.
```yaml
{% include 'vault-mtls-store.yaml' %}

View file

@ -0,0 +1,68 @@
![Infisical k8s Diagram](../pictures/external-secrets-operator.png)
Sync secrets from [Infisical](https://www.infisical.com) to your Kubernetes cluster using External Secrets Operator.
## Authentication
In order for the operator to fetch secrets from Infisical, it needs to first authenticate with Infisical.
To authenticate, you can use [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) from [Machine identities](https://infisical.com/docs/documentation/platform/identities/machine-identities).
Follow the [guide here](https://infisical.com/docs/documentation/platform/identities/universal-auth) to learn how to create and obtain a pair of Client Secret and Client ID.
## Storing Your Machine Identity Secrets
Once you have generated a pair of `Client ID` and `Client Secret`, you will need to store these credentials in your cluster as a Kubernetes secret.
!!! note inline end
Remember to replace with your own Machine Identity credentials.
```yaml
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine identity client id>
clientSecret: <machine identity client secret>
```
### Secret Store
You will then need to create a generic `SecretStore`. An sample `SecretStore` has been is shown below.
!!! tip inline end
To get your project slug from Infisical, head over to the project settings and click the button `Copy Project Slug`.
```yaml
{% include 'infisical-generic-secret-store.yaml' %}
```
!!! Note
For `ClusterSecretStore`, be sure to set `namespace` in `universalAuthCredentials.clientId` and `universalAuthCredentials.clientSecret`.
## Fetch Individual Secret(s)
To sync one or more secrets individually, use the following YAML:
```yaml
{% include 'infisical-fetch-secret.yaml' %}
```
## Fetch All Secrets
To sync all secrets from an Infisical , use the following YAML:
``` yaml
{% include 'infisical-fetch-all-secrets.yaml' %}
```
## Filter By Prefix/Name
To filter secrets by `path` (path prefix) and `name` (regular expression).
``` yaml
{% include 'infisical-filtered-secrets.yaml' %}
```

View file

@ -0,0 +1,16 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: infisical-managed-secrets
spec:
secretStoreRef:
kind: SecretStore
name: infisical
target:
name: auth-api
dataFrom:
- find:
name:
regexp: .*

View file

@ -0,0 +1,16 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: infisical-managed-secrets
spec:
secretStoreRef:
kind: SecretStore
name: infisical
target:
name: auth-api
data:
- secretKey: API_KEY
remoteRef:
key: API_KEY

View file

@ -0,0 +1,15 @@
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: infisical-managed-secrets
spec:
secretStoreRef:
kind: SecretStore
name: infisical
target:
name: auth-api
dataFrom:
- find:
path: DB_

View file

@ -0,0 +1,25 @@
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: infisical
spec:
provider:
infisical:
auth:
universalAuthCredentials:
clientId:
key: clientId
namespace: default
name: universal-auth-credentials
clientSecret:
key: clientSecret
namespace: default
name: universal-auth-credentials
# Details to pull secrets from
secretsScope:
projectSlug: first-project-fujo
environmentSlug: dev # "dev", "staging", "prod", etc..
# optional
secretsPath: / # Root is "/"
# optional
hostAPI: https://app.infisical.com

View file

@ -119,6 +119,7 @@ nav:
- Onboardbase: provider/onboardbase.md
- Password Depot: provider-passworddepot.md
- Fortanix: provider/fortanix.md
- Infisical: provider/infisical.md
- Examples:
- FluxCD: examples/gitops-using-fluxcd.md
- Anchore Engine: examples/anchore-engine-credentials.md

View file

@ -0,0 +1,257 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"time"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/metrics"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
)
type InfisicalClient struct {
BaseURL *url.URL
client *http.Client
token string
}
type InfisicalApis interface {
MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error)
GetSecretsV3(data GetSecretsV3Request) (map[string]string, error)
GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error)
RevokeAccessToken() error
}
const UserAgentName = "k8-external-secrets-operator"
const errJSONSecretUnmarshal = "unable to unmarshal secret: %w"
func NewAPIClient(baseURL string) (*InfisicalClient, error) {
baseParsedURL, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
api := &InfisicalClient{
BaseURL: baseParsedURL,
client: &http.Client{
Timeout: time.Second * 15,
},
}
return api, nil
}
func (a *InfisicalClient) SetTokenViaMachineIdentity(clientID, clientSecret string) error {
if a.token != "" {
return nil
}
loginResponse, err := a.MachineIdentityLoginViaUniversalAuth(MachineIdentityUniversalAuthLoginRequest{
ClientID: clientID,
ClientSecret: clientSecret,
})
if err != nil {
return err
}
a.token = loginResponse.AccessToken
return nil
}
func (a *InfisicalClient) RevokeAccessToken() error {
if a.token == "" {
return nil
}
if _, err := a.RevokeMachineIdentityAccessToken(RevokeMachineIdentityAccessTokenRequest{AccessToken: a.token}); err != nil {
return err
}
a.token = ""
return nil
}
func (a *InfisicalClient) resolveEndpoint(path string) string {
return a.BaseURL.ResolveReference(&url.URL{Path: path}).String()
}
func (a *InfisicalClient) do(r *http.Request) (*http.Response, error) {
if a.token != "" {
r.Header.Add("Authorization", "Bearer "+a.token)
}
r.Header.Add("User-Agent", UserAgentName)
r.Header.Add("Content-Type", "application/json")
return a.client.Do(r)
}
func (a *InfisicalClient) MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error) {
endpointURL := a.resolveEndpoint("api/v1/auth/universal-auth/login")
body, err := MarshalReqBody(data)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, endpointURL, body)
metrics.ObserveAPICall(constants.ProviderName, "MachineIdentityLoginViaUniversalAuth", err)
if err != nil {
return nil, err
}
rawRes, err := a.do(req)
if err != nil {
return nil, err
}
var res MachineIdentityDetailsResponse
err = ReadAndUnmarshal(rawRes, &res)
if err != nil {
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
}
return &res, nil
}
func (a *InfisicalClient) RevokeMachineIdentityAccessToken(data RevokeMachineIdentityAccessTokenRequest) (*RevokeMachineIdentityAccessTokenResponse, error) {
endpointURL := a.resolveEndpoint("api/v1/auth/token/revoke")
body, err := MarshalReqBody(data)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, endpointURL, body)
metrics.ObserveAPICall(constants.ProviderName, "RevokeMachineIdentityAccessToken", err)
if err != nil {
return nil, err
}
rawRes, err := a.do(req)
if err != nil {
return nil, err
}
var res RevokeMachineIdentityAccessTokenResponse
err = ReadAndUnmarshal(rawRes, &res)
if err != nil {
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
}
return &res, nil
}
func (a *InfisicalClient) GetSecretsV3(data GetSecretsV3Request) (map[string]string, error) {
endpointURL := a.resolveEndpoint("api/v3/secrets/raw")
req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
metrics.ObserveAPICall(constants.ProviderName, "GetSecretsV3", err)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("workspaceSlug", data.ProjectSlug)
q.Add("environment", data.EnvironmentSlug)
q.Add("secretPath", data.SecretPath)
q.Add("include_imports", "true")
q.Add("expandSecretReferences", "true")
req.URL.RawQuery = q.Encode()
rawRes, err := a.do(req)
if err != nil {
return nil, err
}
var res GetSecretsV3Response
err = ReadAndUnmarshal(rawRes, &res)
if err != nil {
return nil, fmt.Errorf(errJSONSecretUnmarshal, err)
}
secrets := make(map[string]string)
for _, s := range res.ImportedSecrets {
for _, el := range s.Secrets {
secrets[el.SecretKey] = el.SecretValue
}
}
for _, el := range res.Secrets {
secrets[el.SecretKey] = el.SecretValue
}
return secrets, nil
}
func (a *InfisicalClient) GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error) {
endpointURL := a.resolveEndpoint(fmt.Sprintf("api/v3/secrets/raw/%s", data.SecretKey))
req, err := http.NewRequest(http.MethodGet, endpointURL, http.NoBody)
metrics.ObserveAPICall(constants.ProviderName, "GetSecretByKeyV3", err)
if err != nil {
return "", err
}
q := req.URL.Query()
q.Add("workspaceSlug", data.ProjectSlug)
q.Add("environment", data.EnvironmentSlug)
q.Add("secretPath", data.SecretPath)
q.Add("include_imports", "true")
req.URL.RawQuery = q.Encode()
rawRes, err := a.do(req)
if err != nil {
return "", err
}
if rawRes.StatusCode == 400 {
var errRes InfisicalAPIErrorResponse
err = ReadAndUnmarshal(rawRes, &errRes)
if err != nil {
return "", fmt.Errorf(errJSONSecretUnmarshal, err)
}
if errRes.Message == "Secret not found" {
return "", esv1beta1.NoSecretError{}
}
return "", errors.New(errRes.Message)
}
var res GetSecretByKeyV3Response
err = ReadAndUnmarshal(rawRes, &res)
if err != nil {
return "", fmt.Errorf(errJSONSecretUnmarshal, err)
}
return res.Secret.SecretValue, nil
}
func MarshalReqBody(data any) (*bytes.Reader, error) {
body, err := json.Marshal(data)
if err != nil {
return nil, err
}
return bytes.NewReader(body), nil
}
func ReadAndUnmarshal(resp *http.Response, target any) error {
var buf bytes.Buffer
defer resp.Body.Close()
_, err := buf.ReadFrom(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(buf.Bytes(), target)
}

View file

@ -0,0 +1,87 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package api
type MachineIdentityUniversalAuthRefreshRequest struct {
AccessToken string `json:"accessToken"`
}
type MachineIdentityDetailsResponse struct {
AccessToken string `json:"accessToken"`
ExpiresIn int `json:"expiresIn"`
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
TokenType string `json:"tokenType"`
}
type MachineIdentityUniversalAuthLoginRequest struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
type RevokeMachineIdentityAccessTokenRequest struct {
AccessToken string `json:"accessToken"`
}
type RevokeMachineIdentityAccessTokenResponse struct {
Message string `json:"message"`
}
type GetSecretByKeyV3Request struct {
EnvironmentSlug string `json:"environment"`
ProjectSlug string `json:"workspaceSlug"`
SecretPath string `json:"secretPath"`
SecretKey string `json:"secretKey"`
}
type GetSecretByKeyV3Response struct {
Secret SecretsV3 `json:"secret"`
}
type GetSecretsV3Request struct {
EnvironmentSlug string `json:"environment"`
ProjectSlug string `json:"workspaceSlug"`
SecretPath string `json:"secretPath"`
}
type GetSecretsV3Response struct {
Secrets []SecretsV3 `json:"secrets"`
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"`
}
type SecretsV3 struct {
ID string `json:"id"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
Version int `json:"version"`
Type string `json:"string"`
SecretKey string `json:"secretKey"`
SecretValue string `json:"secretValue"`
SecretComment string `json:"secretComment"`
}
type ImportedSecretV3 struct {
Environment string `json:"environment"`
FolderID string `json:"folderId"`
SecretPath string `json:"secretPath"`
Secrets []SecretsV3 `json:"secrets"`
}
type InfisicalAPIErrorResponse struct {
StatusCode int `json:"statusCode"`
Message string `json:"message"`
Error any `json:"error"`
}

View file

@ -0,0 +1,170 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package infisical
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/tidwall/gjson"
corev1 "k8s.io/api/core/v1"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"github.com/external-secrets/external-secrets/pkg/find"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
)
var (
errNotImplemented = errors.New("not implemented")
errPropertyNotFound = "property %s does not exist in secret %s"
errTagsNotImplemented = errors.New("find by tags not supported")
)
func getPropertyValue(jsonData, propertyName, keyName string) ([]byte, error) {
result := gjson.Get(jsonData, propertyName)
if !result.Exists() {
return nil, fmt.Errorf(errPropertyNotFound, propertyName, keyName)
}
return []byte(result.Str), nil
}
// if GetSecret returns an error with type NoSecretError.
// then the secret entry will be deleted depending on the deletionPolicy.
func (p *Provider) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
secret, err := p.apiClient.GetSecretByKeyV3(api.GetSecretByKeyV3Request{
EnvironmentSlug: p.apiScope.EnvironmentSlug,
ProjectSlug: p.apiScope.ProjectSlug,
SecretPath: p.apiScope.SecretPath,
SecretKey: ref.Key,
})
if err != nil {
return nil, err
}
if ref.Property != "" {
propertyValue, err := getPropertyValue(secret, ref.Property, ref.Key)
if err != nil {
return nil, err
}
return propertyValue, nil
}
return []byte(secret), nil
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (p *Provider) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
secret, err := p.GetSecret(ctx, ref)
if err != nil {
return nil, err
}
kv := make(map[string]json.RawMessage)
err = json.Unmarshal(secret, &kv)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
}
secretData := make(map[string][]byte)
for k, v := range kv {
var strVal string
err = json.Unmarshal(v, &strVal)
if err == nil {
secretData[k] = []byte(strVal)
} else {
secretData[k] = v
}
}
return secretData, nil
}
// GetAllSecrets returns multiple k/v pairs from the provider.
func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
if ref.Tags != nil {
return nil, errTagsNotImplemented
}
secrets, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
EnvironmentSlug: p.apiScope.EnvironmentSlug,
ProjectSlug: p.apiScope.ProjectSlug,
SecretPath: p.apiScope.SecretPath,
})
if err != nil {
return nil, err
}
secretMap := make(map[string][]byte)
for key, value := range secrets {
secretMap[key] = []byte(value)
}
if ref.Name == nil && ref.Path == nil {
return secretMap, nil
}
var matcher *find.Matcher
if ref.Name != nil {
m, err := find.New(*ref.Name)
if err != nil {
return nil, err
}
matcher = m
}
selected := map[string][]byte{}
for key, value := range secrets {
if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
continue
}
selected[key] = []byte(value)
}
return selected, nil
}
// Validate checks if the client is configured correctly.
// and is able to retrieve secrets from the provider.
// If the validation result is unknown it will be ignored.
func (p *Provider) Validate() (esv1beta1.ValidationResult, error) {
// try to fetch the secrets to ensure provided credentials has access to read secrets
_, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
EnvironmentSlug: p.apiScope.EnvironmentSlug,
ProjectSlug: p.apiScope.ProjectSlug,
SecretPath: p.apiScope.SecretPath,
})
if err != nil {
return esv1beta1.ValidationResultError, fmt.Errorf("cannot read secrets with provided project scope project:%s environment:%s secret-path:%s, %w", p.apiScope.ProjectSlug, p.apiScope.EnvironmentSlug, p.apiScope.SecretPath, err)
}
return esv1beta1.ValidationResultReady, nil
}
// PushSecret will write a single secret into the provider.
func (p *Provider) PushSecret(ctx context.Context, secret *corev1.Secret, data esv1beta1.PushSecretData) error {
return errNotImplemented
}
// DeleteSecret will delete the secret from a provider.
func (p *Provider) DeleteSecret(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) error {
return errNotImplemented
}
// SecretExists checks if a secret is already present in the provider at the given location.
func (p *Provider) SecretExists(ctx context.Context, remoteRef esv1beta1.PushSecretRemoteRef) (bool, error) {
return false, errNotImplemented
}

View file

@ -0,0 +1,19 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package constants
const (
UniversalAuth = "universal-auth"
ProviderName = "infisical"
)

View file

@ -0,0 +1,58 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import (
"errors"
"time"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
)
var (
ErrMissingMockImplementation = errors.New("missing mock implmentation")
)
type MockInfisicalClient struct {
MockedGetSecretV3 func(data api.GetSecretsV3Request) (map[string]string, error)
MockedGetSecretByKeyV3 func(data api.GetSecretByKeyV3Request) (string, error)
}
func (a *MockInfisicalClient) MachineIdentityLoginViaUniversalAuth(data api.MachineIdentityUniversalAuthLoginRequest) (*api.MachineIdentityDetailsResponse, error) {
return &api.MachineIdentityDetailsResponse{
AccessToken: "test-access-token",
ExpiresIn: int(time.Hour * 24),
TokenType: "bearer",
AccessTokenMaxTTL: int(time.Hour * 24 * 2),
}, nil
}
func (a *MockInfisicalClient) GetSecretsV3(data api.GetSecretsV3Request) (map[string]string, error) {
if a.MockedGetSecretV3 == nil {
return nil, ErrMissingMockImplementation
}
return a.MockedGetSecretV3(data)
}
func (a *MockInfisicalClient) GetSecretByKeyV3(data api.GetSecretByKeyV3Request) (string, error) {
if a.MockedGetSecretByKeyV3 == nil {
return "", ErrMissingMockImplementation
}
return a.MockedGetSecretByKeyV3(data)
}
func (a *MockInfisicalClient) RevokeAccessToken() error {
return nil
}

View file

@ -0,0 +1,159 @@
/*
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 implieclient.
See the License for the specific language governing permissions and
limitations under the License.
*/
package infisical
import (
"context"
"errors"
"fmt"
ctrl "sigs.k8s.io/controller-runtime"
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"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
"github.com/external-secrets/external-secrets/pkg/utils"
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
)
var (
Logger = ctrl.Log.WithName("provider").WithName(constants.ProviderName)
)
type Provider struct {
apiClient api.InfisicalApis
apiScope *InfisicalClientScope
}
type InfisicalClientScope struct {
SecretPath string
ProjectSlug string
EnvironmentSlug string
}
// https://github.com/external-secrets/external-secrets/issues/644
var _ esv1beta1.SecretsClient = &Provider{}
var _ esv1beta1.Provider = &Provider{}
func init() {
esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
Infisical: &esv1beta1.InfisicalProvider{},
})
}
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
return esv1beta1.SecretStoreReadOnly
}
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Infisical == nil {
return nil, errors.New("invalid infisical store")
}
infisicalSpec := storeSpec.Provider.Infisical
apiClient, err := api.NewAPIClient(infisicalSpec.HostAPI)
if err != nil {
return nil, err
}
if infisicalSpec.Auth.UniversalAuthCredentials != nil {
universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
if err != nil {
return nil, err
}
clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
if err != nil {
return nil, err
}
if err := apiClient.SetTokenViaMachineIdentity(clientID, clientSecret); err != nil {
return nil, fmt.Errorf("failed to authenticate via universal auth %w", err)
}
return &Provider{
apiClient: apiClient,
apiScope: &InfisicalClientScope{
SecretPath: infisicalSpec.SecretsScope.SecretsPath,
ProjectSlug: infisicalSpec.SecretsScope.ProjectSlug,
EnvironmentSlug: infisicalSpec.SecretsScope.EnvironmentSlug,
},
}, nil
}
return &Provider{}, errors.New("authentication method not found")
}
func (p *Provider) Close(ctx context.Context) error {
if err := p.apiClient.RevokeAccessToken(); err != nil {
return err
}
return nil
}
func GetStoreSecretData(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string, secret esmeta.SecretKeySelector) (string, error) {
secretRef := esmeta.SecretKeySelector{
Name: secret.Name,
Key: secret.Key,
}
if secret.Namespace != nil {
secretRef.Namespace = secret.Namespace
}
secretData, err := resolvers.SecretKeyRef(ctx, kube, store.GetObjectKind().GroupVersionKind().Kind, namespace, &secretRef)
if err != nil {
return "", err
}
return secretData, nil
}
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
storeSpec := store.GetSpec()
infisicalStoreSpec := storeSpec.Provider.Infisical
if infisicalStoreSpec == nil {
return nil, errors.New("invalid infisical store")
}
if infisicalStoreSpec.SecretsScope.EnvironmentSlug == "" || infisicalStoreSpec.SecretsScope.ProjectSlug == "" {
return nil, errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
}
if infisicalStoreSpec.Auth.UniversalAuthCredentials != nil {
uaCredential := infisicalStoreSpec.Auth.UniversalAuthCredentials
// to validate reference authentication
err := utils.ValidateReferentSecretSelector(store, uaCredential.ClientID)
if err != nil {
return nil, err
}
err = utils.ValidateReferentSecretSelector(store, uaCredential.ClientSecret)
if err != nil {
return nil, err
}
if uaCredential.ClientID.Key == "" || uaCredential.ClientSecret.Key == "" {
return nil, errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
}
}
return nil, nil
}

View file

@ -0,0 +1,238 @@
/*
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 impliec.
See the License for the specific language governing permissions and
limitations under the License.
*/
package infisical
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esv1meta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
"github.com/external-secrets/external-secrets/pkg/provider/infisical/fake"
)
type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
var apiScope = InfisicalClientScope{
SecretPath: "/",
ProjectSlug: "first-project",
EnvironmentSlug: "dev",
}
type TestCases struct {
Name string
MockClient *fake.MockInfisicalClient
PropertyAccess string
Error error
Output any
}
func TestGetSecret(t *testing.T) {
testCases := []TestCases{
{
Name: "Get_valid_key",
MockClient: &fake.MockInfisicalClient{
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
return "value", nil
},
},
Error: nil,
Output: []byte("value"),
},
{
Name: "Get_property_key",
MockClient: &fake.MockInfisicalClient{
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
return `{"key":"value"}`, nil
},
},
Error: nil,
Output: []byte("value"),
},
{
Name: "Key_not_found",
MockClient: &fake.MockInfisicalClient{
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
// from server
return "", errors.New("Secret not found")
},
},
Error: errors.New("Secret not found"),
Output: "",
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
p := &Provider{
apiClient: tc.MockClient,
apiScope: &apiScope,
}
var property string
if tc.Name == "Get_property_key" {
property = "key"
}
output, err := p.GetSecret(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
Key: "key",
Property: property,
})
if tc.Error == nil {
assert.NoError(t, err)
assert.Equal(t, tc.Output, output)
} else {
assert.ErrorAs(t, err, &tc.Error)
}
})
}
}
func TestGetSecretMap(t *testing.T) {
testCases := []TestCases{
{
Name: "Get_valid_key_map",
MockClient: &fake.MockInfisicalClient{
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
return `{"key":"value"}`, nil
},
},
Error: nil,
Output: map[string][]byte{
"key": []byte("value"),
},
},
{
Name: "Get_invalid_map",
MockClient: &fake.MockInfisicalClient{
MockedGetSecretByKeyV3: func(data api.GetSecretByKeyV3Request) (string, error) {
return ``, nil
},
},
Error: errors.New("unexpected end of JSON input"),
Output: nil,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
p := &Provider{
apiClient: tc.MockClient,
apiScope: &apiScope,
}
output, err := p.GetSecretMap(context.Background(), esv1beta1.ExternalSecretDataRemoteRef{
Key: "key",
})
if tc.Error == nil {
assert.NoError(t, err)
assert.Equal(t, tc.Output, output)
} else {
assert.ErrorAs(t, err, &tc.Error)
}
})
}
}
func makeSecretStore(projectSlug, environment, secretPath string, fn ...storeModifier) *esv1beta1.SecretStore {
store := &esv1beta1.SecretStore{
Spec: esv1beta1.SecretStoreSpec{
Provider: &esv1beta1.SecretStoreProvider{
Infisical: &esv1beta1.InfisicalProvider{
Auth: esv1beta1.InfisicalAuth{
UniversalAuthCredentials: &esv1beta1.UniversalAuthCredentials{},
},
SecretsScope: esv1beta1.MachineIdentityScopeInWorkspace{
SecretsPath: secretPath,
EnvironmentSlug: environment,
ProjectSlug: projectSlug,
},
},
},
},
}
for _, f := range fn {
store = f(store)
}
return store
}
func withClientID(name, key string, namespace *string) storeModifier {
return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientID = esv1meta.SecretKeySelector{
Name: name,
Key: key,
Namespace: namespace,
}
return store
}
}
func withClientSecret(name, key string, namespace *string) storeModifier {
return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
store.Spec.Provider.Infisical.Auth.UniversalAuthCredentials.ClientSecret = esv1meta.SecretKeySelector{
Name: name,
Key: key,
Namespace: namespace,
}
return store
}
}
type ValidateStoreTestCase struct {
store *esv1beta1.SecretStore
assertError func(t *testing.T, err error)
}
func TestValidateStore(t *testing.T) {
const randomID = "some-random-id"
const authType = "universal-auth"
var authCredMissingErr = errors.New("universalAuthCredentials.clientId and universalAuthCredentials.clientSecret cannot be empty")
var authScopeMissingErr = errors.New("secretsScope.projectSlug and secretsScope.environmentSlug cannot be empty")
testCases := []ValidateStoreTestCase{
{
store: makeSecretStore("", "", ""),
assertError: func(t *testing.T, err error) {
require.ErrorAs(t, err, &authScopeMissingErr)
},
},
{
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil)),
assertError: func(t *testing.T, err error) {
require.ErrorAs(t, err, &authCredMissingErr)
},
},
{
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientSecret(authType, randomID, nil)),
assertError: func(t *testing.T, err error) {
require.ErrorAs(t, err, &authCredMissingErr)
},
},
{
store: makeSecretStore(apiScope.ProjectSlug, apiScope.EnvironmentSlug, apiScope.SecretPath, withClientID(authType, randomID, nil), withClientSecret(authType, randomID, nil)),
assertError: func(t *testing.T, err error) { require.NoError(t, err) },
},
}
p := Provider{}
for _, tc := range testCases {
_, err := p.ValidateStore(tc.store)
tc.assertError(t, err)
}
}

View file

@ -30,6 +30,7 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
_ "github.com/external-secrets/external-secrets/pkg/provider/gitlab"
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
_ "github.com/external-secrets/external-secrets/pkg/provider/infisical"
_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
_ "github.com/external-secrets/external-secrets/pkg/provider/onboardbase"