diff --git a/apis/externalsecrets/v1beta1/secretstore_chef_types.go b/apis/externalsecrets/v1beta1/secretstore_chef_types.go new file mode 100644 index 00000000000..136b0fac67a --- /dev/null +++ b/apis/externalsecrets/v1beta1/secretstore_chef_types.go @@ -0,0 +1,38 @@ +/* +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" +) + +// ChefAuth contains a secretRef for credentials. +type ChefAuth struct { + SecretRef ChefAuthSecretRef `json:"secretRef"` +} + +// ChefAuthSecretRef holds secret references for chef server login credentials. +type ChefAuthSecretRef struct { + // SecretKey is the Signing Key in PEM format, used for authentication. + SecretKey esmeta.SecretKeySelector `json:"privateKeySecretRef"` +} + +// ChefProvider configures a store to sync secrets using basic chef server connection credentials. +type ChefProvider struct { + // Auth defines the information necessary to authenticate against chef Server + Auth *ChefAuth `json:"auth"` + // UserName should be the user ID on the chef server + UserName string `json:"username"` + // ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/" + ServerURL string `json:"serverUrl"` +} diff --git a/apis/externalsecrets/v1beta1/secretstore_types.go b/apis/externalsecrets/v1beta1/secretstore_types.go index e818298a187..42edc797880 100644 --- a/apis/externalsecrets/v1beta1/secretstore_types.go +++ b/apis/externalsecrets/v1beta1/secretstore_types.go @@ -141,6 +141,10 @@ type SecretStoreProvider struct { // https://docs.delinea.com/online-help/products/devops-secrets-vault/current // +optional Delinea *DelineaProvider `json:"delinea,omitempty"` + + // Chef configures this store to sync secrets with chef server + // +optional + Chef *ChefProvider `json:"chef,omitempty"` } type CAProviderType string diff --git a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go index 87abd5d5ce5..e748363897f 100644 --- a/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1beta1/zz_generated.deepcopy.go @@ -418,6 +418,58 @@ func (in *CertAuth) DeepCopy() *CertAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChefAuth) DeepCopyInto(out *ChefAuth) { + *out = *in + in.SecretRef.DeepCopyInto(&out.SecretRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefAuth. +func (in *ChefAuth) DeepCopy() *ChefAuth { + if in == nil { + return nil + } + out := new(ChefAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChefAuthSecretRef) DeepCopyInto(out *ChefAuthSecretRef) { + *out = *in + in.SecretKey.DeepCopyInto(&out.SecretKey) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefAuthSecretRef. +func (in *ChefAuthSecretRef) DeepCopy() *ChefAuthSecretRef { + if in == nil { + return nil + } + out := new(ChefAuthSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ChefProvider) DeepCopyInto(out *ChefProvider) { + *out = *in + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(ChefAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChefProvider. +func (in *ChefProvider) DeepCopy() *ChefProvider { + if in == nil { + return nil + } + out := new(ChefProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterExternalSecret) DeepCopyInto(out *ClusterExternalSecret) { *out = *in @@ -1992,6 +2044,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) { *out = new(DelineaProvider) (*in).DeepCopyInto(*out) } + if in.Chef != nil { + in, out := &in.Chef, &out.Chef + *out = new(ChefProvider) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider. diff --git a/config/crds/bases/external-secrets.io_clustersecretstores.yaml b/config/crds/bases/external-secrets.io_clustersecretstores.yaml index a1bbe6aabfa..81522c14930 100644 --- a/config/crds/bases/external-secrets.io_clustersecretstores.yaml +++ b/config/crds/bases/external-secrets.io_clustersecretstores.yaml @@ -2202,6 +2202,56 @@ spec: required: - vaultUrl type: object + chef: + description: Chef configures this store to sync secrets with chef + server + properties: + auth: + description: Auth defines the information necessary to authenticate + against chef Server + properties: + secretRef: + description: ChefAuthSecretRef holds secret references + for chef server login credentials. + properties: + privateKeySecretRef: + description: SecretKey is the Signing Key in PEM format, + used for authentication. + 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: + - privateKeySecretRef + type: object + required: + - secretRef + type: object + serverUrl: + description: ServerURL is the chef server URL used to connect + to. If using orgs you should include your org in the url + and terminate the url with a "/" + type: string + username: + description: UserName should be the user ID on the chef server + type: string + required: + - auth + - serverUrl + - username + type: object conjur: description: Conjur configures this store to sync secrets using conjur provider diff --git a/config/crds/bases/external-secrets.io_secretstores.yaml b/config/crds/bases/external-secrets.io_secretstores.yaml index f92a45c2c80..8b034fcefa4 100644 --- a/config/crds/bases/external-secrets.io_secretstores.yaml +++ b/config/crds/bases/external-secrets.io_secretstores.yaml @@ -2202,6 +2202,56 @@ spec: required: - vaultUrl type: object + chef: + description: Chef configures this store to sync secrets with chef + server + properties: + auth: + description: Auth defines the information necessary to authenticate + against chef Server + properties: + secretRef: + description: ChefAuthSecretRef holds secret references + for chef server login credentials. + properties: + privateKeySecretRef: + description: SecretKey is the Signing Key in PEM format, + used for authentication. + 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: + - privateKeySecretRef + type: object + required: + - secretRef + type: object + serverUrl: + description: ServerURL is the chef server URL used to connect + to. If using orgs you should include your org in the url + and terminate the url with a "/" + type: string + username: + description: UserName should be the user ID on the chef server + type: string + required: + - auth + - serverUrl + - username + type: object conjur: description: Conjur configures this store to sync secrets using conjur provider diff --git a/deploy/crds/bundle.yaml b/deploy/crds/bundle.yaml index 57e629f18ef..e10b8a0fc64 100644 --- a/deploy/crds/bundle.yaml +++ b/deploy/crds/bundle.yaml @@ -2665,6 +2665,49 @@ spec: required: - vaultUrl type: object + chef: + description: Chef configures this store to sync secrets with chef server + properties: + auth: + description: Auth defines the information necessary to authenticate against chef Server + properties: + secretRef: + description: ChefAuthSecretRef holds secret references for chef server login credentials. + properties: + privateKeySecretRef: + description: SecretKey is the Signing Key in PEM format, used for authentication. + 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: + - privateKeySecretRef + type: object + required: + - secretRef + type: object + serverUrl: + description: ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/" + type: string + username: + description: UserName should be the user ID on the chef server + type: string + required: + - auth + - serverUrl + - username + type: object conjur: description: Conjur configures this store to sync secrets using conjur provider properties: @@ -7639,6 +7682,49 @@ spec: required: - vaultUrl type: object + chef: + description: Chef configures this store to sync secrets with chef server + properties: + auth: + description: Auth defines the information necessary to authenticate against chef Server + properties: + secretRef: + description: ChefAuthSecretRef holds secret references for chef server login credentials. + properties: + privateKeySecretRef: + description: SecretKey is the Signing Key in PEM format, used for authentication. + 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: + - privateKeySecretRef + type: object + required: + - secretRef + type: object + serverUrl: + description: ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a "/" + type: string + username: + description: UserName should be the user ID on the chef server + type: string + required: + - auth + - serverUrl + - username + type: object conjur: description: Conjur configures this store to sync secrets using conjur provider properties: diff --git a/docs/api/spec.md b/docs/api/spec.md index 479819bbb49..ac64bbdb0ca 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -1108,6 +1108,123 @@ External Secrets meta/v1.SecretKeySelector +

ChefAuth +

+

+(Appears on: +ChefProvider) +

+

+

ChefAuth contains a secretRef for credentials.

+

+ + + + + + + + + + + + + +
FieldDescription
+secretRef
+ + +ChefAuthSecretRef + + +
+
+

ChefAuthSecretRef +

+

+(Appears on: +ChefAuth) +

+

+

ChefAuthSecretRef holds secret references for chef server login credentials.

+

+ + + + + + + + + + + + + +
FieldDescription
+privateKeySecretRef
+ + +External Secrets meta/v1.SecretKeySelector + + +
+

SecretKey is the Signing Key in PEM format, used for authentication.

+
+

ChefProvider +

+

+(Appears on: +SecretStoreProvider) +

+

+

ChefProvider configures a store to sync secrets using basic chef server connection credentials.

+

+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+auth
+ + +ChefAuth + + +
+

Auth defines the information necessary to authenticate against chef Server

+
+username
+ +string + +
+

UserName should be the user ID on the chef server

+
+serverUrl
+ +string + +
+

ServerURL is the chef server URL used to connect to. If using orgs you should include your org in the url and terminate the url with a “/”

+

ClusterExternalSecret

@@ -5289,6 +5406,20 @@ DelineaProvider https://docs.delinea.com/online-help/products/devops-secrets-vault/current

+ + +chef
+ + +ChefProvider + + + + +(Optional) +

Chef configures this store to sync secrets with chef server

+ +

SecretStoreRef diff --git a/docs/provider/chef.md b/docs/provider/chef.md new file mode 100644 index 00000000000..51622bbed8a --- /dev/null +++ b/docs/provider/chef.md @@ -0,0 +1,113 @@ +## Chef + +`Chef External Secrets provider` will enable users to seamlessly integrate their Chef-based secret management with Kubernetes through the existing External Secrets framework. + +In many enterprises, legacy applications and infrastructure are still tightly integrated with the Chef/Chef Infra Server/Chef Server Cluster for configuration and secrets management. Teams often rely on [Chef data bags](https://docs.chef.io/data_bags/) to securely store sensitive information such as application secrets and infrastructure configurations. These data bags serve as a centralized repository for managing and distributing sensitive data across the Chef ecosystem. + +**NOTE:** `Chef External Secrets provider` is designed only to fetch data from the Chef data bags into Kubernetes secrets, it won't update/delete any item in the data bags. + +### Authentication + +Every request made to the Chef Infra server needs to be authenticated. [Authentication](https://docs.chef.io/server/auth/) is done using the Private keys of the Chef Users. The User needs to have appropriate [Permissions](https://docs.chef.io/server/server_orgs/#permissions) to the data bags containing the data that they want to fetch using the External Secrets Operator. + +The following command can be used to create Chef Users: +```sh +chef-server-ctl user-create USER_NAME FIRST_NAME [MIDDLE_NAME] LAST_NAME EMAIL 'PASSWORD' (options) +``` + +More details on the above command are available here [Chef User Create Option](https://docs.chef.io/server/server_users/#user-create). The above command will return the default private key (PRIVATE_KEY_VALUE), which we will use for authentication. Additionally, a Chef User with access to specific data bags, a private key pair with an expiration date can be created with the help of the [knife user key](https://docs.chef.io/server/auth/#knife-user-key) command. + +### Create a secret containing your private key + +We need to store the above User's API key into a secret resource. +Example: +```sh +kubectl create secret generic chef-user-secret -n vivid --from-literal=user-private-key='PRIVATE_KEY_VALUE' +``` + +### Creating ClusterSecretStore + +The Chef `ClusterSecretStore` is a cluster-scoped SecretStore that can be referenced by all Chef `ExternalSecrets` from all namespaces. You can follow the below example to create a `ClusterSecretStore` resource. + +```yaml +apiVersion: external-secrets.io/v1beta1 +kind: ClusterSecretStore +metadata: + name: vivid-clustersecretstore # name of ClusterSecretStore +spec: + provider: + chef: + username: user # Chef User name + serverUrl: https://manage.chef.io/organizations/testuser/ # Chef server URL + auth: + secretRef: + privateKeySecretRef: + key: user-private-key # name of the key inside Secret resource + name: chef-user-secret # name of Kubernetes Secret resource containing the Chef User's private key + namespace: vivid # the namespace in which the above Secret resource resides +``` + +### Creating SecretStore + +Chef `SecretStores` are bound to a namespace and can not reference resources across namespaces. For cross-namespace SecretStores, you must use Chef `ClusterSecretStores`. + +You can follow the below example to create a `SecretStore` resource. + +```yaml +apiVersion: external-secrets.io/v1beta1 +kind: SecretStore +metadata: + name: vivid-secretstore # name of SecretStore + namespace: vivid # must be required for kind: SecretStore +spec: + provider: + chef: + username: user # Chef User name + serverUrl: https://manage.chef.io/organizations/testuser/ # Chef server URL + auth: + secretRef: + privateKeySecretRef: + name: chef-user-secret # name of Kubernetes Secret resource containing the Chef User's private key + key: user-private-key # name of the key inside Secret resource + namespace: vivid # the ns where the k8s secret resource containing Chef User's private key resides + +``` + +### Creating ExternalSecret + +The Chef `ExternalSecret` describes what data should be fetched from Chef Data bags, and how the data should be transformed and saved as a Kind=Secret. + +You can follow the below example to create an `ExternalSecret` resource. +```yaml +{% include 'chef-external-secret.yaml' %} +``` + +When the above `ClusterSecretStore` and `ExternalSecret` resources are created, the `ExternalSecret` will connect to the Chef Server using the private key and will fetch the data bags contained in the `vivid-credentials` secret resource. + +To get all data items inside the data bag, you can use the `dataFrom` directive: +```yaml +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: vivid-external-secrets # name of ExternalSecret + namespace: vivid # namespace inside which the ExternalSecret will be created + annotations: + company/contacts: user.a@company.com, user.b@company.com + company/team: vivid-dev + labels: + app.kubernetes.io/name: external-secrets +spec: + refreshInterval: 15m + secretStoreRef: + name: vivid-clustersecretstore # name of ClusterSecretStore + kind: ClusterSecretStore + dataFrom: + - extract: + key: vivid_global # only data bag name + target: + name: vivid_global_all_cred # name of Kubernetes Secret resource that will be created and will contain the obtained secrets + creationPolicy: Owner + +``` + +follow : [this file](https://github.com/external-secrets/external-secrets/blob/main/apis/externalsecrets/v1beta1/secretstore_chef_types.go) for more info diff --git a/docs/snippets/chef-external-secret.yaml b/docs/snippets/chef-external-secret.yaml new file mode 100644 index 00000000000..704a6bb9f48 --- /dev/null +++ b/docs/snippets/chef-external-secret.yaml @@ -0,0 +1,48 @@ +{% raw %} +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: vivid-external-secrets # name of ExternalSecret + namespace: vivid # namespace inside which the ExternalSecret will be created + annotations: + company/contacts: user.a@company.com, user.b@company.com + company/team: vivid-dev + labels: + app.kubernetes.io/name: external-secrets +spec: + refreshInterval: 15m + secretStoreRef: + name: vivid-clustersecretstore # name of ClusterSecretStore + kind: ClusterSecretStore + data: + - secretKey: USERNAME + remoteRef: + key: vivid_prod/global_user # databagName/dataItemName + property: username # a json key in dataItem + - secretKey: PASSWORD + remoteRef: + key: vivid_prod/global_user + property: password + - secretKey: APIKEY + remoteRef: + key: vivid_global/apikey + property: api_key + - secretKey: APP_PROPERTIES + remoteRef: + key: vivid_global/app_properties # databagName/dataItemName , it will fetch all key-vlaues present in the dataItem + target: + name: vivid-credentials # name of kubernetes Secret resource that will be created and will contain the obtained secrets + creationPolicy: Owner + template: + mergePolicy: Replace + engineVersion: v2 + data: + secrets.json: | + { + "username": "{{ .USERNAME }}", + "password": "{{ .PASSWORD }}", + "app_apikey": "{{ .APIKEY }}", + "app_properties": "{{ .APP_PROPERTIES }}" + } + +{% endraw %} diff --git a/go.mod b/go.mod index 3dc60a14d71..0c85855da8a 100644 --- a/go.mod +++ b/go.mod @@ -151,6 +151,7 @@ require ( github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-chef/chef v0.28.4 github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/errors v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect diff --git a/go.sum b/go.sum index 4f70c00979d..8046b488ba8 100644 --- a/go.sum +++ b/go.sum @@ -200,6 +200,8 @@ github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbi github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/ctdk/goiardi v0.11.10 h1:IB/3Afl1pC2Q4KGwzmhHPAoJfe8VtU51wZ2V0QkvsL0= +github.com/ctdk/goiardi v0.11.10/go.mod h1:Pr6Cj6Wsahw45myttaOEZeZ0LE7p1qzWmzgsBISkrNI= github.com/cyberark/conjur-api-go v0.11.1 h1:vjaMkw0geJsA+ikMM6UDLg4VLFQWKo/B0i9IWlOQ1f0= github.com/cyberark/conjur-api-go v0.11.1/go.mod h1:n1p46Hj9l8wkZjM17cVYdfcatyPboWyioLGlC0QszCs= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= @@ -245,6 +247,8 @@ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uq github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-chef/chef v0.28.4 h1:NvvEfBnS9sv6y+9NiBKf01kVAK+4LDKnCpYV8LjMi90= +github.com/go-chef/chef v0.28.4/go.mod h1:7RU1oCrRErTrkmIszkhJ9vHw7Bv2hZ1Vv1C1qKj01fc= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -583,6 +587,8 @@ github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqSc github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a h1:2v4Ipjxa3sh+xn6GvtgrMub2ci4ZLQMvTaYIba2lfdc= +github.com/r3labs/diff v0.0.0-20191120142937-b4ed99a31f5a/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml index e52a078506b..9b737dfac7a 100644 --- a/hack/api-docs/mkdocs.yml +++ b/hack/api-docs/mkdocs.yml @@ -89,6 +89,7 @@ nav: - AWS Secrets Manager: provider/aws-secrets-manager.md - AWS Parameter Store: provider/aws-parameter-store.md - Azure Key Vault: provider/azure-key-vault.md + - Chef: provider/chef.md - CyberArk Conjur: provider/conjur.md - Google Cloud Secret Manager: provider/google-secrets-manager.md - HashiCorp Vault: provider/hashicorp-vault.md diff --git a/pkg/provider/chef/chef.go b/pkg/provider/chef/chef.go new file mode 100644 index 00000000000..ab118e0e61e --- /dev/null +++ b/pkg/provider/chef/chef.go @@ -0,0 +1,343 @@ +/* +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 chef + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + "time" + + "github.com/go-chef/chef" + "github.com/go-logr/logr" + "github.com/tidwall/gjson" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + kclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + "github.com/external-secrets/external-secrets/pkg/metrics" + "github.com/external-secrets/external-secrets/pkg/utils" +) + +const ( + errChefStore = "received invalid Chef SecretStore resource: %w" + errMissingStore = "missing store" + errMissingStoreSpec = "missing store spec" + errMissingProvider = "missing provider" + errMissingChefProvider = "missing chef provider" + errMissingUserName = "missing username" + errMissingServerURL = "missing serverurl" + errMissingAuth = "cannot initialize Chef Client: no valid authType was specified" + errMissingSecretKey = "missing Secret Key" + errInvalidClusterStoreMissingPKNamespace = "invalid ClusterSecretStore: missing privateKeySecretRef.Namespace" + errFetchK8sSecret = "could not fetch SecretKey Secret: %w" + errInvalidURL = "invalid serverurl: %w" + errChefClient = "unable to create chef client: %w" + errChefProvider = "missing or invalid spec: %w" + errUninitalizedChefProvider = "chef provider is not initialized" + errNoDatabagItemFound = "data bag item %s not found in data bag %s" + errNoDatabagItemPropertyFound = "property %s not found in data bag item" + errCannotListDataBagItems = "unable to list items in data bag %s, may be given data bag doesn't exists or it is empty" + errUnableToConvertToJSON = "unable to convert databagItem into JSON" + errInvalidFormat = "invalid key format in data section. Expected value 'databagName/databagItemName'" + errStoreValidateFailed = "unable to validate provided store. Check if username, serverUrl and privateKey are correct" + errServerURLNoEndSlash = "serverurl does not end with slash(/)" + errInvalidDataform = "invalid key format in dataForm section. Expected only 'databagName'" + + ProviderChef = "Chef" + CallChefGetDataBagItem = "GetDataBagItem" + CallChefListDataBagItems = "ListDataBagItems" + CallChefGetUser = "GetUser" +) + +var contextTimeout = time.Second * 25 + +type DatabagFetcher interface { + GetItem(databagName string, databagItem string) (item chef.DataBagItem, err error) + ListItems(name string) (data *chef.DataBagListResult, err error) +} + +type UserInterface interface { + Get(name string) (user chef.User, err error) +} + +type Providerchef struct { + clientName string + databagService DatabagFetcher + userService UserInterface + log logr.Logger +} + +var _ v1beta1.SecretsClient = &Providerchef{} +var _ v1beta1.Provider = &Providerchef{} + +func init() { + v1beta1.Register(&Providerchef{}, &v1beta1.SecretStoreProvider{ + Chef: &v1beta1.ChefProvider{}, + }) +} + +func (providerchef *Providerchef) NewClient(ctx context.Context, store v1beta1.GenericStore, kube kclient.Client, namespace string) (v1beta1.SecretsClient, error) { + chefProvider, err := getChefProvider(store) + if err != nil { + return nil, fmt.Errorf(errChefProvider, err) + } + + credentialsSecret := &corev1.Secret{} + objectKey := types.NamespacedName{ + Name: chefProvider.Auth.SecretRef.SecretKey.Name, + Namespace: namespace, + } + + if store.GetObjectKind().GroupVersionKind().Kind == v1beta1.ClusterSecretStoreKind { + if chefProvider.Auth.SecretRef.SecretKey.Namespace == nil { + return nil, fmt.Errorf(errInvalidClusterStoreMissingPKNamespace) + } + objectKey.Namespace = *chefProvider.Auth.SecretRef.SecretKey.Namespace + } + + if err := kube.Get(ctx, objectKey, credentialsSecret); err != nil { + return nil, fmt.Errorf(errFetchK8sSecret, err) + } + + secretKey := credentialsSecret.Data[chefProvider.Auth.SecretRef.SecretKey.Key] + if len(secretKey) == 0 { + return nil, fmt.Errorf(errMissingSecretKey) + } + + client, err := chef.NewClient(&chef.Config{ + Name: chefProvider.UserName, + Key: string(secretKey), + BaseURL: chefProvider.ServerURL, + }) + if err != nil { + return nil, fmt.Errorf(errChefClient, err) + } + + providerchef.clientName = chefProvider.UserName + providerchef.databagService = client.DataBags + providerchef.userService = client.Users + providerchef.log = ctrl.Log.WithName("provider").WithName("chef").WithName("secretsmanager") + return providerchef, nil +} + +// Close closes the client connection. +func (providerchef *Providerchef) Close(_ context.Context) error { + return nil +} + +// Validate checks if the client is configured correctly +// to be able to retrieve secrets from the provider. +func (providerchef *Providerchef) Validate() (v1beta1.ValidationResult, error) { + _, err := providerchef.userService.Get(providerchef.clientName) + metrics.ObserveAPICall(ProviderChef, CallChefGetUser, err) + if err != nil { + return v1beta1.ValidationResultError, fmt.Errorf(errStoreValidateFailed) + } + return v1beta1.ValidationResultReady, nil +} + +// GetAllSecrets Retrieves a map[string][]byte with the Databag names as key and the Databag's Items as secrets. +func (providerchef *Providerchef) GetAllSecrets(_ context.Context, _ v1beta1.ExternalSecretFind) (map[string][]byte, error) { + return nil, fmt.Errorf("dataFrom.find not suppported") +} + +// GetSecret returns a databagItem present in the databag. format example: databagName/databagItemName. +func (providerchef *Providerchef) GetSecret(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) ([]byte, error) { + if utils.IsNil(providerchef.databagService) { + return nil, fmt.Errorf(errUninitalizedChefProvider) + } + + key := ref.Key + databagName := "" + databagItem := "" + nameSplitted := strings.Split(key, "/") + if len(nameSplitted) > 1 { + databagName = nameSplitted[0] + databagItem = nameSplitted[1] + } + providerchef.log.Info("fetching secret value", "databag Name:", databagName, "databag Item:", databagItem) + if databagName != "" && databagItem != "" { + return getSingleDatabagItemWithContext(ctx, providerchef, databagName, databagItem, ref.Property) + } + + return nil, fmt.Errorf(errInvalidFormat) +} + +func getSingleDatabagItemWithContext(ctx context.Context, providerchef *Providerchef, dataBagName, databagItemName, propertyName string) ([]byte, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, contextTimeout) + defer cancel() + type result = struct { + values []byte + err error + } + getWithTimeout := func() chan result { + resultChan := make(chan result, 1) + go func() { + defer close(resultChan) + ditem, err := providerchef.databagService.GetItem(dataBagName, databagItemName) + metrics.ObserveAPICall(ProviderChef, CallChefGetDataBagItem, err) + if err != nil { + resultChan <- result{err: fmt.Errorf(errNoDatabagItemFound, databagItemName, dataBagName)} + return + } + jsonByte, err := json.Marshal(ditem) + if err != nil { + resultChan <- result{err: fmt.Errorf(errUnableToConvertToJSON)} + return + } + if propertyName != "" { + propertyValue, err := getPropertyFromDatabagItem(jsonByte, propertyName) + if err != nil { + resultChan <- result{err: err} + return + } + resultChan <- result{values: propertyValue} + } else { + resultChan <- result{values: jsonByte} + } + }() + return resultChan + } + select { + case <-ctxWithTimeout.Done(): + return nil, ctxWithTimeout.Err() + case r := <-getWithTimeout(): + if r.err != nil { + return nil, r.err + } + return r.values, nil + } +} + +/* +A path is a series of keys separated by a dot. +A key may contain special wildcard characters '*' and '?'. +To access an array value use the index as the key. +To get the number of elements in an array or to access a child path, use the '#' character. +The dot and wildcard characters can be escaped with '\'. + +refer https://github.com/tidwall/gjson#:~:text=JSON%20byte%20slices.-,Path%20Syntax,-Below%20is%20a +*/ +func getPropertyFromDatabagItem(jsonByte []byte, propertyName string) ([]byte, error) { + result := gjson.GetBytes(jsonByte, propertyName) + + if !result.Exists() { + return nil, fmt.Errorf(errNoDatabagItemPropertyFound, propertyName) + } + return []byte(result.Str), nil +} + +// GetSecretMap returns multiple k/v pairs from the provider, for dataFrom.extract.key +// dataFrom.extract.key only accepts dataBagName, example : dataFrom.extract.key: myDatabag +// databagItemName or Property not expected in key. +func (providerchef *Providerchef) GetSecretMap(ctx context.Context, ref v1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + if utils.IsNil(providerchef.databagService) { + return nil, fmt.Errorf(errUninitalizedChefProvider) + } + databagName := ref.Key + + if strings.Contains(databagName, "/") { + return nil, fmt.Errorf(errInvalidDataform) + } + getAllSecrets := make(map[string][]byte) + providerchef.log.Info("fetching all items from", "databag:", databagName) + dataItems, err := providerchef.databagService.ListItems(databagName) + metrics.ObserveAPICall(ProviderChef, CallChefListDataBagItems, err) + if err != nil { + return nil, fmt.Errorf(errCannotListDataBagItems, databagName) + } + + for dataItem := range *dataItems { + dItem, err := getSingleDatabagItemWithContext(ctx, providerchef, databagName, dataItem, "") + if err != nil { + return nil, fmt.Errorf(errNoDatabagItemFound, dataItem, databagName) + } + getAllSecrets[dataItem] = dItem + } + return getAllSecrets, nil +} + +// ValidateStore checks if the provided store is valid. +func (providerchef *Providerchef) ValidateStore(store v1beta1.GenericStore) (admission.Warnings, error) { + chefProvider, err := getChefProvider(store) + if err != nil { + return nil, fmt.Errorf(errChefStore, err) + } + // check namespace compared to kind + if err := utils.ValidateSecretSelector(store, chefProvider.Auth.SecretRef.SecretKey); err != nil { + return nil, fmt.Errorf(errChefStore, err) + } + return nil, nil +} + +// getChefProvider validates the incoming store and return the chef provider. +func getChefProvider(store v1beta1.GenericStore) (*v1beta1.ChefProvider, error) { + if store == nil { + return nil, fmt.Errorf(errMissingStore) + } + storeSpec := store.GetSpec() + if storeSpec == nil { + return nil, fmt.Errorf(errMissingStoreSpec) + } + provider := storeSpec.Provider + if provider == nil { + return nil, fmt.Errorf(errMissingProvider) + } + chefProvider := storeSpec.Provider.Chef + if chefProvider == nil { + return nil, fmt.Errorf(errMissingChefProvider) + } + if chefProvider.UserName == "" { + return chefProvider, fmt.Errorf(errMissingUserName) + } + if chefProvider.ServerURL == "" { + return chefProvider, fmt.Errorf(errMissingServerURL) + } + if !strings.HasSuffix(chefProvider.ServerURL, "/") { + return chefProvider, fmt.Errorf(errServerURLNoEndSlash) + } + // check valid URL + if _, err := url.ParseRequestURI(chefProvider.ServerURL); err != nil { + return chefProvider, fmt.Errorf(errInvalidURL, err) + } + if chefProvider.Auth == nil { + return chefProvider, fmt.Errorf(errMissingAuth) + } + if chefProvider.Auth.SecretRef.SecretKey.Key == "" { + return chefProvider, fmt.Errorf(errMissingSecretKey) + } + + return chefProvider, nil +} + +// Not Implemented DeleteSecret. +func (providerchef *Providerchef) DeleteSecret(_ context.Context, _ v1beta1.PushSecretRemoteRef) error { + return fmt.Errorf("not implemented") +} + +// Not Implemented PushSecret. +func (providerchef *Providerchef) PushSecret(_ context.Context, _ *corev1.Secret, _ v1beta1.PushSecretData) error { + return fmt.Errorf("not implemented") +} + +// Capabilities return the provider supported capabilities (ReadOnly, WriteOnly, ReadWrite). +func (providerchef *Providerchef) Capabilities() v1beta1.SecretStoreCapabilities { + return v1beta1.SecretStoreReadOnly +} diff --git a/pkg/provider/chef/chef_test.go b/pkg/provider/chef/chef_test.go new file mode 100644 index 00000000000..107e2471d98 --- /dev/null +++ b/pkg/provider/chef/chef_test.go @@ -0,0 +1,428 @@ +/* +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 chef + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/go-chef/chef" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + + esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1" + v1 "github.com/external-secrets/external-secrets/apis/meta/v1" + fake "github.com/external-secrets/external-secrets/pkg/provider/chef/fake" + "github.com/external-secrets/external-secrets/pkg/utils" +) + +const ( + name = "chef-demo-user" + baseURL = "https://chef.cloudant.com/organizations/myorg/" + noEndSlashInvalidBaseURL = "no end slash invalid base URL" + baseInvalidURL = "invalid base URL/" + authName = "chef-demo-auth-name" + authKey = "chef-demo-auth-key" + authNamespace = "chef-demo-auth-namespace" + kind = "SecretStore" + apiversion = "external-secrets.io/v1beta1" + databagName = "databag01" +) + +type chefTestCase struct { + mockClient *fake.ChefMockClient + databagName string + databagItemName string + property string + ref *esv1beta1.ExternalSecretDataRemoteRef + apiErr error + expectError string + expectedData map[string][]byte + expectedByte []byte +} + +type ValidateStoreTestCase struct { + store *esv1beta1.SecretStore + err error +} + +// type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore + +func makeValidChefTestCase() *chefTestCase { + smtc := chefTestCase{ + mockClient: &fake.ChefMockClient{}, + databagName: "databag01", + databagItemName: "item01", + property: "", + apiErr: nil, + expectError: "", + expectedData: map[string][]byte{"item01": []byte(`"https://chef.com/organizations/dev/data/databag01/item01"`)}, + expectedByte: []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`), + } + + smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property) + smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr) + smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr) + return &smtc +} + +func makeInValidChefTestCase() *chefTestCase { + smtc := chefTestCase{ + mockClient: &fake.ChefMockClient{}, + databagName: "databag01", + databagItemName: "item03", + property: "", + apiErr: errors.New("unable to convert databagItem into JSON"), + expectError: "unable to convert databagItem into JSON", + expectedData: nil, + expectedByte: nil, + } + + smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, smtc.property) + smtc.mockClient.WithListItems(smtc.databagName, smtc.apiErr) + smtc.mockClient.WithItem(smtc.databagName, smtc.databagItemName, smtc.apiErr) + return &smtc +} + +func makeValidRef(databag, dataitem, property string) *esv1beta1.ExternalSecretDataRemoteRef { + return &esv1beta1.ExternalSecretDataRemoteRef{ + Key: databag + "/" + dataitem, + Property: property, + } +} + +func makeinValidRef() *esv1beta1.ExternalSecretDataRemoteRef { + return &esv1beta1.ExternalSecretDataRemoteRef{ + Key: "", + } +} + +func makeValidRefForGetSecretMap(databag string) *esv1beta1.ExternalSecretDataRemoteRef { + return &esv1beta1.ExternalSecretDataRemoteRef{ + Key: databag, + } +} + +func makeValidChefTestCaseCustom(tweaks ...func(smtc *chefTestCase)) *chefTestCase { + smtc := makeValidChefTestCase() + for _, fn := range tweaks { + fn(smtc) + } + return smtc +} + +func TestChefGetSecret(t *testing.T) { + nilClient := func(smtc *chefTestCase) { + smtc.mockClient = nil + smtc.expectedByte = nil + smtc.expectError = "chef provider is not initialized" + } + + invalidDatabagName := func(smtc *chefTestCase) { + smtc.databagName = "databag02" + smtc.expectedByte = nil + smtc.ref = makeinValidRef() + smtc.expectError = "invalid key format in data section. Expected value 'databagName/databagItemName'" + } + + invalidDatabagItemName := func(smtc *chefTestCase) { + smtc.expectError = "data bag item item02 not found in data bag databag01" + smtc.databagName = databagName + smtc.databagItemName = "item02" + smtc.expectedByte = nil + smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "") + } + + noProperty := func(smtc *chefTestCase) { + smtc.expectError = "property findProperty not found in data bag item" + smtc.databagName = databagName + smtc.databagItemName = "item01" + smtc.expectedByte = nil + smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty") + } + + withProperty := func(smtc *chefTestCase) { + smtc.expectedByte = []byte("foundProperty") + smtc.databagName = "databag03" + smtc.databagItemName = "item03" + smtc.ref = makeValidRef(smtc.databagName, smtc.databagItemName, "findProperty") + } + + successCases := []*chefTestCase{ + makeValidChefTestCase(), + makeValidChefTestCaseCustom(nilClient), + makeValidChefTestCaseCustom(invalidDatabagName), + makeValidChefTestCaseCustom(invalidDatabagItemName), + makeValidChefTestCaseCustom(noProperty), + makeValidChefTestCaseCustom(withProperty), + makeInValidChefTestCase(), + } + + sm := Providerchef{ + databagService: &chef.DataBagService{}, + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + for k, v := range successCases { + sm.databagService = v.mockClient + out, err := sm.GetSecret(ctx, *v.ref) + if err != nil && !utils.ErrorContains(err, v.expectError) { + t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err) + } else if v.expectError != "" && err == nil { + t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError) + } + if !bytes.Equal(out, v.expectedByte) { + t.Errorf("[case %d] expected secret: %s, got: %s", k, v.expectedByte, out) + } + } +} + +func TestChefGetSecretMap(t *testing.T) { + nilClient := func(smtc *chefTestCase) { + smtc.mockClient = nil + smtc.expectedByte = nil + smtc.expectError = "chef provider is not initialized" + } + + databagHasSlash := func(smtc *chefTestCase) { + smtc.expectedByte = nil + smtc.ref = makeinValidRef() + smtc.ref.Key = "data/Bag02" + smtc.expectError = "invalid key format in dataForm section. Expected only 'databagName'" + } + + withProperty := func(smtc *chefTestCase) { + smtc.expectedByte = []byte(`{"item01":"{\"id\":\"databag01-item01\",\"some_key\":\"fe7f29ede349519a1\",\"some_password\":\"dolphin_123zc\",\"some_username\":\"testuser\"}"}`) + smtc.databagName = databagName + smtc.ref = makeValidRefForGetSecretMap(smtc.databagName) + } + + withProperty2 := func(smtc *chefTestCase) { + smtc.expectError = "unable to list items in data bag 123, may be given data bag doesn't exists or it is empty" + smtc.expectedByte = nil + smtc.databagName = "123" + smtc.ref = makeValidRefForGetSecretMap(smtc.databagName) + } + + successCases := []*chefTestCase{ + makeValidChefTestCaseCustom(nilClient), + makeValidChefTestCaseCustom(databagHasSlash), + makeValidChefTestCaseCustom(withProperty), + makeValidChefTestCaseCustom(withProperty2), + } + + pc := Providerchef{ + databagService: &chef.DataBagService{}, + } + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + for k, v := range successCases { + pc.databagService = v.mockClient + out, err := pc.GetSecretMap(ctx, *v.ref) + if err != nil && !utils.ErrorContains(err, v.expectError) { + t.Errorf("[case %d] expected error: %v, got: %v", k, v.expectError, err) + } else if v.expectError != "" && err == nil { + t.Errorf("[case %d] expected error: %v, got: nil", k, v.expectError) + } + if !bytes.Equal(out["item01"], v.expectedByte) { + t.Errorf("[case %d] unexpected secret: expected %s, got %s", k, v.expectedByte, out) + } + } +} + +func makeSecretStore(name, baseURL string, auth *esv1beta1.ChefAuth) *esv1beta1.SecretStore { + store := &esv1beta1.SecretStore{ + Spec: esv1beta1.SecretStoreSpec{ + Provider: &esv1beta1.SecretStoreProvider{ + Chef: &esv1beta1.ChefProvider{ + UserName: name, + ServerURL: baseURL, + Auth: auth, + }, + }, + }, + } + return store +} + +func makeAuth(name, namespace, key string) *esv1beta1.ChefAuth { + return &esv1beta1.ChefAuth{ + SecretRef: esv1beta1.ChefAuthSecretRef{ + SecretKey: v1.SecretKeySelector{ + Name: name, + Key: key, + Namespace: &namespace, + }, + }, + } +} + +func TestValidateStore(t *testing.T) { + testCases := []ValidateStoreTestCase{ + { + store: makeSecretStore("", baseURL, makeAuth(authName, authNamespace, authKey)), + err: fmt.Errorf("received invalid Chef SecretStore resource: missing username"), + }, + { + store: makeSecretStore(name, "", makeAuth(authName, authNamespace, authKey)), + err: fmt.Errorf("received invalid Chef SecretStore resource: missing serverurl"), + }, + { + store: makeSecretStore(name, baseURL, nil), + err: fmt.Errorf("received invalid Chef SecretStore resource: cannot initialize Chef Client: no valid authType was specified"), + }, + { + store: makeSecretStore(name, baseInvalidURL, makeAuth(authName, authNamespace, authKey)), + err: fmt.Errorf("received invalid Chef SecretStore resource: invalid serverurl: parse \"invalid base URL/\": invalid URI for request"), + }, + { + store: makeSecretStore(name, noEndSlashInvalidBaseURL, makeAuth(authName, authNamespace, authKey)), + err: fmt.Errorf("received invalid Chef SecretStore resource: serverurl does not end with slash(/)"), + }, + { + store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, "")), + err: fmt.Errorf("received invalid Chef SecretStore resource: missing Secret Key"), + }, + { + store: makeSecretStore(name, baseURL, makeAuth(authName, authNamespace, authKey)), + err: fmt.Errorf("received invalid Chef SecretStore resource: namespace not allowed with namespaced SecretStore"), + }, + { + store: &esv1beta1.SecretStore{ + Spec: esv1beta1.SecretStoreSpec{ + Provider: nil, + }, + }, + err: fmt.Errorf("received invalid Chef SecretStore resource: missing provider"), + }, + { + store: &esv1beta1.SecretStore{ + Spec: esv1beta1.SecretStoreSpec{ + Provider: &esv1beta1.SecretStoreProvider{ + Chef: nil, + }, + }, + }, + err: fmt.Errorf("received invalid Chef SecretStore resource: missing chef provider"), + }, + } + pc := Providerchef{} + for _, tc := range testCases { + _, err := pc.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 TestNewClient(t *testing.T) { + store := &esv1beta1.SecretStore{TypeMeta: metav1.TypeMeta{Kind: "ClusterSecretStore"}, + Spec: esv1beta1.SecretStoreSpec{ + Provider: &esv1beta1.SecretStoreProvider{ + Chef: &esv1beta1.ChefProvider{ + Auth: makeAuth(authName, authNamespace, authKey), + UserName: name, + ServerURL: baseURL, + }, + }, + }, + } + + expected := fmt.Sprintf("could not fetch SecretKey Secret: secrets %q not found", authName) + expectedMissingStore := "missing or invalid spec: missing store" + + ctx := context.TODO() + + kube := clientfake.NewClientBuilder().WithObjects(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "creds", + Namespace: "default", + }, TypeMeta: metav1.TypeMeta{ + Kind: kind, + APIVersion: apiversion, + }, + }).Build() + + pc := Providerchef{databagService: &fake.ChefMockClient{}} + _, errMissingStore := pc.NewClient(ctx, nil, kube, "default") + if !ErrorContains(errMissingStore, expectedMissingStore) { + t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", errMissingStore.Error(), expectedMissingStore) + } + _, err := pc.NewClient(ctx, store, kube, "default") + if !ErrorContains(err, expected) { + t.Errorf("CheckNewClient unexpected error: %s, expected: '%s'", err.Error(), expected) + } +} + +func ErrorContains(out error, want string) bool { + if out == nil { + return want == "" + } + if want == "" { + return false + } + return strings.Contains(out.Error(), want) +} + +func TestValidate(t *testing.T) { + pc := Providerchef{} + var mockClient *fake.ChefMockClient + pc.userService = mockClient + pc.clientName = "correctUser" + _, err := pc.Validate() + t.Log("Error: ", err) + pc.clientName = "wrongUser" + _, err = pc.Validate() + t.Log("Error: ", err) +} + +func TestCapabilities(t *testing.T) { + pc := Providerchef{} + capabilities := pc.Capabilities() + t.Log(capabilities) +} + +// Test Cases To be added when Close function is implemented. +func TestClose(_ *testing.T) { + pc := Providerchef{} + pc.Close(context.Background()) +} + +// Test Cases To be added when GetAllSecrets function is implemented. +func TestGetAllSecrets(_ *testing.T) { + pc := Providerchef{} + pc.GetAllSecrets(context.Background(), esv1beta1.ExternalSecretFind{}) +} + +// Test Cases To be implemented when DeleteSecret function is implemented. +func TestDeleteSecret(_ *testing.T) { + pc := Providerchef{} + pc.DeleteSecret(context.Background(), nil) +} + +// Test Cases To be implemented when PushSecret function is implemented. +func TestPushSecret(_ *testing.T) { + pc := Providerchef{} + pc.PushSecret(context.Background(), &corev1.Secret{}, nil) +} diff --git a/pkg/provider/chef/fake/fake.go b/pkg/provider/chef/fake/fake.go new file mode 100644 index 00000000000..41466fe1e90 --- /dev/null +++ b/pkg/provider/chef/fake/fake.go @@ -0,0 +1,104 @@ +/* +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 fake + +import ( + "errors" + "fmt" + "math" + + "github.com/go-chef/chef" +) + +const ( + CORRECTUSER = "correctUser" + testitem = "item03" + DatabagName = "databag01" +) + +type ChefMockClient struct { + getItem func(databagName string, databagItem string) (item chef.DataBagItem, err error) + listItems func(name string) (data *chef.DataBagListResult, err error) + getUser func(name string) (user chef.User, err error) +} + +func (mc *ChefMockClient) GetItem(databagName, databagItem string) (item chef.DataBagItem, err error) { + return mc.getItem(databagName, databagItem) +} + +func (mc *ChefMockClient) ListItems(name string) (data *chef.DataBagListResult, err error) { + return mc.listItems(name) +} + +func (mc *ChefMockClient) Get(name string) (user chef.User, err error) { + if name == CORRECTUSER { + user = chef.User{ + UserName: name, + } + err = nil + } else { + user = chef.User{} + err = errors.New("message") + } + return user, err +} + +func (mc *ChefMockClient) WithItem(_, _ string, _ error) { + if mc != nil { + mc.getItem = func(dataBagName, databagItemName string) (item chef.DataBagItem, err error) { + ret := make(map[string]interface{}) + switch { + case dataBagName == DatabagName && databagItemName == "item01": + jsonstring := `{"id":"` + dataBagName + `-` + databagItemName + `","some_key":"fe7f29ede349519a1","some_password":"dolphin_123zc","some_username":"testuser"}` + ret[databagItemName] = jsonstring + case dataBagName == "databag03" && databagItemName == testitem: + jsonMap := make(map[string]string) + jsonMap["id"] = testitem + jsonMap["findProperty"] = "foundProperty" + return jsonMap, nil + case dataBagName == DatabagName && databagItemName == testitem: + return math.Inf(1), nil + default: + str := "https://chef.com/organizations/dev/data/" + dataBagName + "/" + databagItemName + ": 404" + return nil, errors.New(str) + } + return ret, nil + } + } +} + +func (mc *ChefMockClient) WithListItems(_ string, _ error) { + if mc != nil { + mc.listItems = func(databagName string) (data *chef.DataBagListResult, err error) { + ret := make(chef.DataBagListResult) + if databagName == DatabagName { + jsonstring := fmt.Sprintf("https://chef.com/organizations/dev/data/%s/item01", databagName) + ret["item01"] = jsonstring + } else { + return nil, fmt.Errorf("data bag not found: %s", databagName) + } + return &ret, nil + } + } +} + +func (mc *ChefMockClient) WithUser(_ string, _ error) { + if mc != nil { + mc.getUser = func(name string) (user chef.User, err error) { + return chef.User{ + UserName: name, + }, nil + } + } +} diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go index 68004c4a041..c43b8012f0f 100644 --- a/pkg/provider/register/register.go +++ b/pkg/provider/register/register.go @@ -22,6 +22,7 @@ import ( _ "github.com/external-secrets/external-secrets/pkg/provider/alibaba" _ "github.com/external-secrets/external-secrets/pkg/provider/aws" _ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault" + _ "github.com/external-secrets/external-secrets/pkg/provider/chef" _ "github.com/external-secrets/external-secrets/pkg/provider/conjur" _ "github.com/external-secrets/external-secrets/pkg/provider/delinea" _ "github.com/external-secrets/external-secrets/pkg/provider/doppler"