mirror of
https://github.com/external-secrets/external-secrets.git
synced 2024-12-14 11:57:59 +00:00
Add Conjur Support for FindByName, FindByTag (#3364)
This commit is contained in:
parent
a85de0d1da
commit
02c6f625bd
17 changed files with 1121 additions and 454 deletions
|
@ -51,7 +51,7 @@ The following table describes the stability level of each provider and who's res
|
|||
| [Doppler SecretOps Platform](https://external-secrets.io/latest/provider/doppler) | alpha | [@ryan-blunden](https://github.com/ryan-blunden/) [@nmanoogian](https://github.com/nmanoogian/) |
|
||||
| [Keeper Security](https://www.keepersecurity.com/) | alpha | [@ppodevlab](https://github.com/ppodevlab) |
|
||||
| [Scaleway](https://external-secrets.io/latest/provider/scaleway) | alpha | [@azert9](https://github.com/azert9/) |
|
||||
| [Conjur](https://external-secrets.io/latest/provider/conjur) | alpha | [@davidh-cyberark](https://github.com/davidh-cyberark/) |
|
||||
| [Conjur](https://external-secrets.io/latest/provider/conjur) | stable | [@davidh-cyberark](https://github.com/davidh-cyberark/) [@szh](https://github.com/szh) |
|
||||
| [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 | |
|
||||
|
@ -80,7 +80,7 @@ The following table show the support for features across different providers.
|
|||
| Doppler | x | | | | x | | |
|
||||
| Keeper Security | x | | | | x | x | |
|
||||
| Scaleway | x | x | | | x | x | x |
|
||||
| Conjur | | | | | x | | |
|
||||
| Conjur | x | x | | | x | | |
|
||||
| Delinea | x | | | | x | | |
|
||||
| Pulumi ESC | x | | | | x | | |
|
||||
| Passbolt | x | | | | x | | |
|
||||
|
|
|
@ -6,12 +6,12 @@ This section describes how to set up the Conjur provider for External Secrets Op
|
|||
|
||||
Before installing the Conjur provider, you need:
|
||||
|
||||
* A running Conjur Server, with:
|
||||
* An accessible Conjur endpoint (for example: `https://myapi.example.com`).
|
||||
* Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
|
||||
* Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
|
||||
* **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
|
||||
* A Kubernetes cluster with ESO installed.
|
||||
* A running Conjur Server, with:
|
||||
* An accessible Conjur endpoint (for example: `https://myapi.example.com`).
|
||||
* Your configured Conjur authentication info (such as `hostid`, `apikey`, or JWT service ID). For more information on configuring Conjur, see [Policy statement reference](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Policy/policy-statement-ref.htm).
|
||||
* Support for your authentication method (`apikey` is supported by default, `jwt` requires additional configuration).
|
||||
* **Optional**: Conjur server certificate (see [below](#conjur-server-certificate)).
|
||||
* A Kubernetes cluster with ESO installed.
|
||||
|
||||
### Conjur server certificate
|
||||
|
||||
|
@ -21,11 +21,18 @@ If you set up your Conjur server with a self-signed certificate, we recommend th
|
|||
{% include 'conjur-ca-bundle.yaml' %}
|
||||
```
|
||||
|
||||
### External secret store with apiKey authentication
|
||||
### External secret store
|
||||
|
||||
The Conjur provider is configured as an external secret store in ESO. The Conjur provider supports these two methods to authenticate to Conjur:
|
||||
|
||||
* [`apikey`](#option-1-external-secret-store-with-apikey-authentication): uses a Conjur `hostid` and `apikey` to authenticate with Conjur
|
||||
* [`jwt`](#option-2-external-secret-store-with-jwt-authentication): uses a JWT to authenticate with Conjur
|
||||
|
||||
#### Option 1: External secret store with apiKey authentication
|
||||
|
||||
This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It is the simplest method to set up and use because your Conjur instance requires no additional configuration.
|
||||
|
||||
#### Step 1: Create an external secret store
|
||||
##### Step 1: Define an external secret store
|
||||
|
||||
!!! Tip
|
||||
Save as the file as: `conjur-secret-store.yaml`
|
||||
|
@ -34,7 +41,7 @@ This method uses a Conjur `hostid` and `apikey` to authenticate with Conjur. It
|
|||
{% include 'conjur-secret-store-apikey.yaml' %}
|
||||
```
|
||||
|
||||
#### Step 2: Create Kubernetes secrets
|
||||
##### Step 2: Create Kubernetes secrets for Conjur credentials
|
||||
|
||||
To connect to the Conjur server, the **ESO Conjur provider** needs to retrieve the `apikey` credentials from K8s secrets.
|
||||
|
||||
|
@ -54,19 +61,36 @@ kubectl -n external-secrets create secret generic conjur-creds --from-literal=ho
|
|||
!!! Note
|
||||
`conjur-creds` is the `name` defined in the `userRef` and `apikeyRef` fields of the `conjur-secret-store.yml` file.
|
||||
|
||||
### External secret store with JWT authentication
|
||||
|
||||
##### Step 3: Create the external secrets store
|
||||
|
||||
!!! Important
|
||||
Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
|
||||
|
||||
```shell
|
||||
# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
|
||||
#
|
||||
kubectl apply -n external-secrets -f conjur-secret-store.yaml
|
||||
|
||||
# WARNING: running the delete command will delete the secret store configuration
|
||||
#
|
||||
# If there is a need to delete the external secretstore
|
||||
# kubectl delete secretstore -n external-secrets conjur
|
||||
```
|
||||
|
||||
#### Option 2: External secret store with JWT authentication
|
||||
|
||||
This method uses JWT tokens to authenticate with Conjur. You can use the following methods to retrieve a JWT token for authentication:
|
||||
|
||||
- JWT token from a referenced Kubernetes service account
|
||||
- JWT token stored in a Kubernetes secret
|
||||
* JWT token from a referenced Kubernetes service account
|
||||
* JWT token stored in a Kubernetes secret
|
||||
|
||||
#### Step 1: Define an external secret store
|
||||
##### Step 1: Define an external secret store
|
||||
|
||||
When you use JWT authentication, the following must be specified in the `SecretStore`:
|
||||
|
||||
- `account` - The name of the Conjur account
|
||||
- `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that is used to authenticate the JWT token
|
||||
* `account` - The name of the Conjur account
|
||||
* `serviceId` - The ID of the JWT Authenticator `WebService` configured in Conjur that is used to authenticate the JWT token
|
||||
|
||||
You can retrieve the JWT token from either a referenced service account or a Kubernetes secret.
|
||||
|
||||
|
@ -93,20 +117,9 @@ You can use an external JWT issuer or the Kubernetes API server to create the to
|
|||
kubectl create token my-service-account --audience='https://conjur.company.com' --duration=3600s
|
||||
```
|
||||
|
||||
Save the secret store file as `conjur-secret-store.yaml` (the filename used in subsequent steps).
|
||||
Save the secret store file as `conjur-secret-store.yaml`.
|
||||
|
||||
#### Step 2: Define an external secret
|
||||
|
||||
Save the external secret file as: `conjur-external-secret.yaml`
|
||||
|
||||
```yaml
|
||||
{% include 'conjur-external-secret.yaml' %}
|
||||
```
|
||||
|
||||
!!!Important
|
||||
Unless you are using a [ClusterSecretStore](../api/clustersecretstore.md), credentials must reside in the same namespace as the SecretStore.
|
||||
|
||||
#### Step 3: Create the external secrets store
|
||||
##### Step 2: Create the external secrets store
|
||||
|
||||
```shell
|
||||
# WARNING: creates the store in the "external-secrets" namespace, update the value as needed
|
||||
|
@ -119,7 +132,32 @@ kubectl apply -n external-secrets -f conjur-secret-store.yaml
|
|||
# kubectl delete secretstore -n external-secrets conjur
|
||||
```
|
||||
|
||||
#### Step 4: Create the external secret
|
||||
### Define an external secret
|
||||
|
||||
After you have configured the Conjur provider secret store, you can fetch secrets from Conjur.
|
||||
|
||||
Here is an example of how to fetch a single secret from Conjur:
|
||||
|
||||
```yaml
|
||||
{% include 'conjur-external-secret.yaml' %}
|
||||
```
|
||||
|
||||
Save the external secret file as `conjur-external-secret.yaml`.
|
||||
|
||||
#### Find by Name and Find by Tag
|
||||
|
||||
The Conjur provider also supports the Find by Name and Find by Tag ESO features. This means that
|
||||
you can use a regular expression or tags to dynamically fetch multiple secrets from Conjur.
|
||||
|
||||
```yaml
|
||||
{% include 'conjur-external-secret-find.yaml' %}
|
||||
```
|
||||
|
||||
If you use these features, we strongly recommend that you limit the permissions of the Conjur host
|
||||
to only the secrets that it needs to access. This is more secure and it reduces the load on
|
||||
both the Conjur server and ESO.
|
||||
|
||||
### Create the external secret
|
||||
|
||||
```shell
|
||||
# WARNING: creates the external-secret in the "external-secrets" namespace, update the value as needed
|
||||
|
@ -132,7 +170,7 @@ kubectl apply -n external-secrets -f conjur-external-secret.yaml
|
|||
# kubectl delete externalsecret -n external-secrets conjur
|
||||
```
|
||||
|
||||
#### Step 5: Get the K8s secret
|
||||
### Get the K8s secret
|
||||
|
||||
* Log in to your Conjur server and verify that your secret exists
|
||||
* Review the value of your Kubernetes secret to verify that it contains the same value as the Conjur server
|
||||
|
@ -149,10 +187,9 @@ kubectl get secret -n external-secrets conjur -o jsonpath="{.data.secret00}" |
|
|||
* [Accelerator-K8s-External-Secrets repo](https://github.com/conjurdemos/Accelerator-K8s-External-Secrets)
|
||||
* [Configure Conjur JWT authentication](https://docs.cyberark.com/conjur-open-source/Latest/en/Content/Operations/Services/cjr-authn-jwt-guidelines.htm)
|
||||
|
||||
|
||||
### License
|
||||
|
||||
Copyright (c) 2023 CyberArk Software Ltd. All rights reserved.
|
||||
Copyright (c) 2023-2024 CyberArk Software Ltd. All rights reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
22
docs/snippets/conjur-external-secret-find.yaml
Normal file
22
docs/snippets/conjur-external-secret-find.yaml
Normal file
|
@ -0,0 +1,22 @@
|
|||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: conjur-find-by-name
|
||||
spec:
|
||||
refreshInterval: 10s
|
||||
secretStoreRef:
|
||||
# This name must match the metadata.name in the `SecretStore`
|
||||
name: conjur
|
||||
kind: SecretStore
|
||||
target:
|
||||
name: k8s-secret-to-be-created
|
||||
dataFrom:
|
||||
- find:
|
||||
# You can use *either* `name` or `tags` to filter the secrets. Here are basic examples of both:
|
||||
name:
|
||||
# Match all secrets in the app1 namespace (e.g., `app1/secret00`, `app1/secret01`, etc.)
|
||||
regexp: "^app1\/.+$"
|
||||
tags:
|
||||
# Only fetch Conjur secrets with the following annotations
|
||||
environment: "prod"
|
||||
application: "app1"
|
|
@ -1,8 +1,6 @@
|
|||
module github.com/external-secrets/external-secrets-e2e
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.1
|
||||
go 1.22.1
|
||||
|
||||
replace github.com/external-secrets/external-secrets => ../
|
||||
|
||||
|
|
|
@ -32,20 +32,40 @@ var _ = Describe("[conjur]", Label("conjur"), func() {
|
|||
|
||||
DescribeTable("sync secrets",
|
||||
framework.TableFuncWithExternalSecret(f, prov),
|
||||
// uses token auth
|
||||
// use api key auth
|
||||
framework.Compose(withTokenAuth, f, common.FindByName, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.FindByNameAndRewrite, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.FindByTag, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.SimpleDataSync, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.SyncWithoutTargetName, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataFromSync, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataFromRewrite, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataWithProperty, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataWithTemplate, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.DataPropertyDockerconfigJSON, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.JSONDataWithTemplateFromLiteral, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.TemplateFromConfigmaps, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.SSHKeySync, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.SSHKeySyncDataProperty, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.DockerJSONConfig, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.NestedJSONWithGJSON, useApiKeyAuth),
|
||||
framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useApiKeyAuth),
|
||||
|
||||
// use jwt k8s provider
|
||||
framework.Compose(withJWTK8s, f, common.FindByName, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.FindByNameAndRewrite, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.FindByTag, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.SimpleDataSync, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.SyncWithoutTargetName, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.JSONDataFromSync, useJWTK8sProvider),
|
||||
framework.Compose(withJWTK8s, f, common.JSONDataFromRewrite, useJWTK8sProvider),
|
||||
|
||||
// use jwt k8s hostid provider
|
||||
framework.Compose(withJWTK8sHostID, f, common.FindByName, useJWTK8sHostIDProvider),
|
||||
framework.Compose(withJWTK8sHostID, f, common.FindByNameAndRewrite, useJWTK8sHostIDProvider),
|
||||
framework.Compose(withJWTK8sHostID, f, common.FindByTag, useJWTK8sHostIDProvider),
|
||||
framework.Compose(withJWTK8sHostID, f, common.SimpleDataSync, useJWTK8sHostIDProvider),
|
||||
framework.Compose(withJWTK8sHostID, f, common.SyncWithoutTargetName, useJWTK8sHostIDProvider),
|
||||
framework.Compose(withJWTK8sHostID, f, common.JSONDataFromSync, useJWTK8sHostIDProvider),
|
||||
|
|
|
@ -20,6 +20,12 @@ import (
|
|||
|
||||
const createVariablePolicyTemplate = `- !variable
|
||||
id: {{ .Key }}
|
||||
{{ if .Tags }}
|
||||
annotations:
|
||||
{{- range $key, $value := .Tags }}
|
||||
{{ $key }}: "{{ $value }}"
|
||||
{{- end }}
|
||||
{{ end }}
|
||||
|
||||
- !permit
|
||||
role: !host system:serviceaccount:{{ .Namespace }}:test-app-sa
|
||||
|
@ -44,27 +50,28 @@ const jwtHostPolicyTemplate = `- !host
|
|||
privilege: [ read, authenticate ]
|
||||
resource: !webservice conjur/authn-jwt/{{ .ServiceID }}`
|
||||
|
||||
func createVariablePolicy(key, namespace string) string {
|
||||
return renderTemplate(createVariablePolicyTemplate, map[string]string{
|
||||
func createVariablePolicy(key, namespace string, tags map[string]string) string {
|
||||
return renderTemplate(createVariablePolicyTemplate, map[string]interface{}{
|
||||
"Key": key,
|
||||
"Namespace": namespace,
|
||||
"Tags": tags,
|
||||
})
|
||||
}
|
||||
|
||||
func deleteVariablePolicy(key string) string {
|
||||
return renderTemplate(deleteVariablePolicyTemplate, map[string]string{
|
||||
return renderTemplate(deleteVariablePolicyTemplate, map[string]interface{}{
|
||||
"Key": key,
|
||||
})
|
||||
}
|
||||
|
||||
func createJwtHostPolicy(hostID, serviceID string) string {
|
||||
return renderTemplate(jwtHostPolicyTemplate, map[string]string{
|
||||
return renderTemplate(jwtHostPolicyTemplate, map[string]interface{}{
|
||||
"HostID": hostID,
|
||||
"ServiceID": serviceID,
|
||||
})
|
||||
}
|
||||
|
||||
func renderTemplate(templateText string, data map[string]string) string {
|
||||
func renderTemplate(templateText string, data map[string]interface{}) string {
|
||||
// Use golang templates to render the policy
|
||||
tmpl, err := template.New("policy").Parse(templateText)
|
||||
if err != nil {
|
||||
|
|
|
@ -49,13 +49,12 @@ func newConjurProvider(f *framework.Framework) *conjurProvider {
|
|||
framework: f,
|
||||
}
|
||||
BeforeEach(prov.BeforeEach)
|
||||
AfterEach(prov.AfterEach)
|
||||
return prov
|
||||
}
|
||||
|
||||
func (s *conjurProvider) CreateSecret(key string, val framework.SecretEntry) {
|
||||
// Generate a policy file for the secret key
|
||||
policy := createVariablePolicy(key, s.framework.Namespace.Name)
|
||||
policy := createVariablePolicy(key, s.framework.Namespace.Name, val.Tags)
|
||||
|
||||
_, err := s.client.LoadPolicy(conjurapi.PolicyModePost, "root", strings.NewReader(policy))
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
@ -84,31 +83,6 @@ func (s *conjurProvider) BeforeEach() {
|
|||
s.CreateJWTK8sHostIDStore(c, ns)
|
||||
}
|
||||
|
||||
func (s *conjurProvider) AfterEach() {
|
||||
// Print Conjur logs if the test failed
|
||||
if !CurrentGinkgoTestDescription().Failed {
|
||||
return
|
||||
}
|
||||
|
||||
// Get logs from Conjur pod
|
||||
ns := s.framework.Namespace.Name
|
||||
pods, err := s.framework.KubeClientSet.CoreV1().Pods(ns).List(context.Background(), metav1.ListOptions{})
|
||||
if err != nil {
|
||||
GinkgoWriter.Printf("Error getting pods: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, pod := range pods.Items {
|
||||
if strings.Contains(pod.Name, "conjur-oss") {
|
||||
logs, err := s.framework.KubeClientSet.CoreV1().Pods(ns).GetLogs(pod.Name, &v1.PodLogOptions{Container: "conjur-oss"}).DoRaw(context.Background())
|
||||
if err != nil {
|
||||
GinkgoWriter.Printf("Error getting logs from Conjur pod: %s\n", err)
|
||||
}
|
||||
GinkgoWriter.Printf("Conjur logs:\n%s\n", logs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeStore(name, ns string, c *addon.Conjur) *esv1beta1.SecretStore {
|
||||
return &esv1beta1.SecretStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
|
2
go.mod
2
go.mod
|
@ -1,6 +1,6 @@
|
|||
module github.com/external-secrets/external-secrets
|
||||
|
||||
go 1.22
|
||||
go 1.22.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/iam v1.1.7
|
||||
|
|
|
@ -34,10 +34,10 @@ import (
|
|||
const JwtLifespan = 600 // 10 minutes
|
||||
|
||||
// getJWTToken retrieves a JWT token either using the TokenRequest API for a specified service account, or from a jwt stored in a k8s secret.
|
||||
func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
|
||||
func (c *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.ConjurJWT) (string, error) {
|
||||
if conjurJWTConfig.ServiceAccountRef != nil {
|
||||
// Should work for Kubernetes >=v1.22: fetch token via TokenRequest API
|
||||
jwtToken, err := p.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
|
||||
jwtToken, err := c.getJwtFromServiceAccountTokenRequest(ctx, *conjurJWTConfig.ServiceAccountRef, nil, JwtLifespan)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -50,9 +50,9 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
|
|||
}
|
||||
jwtToken, err := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
p.kube,
|
||||
p.StoreKind,
|
||||
p.namespace,
|
||||
c.kube,
|
||||
c.StoreKind,
|
||||
c.namespace,
|
||||
tokenRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
@ -63,25 +63,25 @@ func (p *Client) getJWTToken(ctx context.Context, conjurJWTConfig *esv1beta1.Con
|
|||
}
|
||||
|
||||
// getJwtFromServiceAccountTokenRequest uses the TokenRequest API to get a JWT token for the given service account.
|
||||
func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
|
||||
func (c *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, serviceAccountRef esmeta.ServiceAccountSelector, additionalAud []string, expirationSeconds int64) (string, error) {
|
||||
audiences := serviceAccountRef.Audiences
|
||||
if len(additionalAud) > 0 {
|
||||
audiences = append(audiences, additionalAud...)
|
||||
}
|
||||
tokenRequest := &authenticationv1.TokenRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: p.namespace,
|
||||
Namespace: c.namespace,
|
||||
},
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: audiences,
|
||||
ExpirationSeconds: &expirationSeconds,
|
||||
},
|
||||
}
|
||||
if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
|
||||
if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
|
||||
(serviceAccountRef.Namespace != nil) {
|
||||
tokenRequest.Namespace = *serviceAccountRef.Namespace
|
||||
}
|
||||
tokenResponse, err := p.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
|
||||
tokenResponse, err := c.corev1.ServiceAccounts(tokenRequest.Namespace).CreateToken(ctx, serviceAccountRef.Name, tokenRequest, metav1.CreateOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(errGetKubeSATokenRequest, serviceAccountRef.Name, err)
|
||||
}
|
||||
|
@ -89,13 +89,13 @@ func (p *Client) getJwtFromServiceAccountTokenRequest(ctx context.Context, servi
|
|||
}
|
||||
|
||||
// newClientFromJwt creates a new Conjur client using the given JWT Auth Config.
|
||||
func (p *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
|
||||
jwtToken, getJWTError := p.getJWTToken(ctx, jwtAuth)
|
||||
func (c *Client) newClientFromJwt(ctx context.Context, config conjurapi.Config, jwtAuth *esv1beta1.ConjurJWT) (SecretsClient, error) {
|
||||
jwtToken, getJWTError := c.getJWTToken(ctx, jwtAuth)
|
||||
if getJWTError != nil {
|
||||
return nil, getJWTError
|
||||
}
|
||||
|
||||
client, clientError := p.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
|
||||
client, clientError := c.clientAPI.NewClientFromJWT(config, jwtToken, jwtAuth.ServiceID, jwtAuth.HostID)
|
||||
if clientError != nil {
|
||||
return nil, clientError
|
||||
}
|
||||
|
|
219
pkg/provider/conjur/client.go
Normal file
219
pkg/provider/conjur/client.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
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 conjur
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cyberark/conjur-api-go/conjurapi"
|
||||
"github.com/cyberark/conjur-api-go/conjurapi/authn"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
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/conjur/util"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
||||
)
|
||||
|
||||
var (
|
||||
errConjurClient = "cannot setup new Conjur client: %w"
|
||||
errBadCertBundle = "caBundle failed to base64 decode: %w"
|
||||
errBadServiceUser = "could not get Auth.Apikey.UserRef: %w"
|
||||
errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
|
||||
|
||||
errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
|
||||
|
||||
errUnableToFetchCAProviderCM = "unable to fetch Server.CAProvider ConfigMap: %w"
|
||||
errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
|
||||
|
||||
errSecretKeyFmt = "cannot find secret data for key: %q"
|
||||
)
|
||||
|
||||
// Client is a provider for Conjur.
|
||||
type Client struct {
|
||||
StoreKind string
|
||||
kube client.Client
|
||||
store esv1beta1.GenericStore
|
||||
namespace string
|
||||
corev1 typedcorev1.CoreV1Interface
|
||||
clientAPI SecretsClientFactory
|
||||
client SecretsClient
|
||||
}
|
||||
|
||||
func (c *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
|
||||
// if the client is initialized already, return it
|
||||
if c.client != nil {
|
||||
return c.client, nil
|
||||
}
|
||||
|
||||
prov, err := util.GetConjurProvider(c.store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, getCertErr := c.getCA(ctx, prov)
|
||||
if getCertErr != nil {
|
||||
return nil, getCertErr
|
||||
}
|
||||
|
||||
config := conjurapi.Config{
|
||||
ApplianceURL: prov.URL,
|
||||
SSLCert: cert,
|
||||
}
|
||||
|
||||
if prov.Auth.APIKey != nil {
|
||||
config.Account = prov.Auth.APIKey.Account
|
||||
conjUser, secErr := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
c.kube,
|
||||
c.StoreKind,
|
||||
c.namespace, prov.Auth.APIKey.UserRef)
|
||||
if secErr != nil {
|
||||
return nil, fmt.Errorf(errBadServiceUser, secErr)
|
||||
}
|
||||
conjAPIKey, secErr := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
c.kube,
|
||||
c.StoreKind,
|
||||
c.namespace,
|
||||
prov.Auth.APIKey.APIKeyRef)
|
||||
if secErr != nil {
|
||||
return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
|
||||
}
|
||||
|
||||
conjur, newClientFromKeyError := c.clientAPI.NewClientFromKey(config,
|
||||
authn.LoginPair{
|
||||
Login: conjUser,
|
||||
APIKey: conjAPIKey,
|
||||
},
|
||||
)
|
||||
|
||||
if newClientFromKeyError != nil {
|
||||
return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
|
||||
}
|
||||
c.client = conjur
|
||||
return conjur, nil
|
||||
} else if prov.Auth.Jwt != nil {
|
||||
config.Account = prov.Auth.Jwt.Account
|
||||
|
||||
conjur, clientFromJwtError := c.newClientFromJwt(ctx, config, prov.Auth.Jwt)
|
||||
if clientFromJwtError != nil {
|
||||
return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
|
||||
}
|
||||
|
||||
c.client = conjur
|
||||
|
||||
return conjur, nil
|
||||
} else {
|
||||
// Should not happen because validate func should catch this
|
||||
return nil, fmt.Errorf("no authentication method provided")
|
||||
}
|
||||
}
|
||||
|
||||
// PushSecret will write a single secret into the provider.
|
||||
func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
|
||||
// NOT IMPLEMENTED
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
|
||||
// NOT IMPLEMENTED
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
|
||||
return false, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// Validate validates the provider.
|
||||
func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
|
||||
return esv1beta1.ValidationResultReady, nil
|
||||
}
|
||||
|
||||
// Close closes the provider.
|
||||
func (c *Client) Close(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// configMapKeyRef returns the value of a key in a ConfigMap.
|
||||
func (c *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
|
||||
configMap := &corev1.ConfigMap{}
|
||||
ref := client.ObjectKey{
|
||||
Namespace: c.namespace,
|
||||
Name: cmRef.Name,
|
||||
}
|
||||
if (c.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
|
||||
(cmRef.Namespace != nil) {
|
||||
ref.Namespace = *cmRef.Namespace
|
||||
}
|
||||
err := c.kube.Get(ctx, ref, configMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyBytes, ok := configMap.Data[cmRef.Key]
|
||||
if !ok {
|
||||
return "", err
|
||||
}
|
||||
|
||||
valueStr := strings.TrimSpace(keyBytes)
|
||||
return valueStr, nil
|
||||
}
|
||||
|
||||
// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
|
||||
func (c *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
|
||||
if provider.CAProvider != nil {
|
||||
var ca string
|
||||
var err error
|
||||
switch provider.CAProvider.Type {
|
||||
case esv1beta1.CAProviderTypeConfigMap:
|
||||
keySelector := esmeta.SecretKeySelector{
|
||||
Name: provider.CAProvider.Name,
|
||||
Namespace: provider.CAProvider.Namespace,
|
||||
Key: provider.CAProvider.Key,
|
||||
}
|
||||
ca, err = c.configMapKeyRef(ctx, &keySelector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
|
||||
}
|
||||
case esv1beta1.CAProviderTypeSecret:
|
||||
keySelector := esmeta.SecretKeySelector{
|
||||
Name: provider.CAProvider.Name,
|
||||
Namespace: provider.CAProvider.Namespace,
|
||||
Key: provider.CAProvider.Key,
|
||||
}
|
||||
ca, err = resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
c.kube,
|
||||
c.StoreKind,
|
||||
c.namespace,
|
||||
&keySelector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
|
||||
}
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
|
||||
if decodeErr != nil {
|
||||
return "", fmt.Errorf(errBadCertBundle, decodeErr)
|
||||
}
|
||||
return string(certBytes), nil
|
||||
}
|
223
pkg/provider/conjur/client_get.go
Normal file
223
pkg/provider/conjur/client_get.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
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 conjur
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cyberark/conjur-api-go/conjurapi"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
"github.com/external-secrets/external-secrets/pkg/find"
|
||||
)
|
||||
|
||||
type conjurResource map[string]interface{}
|
||||
|
||||
// resourceFilterFunc is a function that filters resources.
|
||||
// It takes a resource as input and returns the name of the resource if it should be included.
|
||||
// If the resource should not be included, it returns an empty string.
|
||||
// If an error occurs, it returns an empty string and the error.
|
||||
type resourceFilterFunc func(candidate conjurResource) (name string, err error)
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (c *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
|
||||
if getConjurClientError != nil {
|
||||
return nil, getConjurClientError
|
||||
}
|
||||
secretValue, err := conjurClient.RetrieveSecret(ref.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If no property is specified, return the secret value as is
|
||||
if ref.Property == "" {
|
||||
return secretValue, nil
|
||||
}
|
||||
|
||||
// If a property is specified, parse the secret value as JSON and return the property value
|
||||
val := gjson.Get(string(secretValue), ref.Property)
|
||||
if !val.Exists() {
|
||||
return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
|
||||
}
|
||||
return []byte(val.String()), nil
|
||||
}
|
||||
|
||||
// GetSecretMap returns multiple k/v pairs from the provider.
|
||||
func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
// Gets a secret as normal, expecting secret value to be a json object
|
||||
data, err := c.GetSecret(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
|
||||
}
|
||||
|
||||
// Maps the json data to a string:string map
|
||||
kv := make(map[string]string)
|
||||
err = json.Unmarshal(data, &kv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
|
||||
}
|
||||
|
||||
// Converts values in K:V pairs into bytes, while leaving keys as strings
|
||||
secretData := make(map[string][]byte)
|
||||
for k, v := range kv {
|
||||
secretData[k] = []byte(v)
|
||||
}
|
||||
return secretData, nil
|
||||
}
|
||||
|
||||
// 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 (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
|
||||
if ref.Name != nil {
|
||||
return c.findSecretsFromName(ctx, *ref.Name)
|
||||
}
|
||||
return c.findSecretsFromTags(ctx, ref.Tags)
|
||||
}
|
||||
|
||||
func (c *Client) findSecretsFromName(ctx context.Context, ref esv1beta1.FindName) (map[string][]byte, error) {
|
||||
matcher, err := find.New(ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
|
||||
name := trimConjurResourceName(candidate["id"].(string))
|
||||
isMatch := matcher.MatchName(name)
|
||||
if !isMatch {
|
||||
return "", nil
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return c.listSecrets(ctx, resourceFilterFunc)
|
||||
}
|
||||
|
||||
func (c *Client) findSecretsFromTags(ctx context.Context, tags map[string]string) (map[string][]byte, error) {
|
||||
var resourceFilterFunc = func(candidate conjurResource) (string, error) {
|
||||
name := trimConjurResourceName(candidate["id"].(string))
|
||||
annotations, ok := candidate["annotations"].([]interface{})
|
||||
if !ok {
|
||||
// No annotations, skip
|
||||
return "", nil
|
||||
}
|
||||
|
||||
formattedAnnotations, err := formatAnnotations(annotations)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Check if all tags match
|
||||
for tk, tv := range tags {
|
||||
p, ok := formattedAnnotations[tk]
|
||||
if !ok || p != tv {
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
return c.listSecrets(ctx, resourceFilterFunc)
|
||||
}
|
||||
|
||||
func (c *Client) listSecrets(ctx context.Context, filterFunc resourceFilterFunc) (map[string][]byte, error) {
|
||||
conjurClient, getConjurClientError := c.GetConjurClient(ctx)
|
||||
if getConjurClientError != nil {
|
||||
return nil, getConjurClientError
|
||||
}
|
||||
|
||||
filteredResourceNames := []string{}
|
||||
|
||||
// Loop through all secrets in the Conjur account.
|
||||
// Ideally this will be only a small list, but we need to handle pagination in the
|
||||
// case that there are a lot of secrets. To limit load on Conjur and memory usage
|
||||
// in ESO, we will only load 100 secrets at a time. We will then filter these secrets,
|
||||
// discarding any that do not match the filterFunc. We will then repeat this process
|
||||
// until we have loaded all secrets.
|
||||
for offset := 0; ; offset += 100 {
|
||||
resFilter := &conjurapi.ResourceFilter{
|
||||
Kind: "variable",
|
||||
Limit: 100,
|
||||
Offset: offset,
|
||||
}
|
||||
resources, err := conjurClient.Resources(resFilter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, candidate := range resources {
|
||||
name, err := filterFunc(candidate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name != "" {
|
||||
filteredResourceNames = append(filteredResourceNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have less than 100 resources, we reached the last page
|
||||
if len(resources) < 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources, err := c.client.RetrieveBatchSecrets(filteredResourceNames)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Trim the resource names to just the last part of the ID
|
||||
return trimConjurResourceNames(filteredResources), nil
|
||||
}
|
||||
|
||||
// trimConjurResourceNames trims the Conjur resource names to the last part of the ID.
|
||||
// It iterates over a map of secrets and returns a new map with the trimmed names.
|
||||
func trimConjurResourceNames(resources map[string][]byte) map[string][]byte {
|
||||
trimmedResources := make(map[string][]byte)
|
||||
for k, v := range resources {
|
||||
trimmedResources[trimConjurResourceName(k)] = v
|
||||
}
|
||||
return trimmedResources
|
||||
}
|
||||
|
||||
// trimConjurResourceName trims the Conjur resource name to the last part of the ID.
|
||||
// For example, if the ID is "account:variable:secret", the function will return
|
||||
// "secret".
|
||||
func trimConjurResourceName(id string) string {
|
||||
tokens := strings.SplitN(id, ":", 3)
|
||||
return tokens[len(tokens)-1]
|
||||
}
|
||||
|
||||
// Convert annotations from objects with "name", "policy", "value" keys (as returned by the Conjur API)
|
||||
// to a key/value map for easier comparison in code.
|
||||
func formatAnnotations(annotations []interface{}) (map[string]string, error) {
|
||||
formattedAnnotations := make(map[string]string)
|
||||
for _, annot := range annotations {
|
||||
annot, ok := annot.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not parse annotation: %v", annot)
|
||||
}
|
||||
name := annot["name"].(string)
|
||||
value := annot["value"].(string)
|
||||
formattedAnnotations[name] = value
|
||||
}
|
||||
return formattedAnnotations, nil
|
||||
}
|
|
@ -29,6 +29,8 @@ import (
|
|||
// SecretsClient is an interface for the Conjur client.
|
||||
type SecretsClient interface {
|
||||
RetrieveSecret(secret string) (result []byte, err error)
|
||||
RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error)
|
||||
Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error)
|
||||
}
|
||||
|
||||
// SecretsClientFactory is an interface for creating a Conjur client.
|
||||
|
|
|
@ -16,6 +16,10 @@ package fake
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/cyberark/conjur-api-go/conjurapi"
|
||||
)
|
||||
|
||||
type ConjurMockClient struct {
|
||||
|
@ -26,5 +30,95 @@ func (mc *ConjurMockClient) RetrieveSecret(secret string) (result []byte, err er
|
|||
err = errors.New("error")
|
||||
return nil, err
|
||||
}
|
||||
if secret == "json_map" {
|
||||
return []byte(`{"key1":"value1","key2":"value2"}`), nil
|
||||
}
|
||||
if secret == "json_nested" {
|
||||
return []byte(`{"key1":"value1","key2":{"key3":"value3","key4":"value4"}}`), nil
|
||||
}
|
||||
return []byte("secret"), nil
|
||||
}
|
||||
|
||||
func (mc *ConjurMockClient) RetrieveBatchSecrets(variableIDs []string) (map[string][]byte, error) {
|
||||
secrets := make(map[string][]byte)
|
||||
for _, id := range variableIDs {
|
||||
if id == "error" {
|
||||
return nil, errors.New("error")
|
||||
}
|
||||
fullID := fmt.Sprintf("conjur:variable:%s", id)
|
||||
secrets[fullID] = []byte("secret")
|
||||
}
|
||||
return secrets, nil
|
||||
}
|
||||
|
||||
func (mc *ConjurMockClient) Resources(filter *conjurapi.ResourceFilter) (resources []map[string]interface{}, err error) {
|
||||
policyID := "conjur:policy:root"
|
||||
if filter.Offset == 0 {
|
||||
// First "page" of secrets: 2 static ones and 98 random ones
|
||||
secrets := []map[string]interface{}{
|
||||
{
|
||||
"id": "conjur:variable:secret1",
|
||||
"annotations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "conjur/kind",
|
||||
"value": "dummy",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "conjur:variable:secret2",
|
||||
"owner": "conjur:policy:admin1",
|
||||
"annotations": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "Description",
|
||||
"policy": policyID,
|
||||
"value": "Lorem ipsum dolor sit amet",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "conjur/kind",
|
||||
"policy": policyID,
|
||||
"value": "password",
|
||||
},
|
||||
},
|
||||
"permissions": map[string]string{
|
||||
"policy": policyID,
|
||||
"privilege": "update",
|
||||
"role": "conjur:group:admins",
|
||||
},
|
||||
"policy": policyID,
|
||||
},
|
||||
}
|
||||
// Add 98 random secrets so we can simulate a full "page" of 100 secrets
|
||||
secrets = append(secrets, generateRandomSecrets(98)...)
|
||||
return secrets, nil
|
||||
} else if filter.Offset == 100 {
|
||||
// Second "page" of secrets: 100 random ones
|
||||
return generateRandomSecrets(100), nil
|
||||
}
|
||||
|
||||
// Add 50 random secrets so we can simulate a partial "page" of 50 secrets
|
||||
return generateRandomSecrets(50), nil
|
||||
}
|
||||
|
||||
func generateRandomSecrets(count int) []map[string]interface{} {
|
||||
var secrets []map[string]interface{}
|
||||
for i := 0; i < count; i++ {
|
||||
//nolint:gosec
|
||||
randomNumber := rand.Intn(10000)
|
||||
secrets = append(secrets, generateRandomSecret(randomNumber))
|
||||
}
|
||||
return secrets
|
||||
}
|
||||
|
||||
func generateRandomSecret(num int) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": fmt.Sprintf("conjur:variable:random/var_%d", num),
|
||||
"annotations": []map[string]interface{}{
|
||||
{
|
||||
"name": "random_number",
|
||||
"value": fmt.Sprintf("%d", num),
|
||||
},
|
||||
},
|
||||
"policy": "conjur:policy:random",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,55 +17,21 @@ package conjur
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cyberark/conjur-api-go/conjurapi"
|
||||
"github.com/cyberark/conjur-api-go/conjurapi/authn"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
ctrlcfg "sigs.k8s.io/controller-runtime/pkg/client/config"
|
||||
"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/conjur/util"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
|
||||
)
|
||||
|
||||
var (
|
||||
errConjurClient = "cannot setup new Conjur client: %w"
|
||||
errBadCertBundle = "caBundle failed to base64 decode: %w"
|
||||
errBadServiceUser = "could not get Auth.Apikey.UserRef: %w"
|
||||
errBadServiceAPIKey = "could not get Auth.Apikey.ApiKeyRef: %w"
|
||||
|
||||
errGetKubeSATokenRequest = "cannot request Kubernetes service account token for service account %q: %w"
|
||||
|
||||
errUnableToFetchCAProviderCM = "unable to fetch Server.CAProvider ConfigMap: %w"
|
||||
errUnableToFetchCAProviderSecret = "unable to fetch Server.CAProvider Secret: %w"
|
||||
)
|
||||
|
||||
// Client is a provider for Conjur.
|
||||
type Client struct {
|
||||
StoreKind string
|
||||
kube client.Client
|
||||
store esv1beta1.GenericStore
|
||||
namespace string
|
||||
corev1 typedcorev1.CoreV1Interface
|
||||
clientAPI SecretsClientFactory
|
||||
client SecretsClient
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
NewConjurProvider func(context context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientApi SecretsClientFactory) (esv1beta1.SecretsClient, error)
|
||||
}
|
||||
|
||||
// NewClient creates a new Conjur client.
|
||||
func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
|
||||
func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string) (esv1beta1.SecretsClient, error) {
|
||||
// controller-runtime/client does not support TokenRequest or other subresource APIs
|
||||
// so we need to construct our own client and use it to create a TokenRequest
|
||||
restCfg, err := ctrlcfg.GetConfig()
|
||||
|
@ -77,7 +43,12 @@ func (c *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore,
|
|||
return nil, err
|
||||
}
|
||||
|
||||
return c.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
|
||||
return p.NewConjurProvider(ctx, store, kube, namespace, clientset.CoreV1(), &ClientAPIImpl{})
|
||||
}
|
||||
|
||||
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
|
||||
func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
|
||||
return esv1beta1.SecretStoreReadOnly
|
||||
}
|
||||
|
||||
func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube client.Client, namespace string, corev1 typedcorev1.CoreV1Interface, clientAPI SecretsClientFactory) (esv1beta1.SecretsClient, error) {
|
||||
|
@ -91,274 +62,6 @@ func newConjurProvider(_ context.Context, store esv1beta1.GenericStore, kube cli
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (p *Client) GetConjurClient(ctx context.Context) (SecretsClient, error) {
|
||||
// if the client is initialized already, return it
|
||||
if p.client != nil {
|
||||
return p.client, nil
|
||||
}
|
||||
|
||||
prov, err := util.GetConjurProvider(p.store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, getCertErr := p.getCA(ctx, prov)
|
||||
if getCertErr != nil {
|
||||
return nil, getCertErr
|
||||
}
|
||||
|
||||
config := conjurapi.Config{
|
||||
ApplianceURL: prov.URL,
|
||||
SSLCert: cert,
|
||||
}
|
||||
|
||||
if prov.Auth.APIKey != nil {
|
||||
config.Account = prov.Auth.APIKey.Account
|
||||
conjUser, secErr := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
p.kube,
|
||||
p.StoreKind,
|
||||
p.namespace, prov.Auth.APIKey.UserRef)
|
||||
if secErr != nil {
|
||||
return nil, fmt.Errorf(errBadServiceUser, secErr)
|
||||
}
|
||||
conjAPIKey, secErr := resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
p.kube,
|
||||
p.StoreKind,
|
||||
p.namespace,
|
||||
prov.Auth.APIKey.APIKeyRef)
|
||||
if secErr != nil {
|
||||
return nil, fmt.Errorf(errBadServiceAPIKey, secErr)
|
||||
}
|
||||
|
||||
conjur, newClientFromKeyError := p.clientAPI.NewClientFromKey(config,
|
||||
authn.LoginPair{
|
||||
Login: conjUser,
|
||||
APIKey: conjAPIKey,
|
||||
},
|
||||
)
|
||||
|
||||
if newClientFromKeyError != nil {
|
||||
return nil, fmt.Errorf(errConjurClient, newClientFromKeyError)
|
||||
}
|
||||
p.client = conjur
|
||||
return conjur, nil
|
||||
} else if prov.Auth.Jwt != nil {
|
||||
config.Account = prov.Auth.Jwt.Account
|
||||
|
||||
conjur, clientFromJwtError := p.newClientFromJwt(ctx, config, prov.Auth.Jwt)
|
||||
if clientFromJwtError != nil {
|
||||
return nil, fmt.Errorf(errConjurClient, clientFromJwtError)
|
||||
}
|
||||
|
||||
p.client = conjur
|
||||
|
||||
return conjur, nil
|
||||
} else {
|
||||
// Should not happen because validate func should catch this
|
||||
return nil, fmt.Errorf("no authentication method provided")
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllSecrets returns all secrets from the provider.
|
||||
// NOT IMPLEMENTED.
|
||||
func (p *Client) GetAllSecrets(_ context.Context, _ esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
|
||||
// TO be implemented
|
||||
return nil, fmt.Errorf("GetAllSecrets not implemented")
|
||||
}
|
||||
|
||||
// GetSecret returns a single secret from the provider.
|
||||
func (p *Client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
|
||||
conjurClient, getConjurClientError := p.GetConjurClient(ctx)
|
||||
if getConjurClientError != nil {
|
||||
return nil, getConjurClientError
|
||||
}
|
||||
secretValue, err := conjurClient.RetrieveSecret(ref.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretValue, nil
|
||||
}
|
||||
|
||||
// PushSecret will write a single secret into the provider.
|
||||
func (p *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
|
||||
// NOT IMPLEMENTED
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
|
||||
// NOT IMPLEMENTED
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Client) SecretExists(_ context.Context, _ esv1beta1.PushSecretRemoteRef) (bool, error) {
|
||||
return false, fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
// GetSecretMap returns multiple k/v pairs from the provider.
|
||||
func (p *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
|
||||
// Gets a secret as normal, expecting secret value to be a json object
|
||||
data, err := p.GetSecret(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting secret %s: %w", ref.Key, err)
|
||||
}
|
||||
|
||||
// Maps the json data to a string:string map
|
||||
kv := make(map[string]string)
|
||||
err = json.Unmarshal(data, &kv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to unmarshal secret %s: %w", ref.Key, err)
|
||||
}
|
||||
|
||||
// Converts values in K:V pairs into bytes, while leaving keys as strings
|
||||
secretData := make(map[string][]byte)
|
||||
for k, v := range kv {
|
||||
secretData[k] = []byte(v)
|
||||
}
|
||||
return secretData, nil
|
||||
}
|
||||
|
||||
// Close closes the provider.
|
||||
func (p *Client) Close(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the provider.
|
||||
func (p *Client) Validate() (esv1beta1.ValidationResult, error) {
|
||||
return esv1beta1.ValidationResultReady, nil
|
||||
}
|
||||
|
||||
// ValidateStore validates the store.
|
||||
func (c *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
|
||||
prov, err := util.GetConjurProvider(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prov.URL == "" {
|
||||
return nil, fmt.Errorf("conjur URL cannot be empty")
|
||||
}
|
||||
if prov.Auth.APIKey != nil {
|
||||
if prov.Auth.APIKey.Account == "" {
|
||||
return nil, fmt.Errorf("missing Auth.ApiKey.Account")
|
||||
}
|
||||
if prov.Auth.APIKey.UserRef == nil {
|
||||
return nil, fmt.Errorf("missing Auth.Apikey.UserRef")
|
||||
}
|
||||
if prov.Auth.APIKey.APIKeyRef == nil {
|
||||
return nil, fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
|
||||
}
|
||||
if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.UserRef); err != nil {
|
||||
return nil, fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
|
||||
}
|
||||
if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.APIKey.APIKeyRef); err != nil {
|
||||
return nil, fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if prov.Auth.Jwt != nil {
|
||||
if prov.Auth.Jwt.Account == "" {
|
||||
return nil, fmt.Errorf("missing Auth.Jwt.Account")
|
||||
}
|
||||
if prov.Auth.Jwt.ServiceID == "" {
|
||||
return nil, fmt.Errorf("missing Auth.Jwt.ServiceID")
|
||||
}
|
||||
if prov.Auth.Jwt.ServiceAccountRef == nil && prov.Auth.Jwt.SecretRef == nil {
|
||||
return nil, fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
|
||||
}
|
||||
if prov.Auth.Jwt.SecretRef != nil {
|
||||
if err := utils.ValidateReferentSecretSelector(store, *prov.Auth.Jwt.SecretRef); err != nil {
|
||||
return nil, fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
|
||||
}
|
||||
}
|
||||
if prov.Auth.Jwt.ServiceAccountRef != nil {
|
||||
if err := utils.ValidateReferentServiceAccountSelector(store, *prov.Auth.Jwt.ServiceAccountRef); err != nil {
|
||||
return nil, fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At least one auth must be configured
|
||||
if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
|
||||
return nil, fmt.Errorf("missing Auth.* configuration")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Capabilities returns the provider Capabilities (Read, Write, ReadWrite).
|
||||
func (c *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
|
||||
return esv1beta1.SecretStoreReadOnly
|
||||
}
|
||||
|
||||
// configMapKeyRef returns the value of a key in a ConfigMap.
|
||||
func (p *Client) configMapKeyRef(ctx context.Context, cmRef *esmeta.SecretKeySelector) (string, error) {
|
||||
configMap := &corev1.ConfigMap{}
|
||||
ref := client.ObjectKey{
|
||||
Namespace: p.namespace,
|
||||
Name: cmRef.Name,
|
||||
}
|
||||
if (p.StoreKind == esv1beta1.ClusterSecretStoreKind) &&
|
||||
(cmRef.Namespace != nil) {
|
||||
ref.Namespace = *cmRef.Namespace
|
||||
}
|
||||
err := p.kube.Get(ctx, ref, configMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
keyBytes, ok := configMap.Data[cmRef.Key]
|
||||
if !ok {
|
||||
return "", err
|
||||
}
|
||||
|
||||
valueStr := strings.TrimSpace(keyBytes)
|
||||
return valueStr, nil
|
||||
}
|
||||
|
||||
// getCA try retrieve the CA bundle from the provider CABundle or from the CAProvider.
|
||||
func (p *Client) getCA(ctx context.Context, provider *esv1beta1.ConjurProvider) (string, error) {
|
||||
if provider.CAProvider != nil {
|
||||
var ca string
|
||||
var err error
|
||||
switch provider.CAProvider.Type {
|
||||
case esv1beta1.CAProviderTypeConfigMap:
|
||||
keySelector := esmeta.SecretKeySelector{
|
||||
Name: provider.CAProvider.Name,
|
||||
Namespace: provider.CAProvider.Namespace,
|
||||
Key: provider.CAProvider.Key,
|
||||
}
|
||||
ca, err = p.configMapKeyRef(ctx, &keySelector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(errUnableToFetchCAProviderCM, err)
|
||||
}
|
||||
case esv1beta1.CAProviderTypeSecret:
|
||||
keySelector := esmeta.SecretKeySelector{
|
||||
Name: provider.CAProvider.Name,
|
||||
Namespace: provider.CAProvider.Namespace,
|
||||
Key: provider.CAProvider.Key,
|
||||
}
|
||||
ca, err = resolvers.SecretKeyRef(
|
||||
ctx,
|
||||
p.kube,
|
||||
p.StoreKind,
|
||||
p.namespace,
|
||||
&keySelector)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf(errUnableToFetchCAProviderSecret, err)
|
||||
}
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
certBytes, decodeErr := utils.Decode(esv1beta1.ExternalSecretDecodeBase64, []byte(provider.CABundle))
|
||||
if decodeErr != nil {
|
||||
return "", fmt.Errorf(errBadCertBundle, decodeErr)
|
||||
}
|
||||
return string(certBytes), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
esv1beta1.Register(&Provider{
|
||||
NewConjurProvider: newConjurProvider,
|
||||
|
|
|
@ -55,75 +55,17 @@ func makeValidRef(k string) *esv1beta1.ExternalSecretDataRemoteRef {
|
|||
}
|
||||
}
|
||||
|
||||
type ValidateStoreTestCase struct {
|
||||
store *esv1beta1.SecretStore
|
||||
err error
|
||||
}
|
||||
|
||||
func TestValidateStore(t *testing.T) {
|
||||
testCases := []ValidateStoreTestCase{
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
|
||||
err: fmt.Errorf("conjur URL cannot be empty"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
|
||||
err: fmt.Errorf("missing Auth.Apikey.UserRef"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
|
||||
err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
|
||||
err: fmt.Errorf("missing Auth.ApiKey.Account"),
|
||||
},
|
||||
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
|
||||
err: fmt.Errorf("missing Auth.Jwt.Account"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
|
||||
err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: fmt.Errorf("conjur URL cannot be empty"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
|
||||
},
|
||||
|
||||
{
|
||||
store: makeNoAuthSecretStore(svcURL),
|
||||
err: fmt.Errorf("missing Auth.* configuration"),
|
||||
},
|
||||
}
|
||||
c := Provider{}
|
||||
for _, tc := range testCases {
|
||||
_, err := c.ValidateStore(tc.store)
|
||||
if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
|
||||
t.Errorf("test failed! want %v, got %v", tc.err, err)
|
||||
} else if tc.err == nil && err != nil {
|
||||
t.Errorf("want nil got err %v", err)
|
||||
} else if tc.err != nil && err == nil {
|
||||
t.Errorf("want err %v got nil", tc.err)
|
||||
func makeValidFindRef(search string, tags map[string]string) *esv1beta1.ExternalSecretFind {
|
||||
var name *esv1beta1.FindName
|
||||
if search != "" {
|
||||
name = &esv1beta1.FindName{
|
||||
RegExp: search,
|
||||
}
|
||||
}
|
||||
return &esv1beta1.ExternalSecretFind{
|
||||
Name: name,
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecret(t *testing.T) {
|
||||
|
@ -264,6 +206,239 @@ func TestGetSecret(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetAllSecrets(t *testing.T) {
|
||||
type args struct {
|
||||
store esv1beta1.GenericStore
|
||||
kube kclient.Client
|
||||
corev1 typedcorev1.CoreV1Interface
|
||||
namespace string
|
||||
search string
|
||||
tags map[string]string
|
||||
}
|
||||
|
||||
type want struct {
|
||||
err error
|
||||
values map[string][]byte
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
reason string
|
||||
args args
|
||||
want want
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"SimpleSearchSingleResultSuccess": {
|
||||
reason: "Should search for secrets successfully using a simple string.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
search: "secret1",
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
values: map[string][]byte{
|
||||
"secret1": []byte("secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"RegexSearchMultipleResultsSuccess": {
|
||||
reason: "Should search for secrets successfully using a regex and return multiple results.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
search: "^secret[1,2]$",
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
values: map[string][]byte{
|
||||
"secret1": []byte("secret"),
|
||||
"secret2": []byte("secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"RegexSearchInvalidRegexFailure": {
|
||||
reason: "Should fail to search for secrets using an invalid regex.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
search: "^secret[1,2", // Missing `]`
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("could not compile find.name.regexp [%s]: %w", "^secret[1,2", fmt.Errorf("error parsing regexp: missing closing ]: `[1,2`")),
|
||||
values: nil,
|
||||
},
|
||||
},
|
||||
"SimpleSearchNoResultsSuccess": {
|
||||
reason: "Should search for secrets successfully using a simple string and return no results.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
search: "nonexistent",
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
values: map[string][]byte{},
|
||||
},
|
||||
},
|
||||
"TagSearchSingleResultSuccess": {
|
||||
reason: "Should search for secrets successfully using a tag.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
tags: map[string]string{
|
||||
"conjur/kind": "password",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
values: map[string][]byte{
|
||||
"secret2": []byte("secret"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTest := func(t *testing.T, _ string, tc testCase) {
|
||||
provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
|
||||
ref := makeValidFindRef(tc.args.search, tc.args.tags)
|
||||
secrets, err := provider.GetAllSecrets(context.Background(), *ref)
|
||||
if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.values, secrets); diff != "" {
|
||||
t.Errorf("\n%s\nconjur.GetAllSecrets(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
runTest(t, name, tc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSecretMap(t *testing.T) {
|
||||
type args struct {
|
||||
store esv1beta1.GenericStore
|
||||
kube kclient.Client
|
||||
corev1 typedcorev1.CoreV1Interface
|
||||
namespace string
|
||||
ref *esv1beta1.ExternalSecretDataRemoteRef
|
||||
}
|
||||
|
||||
type want struct {
|
||||
err error
|
||||
val map[string][]byte
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
reason string
|
||||
args args
|
||||
want want
|
||||
}
|
||||
|
||||
cases := map[string]testCase{
|
||||
"ReadJsonSecret": {
|
||||
reason: "Should read a JSON key value secret.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
ref: makeValidRef("json_map"),
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
val: map[string][]byte{
|
||||
"key1": []byte("value1"),
|
||||
"key2": []byte("value2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"ReadJsonSecretFailure": {
|
||||
reason: "Should fail to read a non JSON secret",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
ref: makeValidRef("secret1"),
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("%w", fmt.Errorf("unable to unmarshal secret secret1: invalid character 's' looking for beginning of value")),
|
||||
val: nil,
|
||||
},
|
||||
},
|
||||
"ReadJsonSecretSpecificKey": {
|
||||
reason: "Should read a specific key from a JSON secret.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
ref: &esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: "json_nested",
|
||||
Version: "default",
|
||||
Property: "key2",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: nil,
|
||||
val: map[string][]byte{
|
||||
"key3": []byte("value3"),
|
||||
"key4": []byte("value4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
"ReadJsonSecretSpecificKeyNotFound": {
|
||||
reason: "Should fail to read a nonexistent key from a JSON secret.",
|
||||
args: args{
|
||||
store: makeAPIKeySecretStore(svcURL, "conjur-hostid", "conjur-apikey", "myconjuraccount"),
|
||||
kube: clientfake.NewClientBuilder().
|
||||
WithObjects(makeFakeAPIKeySecrets()...).Build(),
|
||||
namespace: "default",
|
||||
ref: &esv1beta1.ExternalSecretDataRemoteRef{
|
||||
Key: "json_map",
|
||||
Version: "default",
|
||||
Property: "key3",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
err: fmt.Errorf("%w", fmt.Errorf("error getting secret json_map: cannot find secret data for key: \"key3\"")),
|
||||
val: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
runTest := func(t *testing.T, _ string, tc testCase) {
|
||||
provider, _ := newConjurProvider(context.Background(), tc.args.store, tc.args.kube, tc.args.namespace, tc.args.corev1, &ConjurMockAPIClient{})
|
||||
val, err := provider.GetSecretMap(context.Background(), *tc.args.ref)
|
||||
if diff := cmp.Diff(tc.want.err, err, EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nconjur.GetSecretMap(...): -want error, +got error:\n%s", tc.reason, diff)
|
||||
}
|
||||
if diff := cmp.Diff(tc.want.val, val); diff != "" {
|
||||
t.Errorf("\n%s\nconjur.GetSecretMap(...): -want val, +got val:\n%s", tc.reason, diff)
|
||||
}
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
runTest(t, name, tc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCA(t *testing.T) {
|
||||
type args struct {
|
||||
store esv1beta1.GenericStore
|
||||
|
|
100
pkg/provider/conjur/validate.go
Normal file
100
pkg/provider/conjur/validate.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
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 conjur provides a Conjur provider for External Secrets.
|
||||
package conjur
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/provider/conjur/util"
|
||||
"github.com/external-secrets/external-secrets/pkg/utils"
|
||||
)
|
||||
|
||||
// ValidateStore validates the store.
|
||||
func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
|
||||
prov, err := util.GetConjurProvider(store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if prov.URL == "" {
|
||||
return nil, fmt.Errorf("conjur URL cannot be empty")
|
||||
}
|
||||
if prov.Auth.APIKey != nil {
|
||||
err := validateAPIKeyStore(store, *prov.Auth.APIKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if prov.Auth.Jwt != nil {
|
||||
err := validateJWTStore(store, *prov.Auth.Jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// At least one auth must be configured
|
||||
if prov.Auth.APIKey == nil && prov.Auth.Jwt == nil {
|
||||
return nil, fmt.Errorf("missing Auth.* configuration")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func validateAPIKeyStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurAPIKey) error {
|
||||
if auth.Account == "" {
|
||||
return fmt.Errorf("missing Auth.ApiKey.Account")
|
||||
}
|
||||
if auth.UserRef == nil {
|
||||
return fmt.Errorf("missing Auth.Apikey.UserRef")
|
||||
}
|
||||
if auth.APIKeyRef == nil {
|
||||
return fmt.Errorf("missing Auth.Apikey.ApiKeyRef")
|
||||
}
|
||||
if err := utils.ValidateReferentSecretSelector(store, *auth.UserRef); err != nil {
|
||||
return fmt.Errorf("invalid Auth.Apikey.UserRef: %w", err)
|
||||
}
|
||||
if err := utils.ValidateReferentSecretSelector(store, *auth.APIKeyRef); err != nil {
|
||||
return fmt.Errorf("invalid Auth.Apikey.ApiKeyRef: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateJWTStore(store esv1beta1.GenericStore, auth esv1beta1.ConjurJWT) error {
|
||||
if auth.Account == "" {
|
||||
return fmt.Errorf("missing Auth.Jwt.Account")
|
||||
}
|
||||
if auth.ServiceID == "" {
|
||||
return fmt.Errorf("missing Auth.Jwt.ServiceID")
|
||||
}
|
||||
if auth.ServiceAccountRef == nil && auth.SecretRef == nil {
|
||||
return fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef")
|
||||
}
|
||||
if auth.SecretRef != nil {
|
||||
if err := utils.ValidateReferentSecretSelector(store, *auth.SecretRef); err != nil {
|
||||
return fmt.Errorf("invalid Auth.Jwt.SecretRef: %w", err)
|
||||
}
|
||||
}
|
||||
if auth.ServiceAccountRef != nil {
|
||||
if err := utils.ValidateReferentServiceAccountSelector(store, *auth.ServiceAccountRef); err != nil {
|
||||
return fmt.Errorf("invalid Auth.Jwt.ServiceAccountRef: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
93
pkg/provider/conjur/validate_test.go
Normal file
93
pkg/provider/conjur/validate_test.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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 conjur
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
|
||||
)
|
||||
|
||||
type ValidateStoreTestCase struct {
|
||||
store *esv1beta1.SecretStore
|
||||
err error
|
||||
}
|
||||
|
||||
func TestValidateStore(t *testing.T) {
|
||||
testCases := []ValidateStoreTestCase{
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, svcAccount),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore("", svcUser, svcApikey, svcAccount),
|
||||
err: fmt.Errorf("conjur URL cannot be empty"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, "", svcApikey, svcAccount),
|
||||
err: fmt.Errorf("missing Auth.Apikey.UserRef"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, "", svcAccount),
|
||||
err: fmt.Errorf("missing Auth.Apikey.ApiKeyRef"),
|
||||
},
|
||||
{
|
||||
store: makeAPIKeySecretStore(svcURL, svcUser, svcApikey, ""),
|
||||
err: fmt.Errorf("missing Auth.ApiKey.Account"),
|
||||
},
|
||||
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "", jwtSecretName, jwtAuthnService, "", "myconjuraccount"),
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", jwtAuthnService, "", ""),
|
||||
err: fmt.Errorf("missing Auth.Jwt.Account"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "conjur", "", "", "", "myconjuraccount"),
|
||||
err: fmt.Errorf("missing Auth.Jwt.ServiceID"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore("", "conjur", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: fmt.Errorf("conjur URL cannot be empty"),
|
||||
},
|
||||
{
|
||||
store: makeJWTSecretStore(svcURL, "", "", jwtAuthnService, "", "myconjuraccount"),
|
||||
err: fmt.Errorf("must specify Auth.Jwt.SecretRef or Auth.Jwt.ServiceAccountRef"),
|
||||
},
|
||||
|
||||
{
|
||||
store: makeNoAuthSecretStore(svcURL),
|
||||
err: fmt.Errorf("missing Auth.* configuration"),
|
||||
},
|
||||
}
|
||||
p := Provider{}
|
||||
for _, tc := range testCases {
|
||||
_, err := p.ValidateStore(tc.store)
|
||||
if tc.err != nil && err != nil && err.Error() != tc.err.Error() {
|
||||
t.Errorf("test failed! want %v, got %v", tc.err, err)
|
||||
} else if tc.err == nil && err != nil {
|
||||
t.Errorf("want nil got err %v", err)
|
||||
} else if tc.err != nil && err == nil {
|
||||
t.Errorf("want err %v got nil", tc.err)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue