Browse Source

Onboardbase (#2697)

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Run decrypt with error

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Commit and Save

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Pull secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Integrate Onboardbase Into ESO

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Minor Fix And Cleanups

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Attend to review comments

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Install deps

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Improved docs

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Improved docs

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Update hack/crd.generate.sh

Co-authored-by: Gustavo Fernandes de Carvalho <gusfcarvalho@gmail.com>
Signed-off-by: Aleem Isiaka <30846935+limistah@users.noreply.github.com>

* address issues with running the code

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* decrypt library into code

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* add docs to onboardbase provider

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* refactor duplicates

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Address Issues with tests

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Address issues with delete policy and json secrets

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Fix lint errors

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* error out when there is tags in the find field

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* execute delete request with the right data

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* ignore deletion policy

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* improve lint errors

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* remove cryptojs decrypt libs

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* Get secret value if property is set

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* run obb operator

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* 👌 IMPROVE: supports request deadline, esv1beta1 api updates

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* use same timeout

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* fix sonar cloud issues

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* fix sonar cloud issues

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* fix sonar cloud issues

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* fix failing test

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* add improve docs

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

* add improve docs

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>

---------

Signed-off-by: Aleem Isiaka <aleemisiaka@gmail.com>
Signed-off-by: Nasirudeen Olohundare <iamnasirudeen@gmail.com>
Signed-off-by: Aleem Isiaka <30846935+limistah@users.noreply.github.com>
Co-authored-by: Nasirudeen Olohundare <iamnasirudeen@gmail.com>
Co-authored-by: Gustavo Fernandes de Carvalho <gusfcarvalho@gmail.com>
Aleem Isiaka 2 years ago
parent
commit
52f6655345

+ 50 - 0
apis/externalsecrets/v1beta1/secretstore_onboardbase_types.go

@@ -0,0 +1,50 @@
+/*
+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"
+)
+
+// OnboardbaseAuthSecretRef holds secret references for onboardbase API Key credentials.
+type OnboardbaseAuthSecretRef struct {
+	// OnboardbaseAPIKey is the APIKey generated by an admin account.
+	// It is used to recognize and authorize access to a project and environment within onboardbase
+	// +kubebuilder:validation:Required
+	OnboardbaseAPIKeyRef esmeta.SecretKeySelector `json:"apiKeyRef"`
+	// OnboardbasePasscode is the passcode attached to the API Key
+	// +kubebuilder:validation:Required
+	OnboardbasePasscodeRef esmeta.SecretKeySelector `json:"passcodeRef"`
+}
+
+// OnboardbaseProvider configures a store to sync secrets using the Onboardbase provider.
+// Project and Config are required if not using a Service Token.
+type OnboardbaseProvider struct {
+	// Auth configures how the Operator authenticates with the Onboardbase API
+	Auth *OnboardbaseAuthSecretRef `json:"auth"`
+
+	// APIHost use this to configure the host url for the API for selfhosted installation, default is https://public.onboardbase.com/api/v1/
+	// +kubebuilder:default:="https://public.onboardbase.com/api/v1/"
+	APIHost string `json:"apiHost"`
+
+	// Project is an onboardbase project that the secrets should be pulled from
+	// +kubebuilder:validation:Required
+	// +kubebuilder:default:="development"
+	Project string `json:"project"`
+	// Environment is the name of an environmnent within a project to pull the secrets from
+	// +kubebuilder:validation:Required
+	// +kubebuilder:default:="development"
+	Environment string `json:"environment"`
+}

+ 4 - 0
apis/externalsecrets/v1beta1/secretstore_types.go

@@ -129,6 +129,10 @@ type SecretStoreProvider struct {
 	// +optional
 	// +optional
 	Doppler *DopplerProvider `json:"doppler,omitempty"`
 	Doppler *DopplerProvider `json:"doppler,omitempty"`
 
 
+	// Onboardbase configures this store to sync secrets using the Onboardbase provider
+	// +optional
+	Onboardbase *OnboardbaseProvider `json:"onboardbase,omitempty"`
+
 	// KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
 	// KeeperSecurity configures this store to sync secrets using the KeeperSecurity provider
 	// +optional
 	// +optional
 	KeeperSecurity *KeeperSecurityProvider `json:"keepersecurity,omitempty"`
 	KeeperSecurity *KeeperSecurityProvider `json:"keepersecurity,omitempty"`

+ 42 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1752,6 +1752,43 @@ func (in *NoSecretError) DeepCopy() *NoSecretError {
 }
 }
 
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OnboardbaseAuthSecretRef) DeepCopyInto(out *OnboardbaseAuthSecretRef) {
+	*out = *in
+	in.OnboardbaseAPIKeyRef.DeepCopyInto(&out.OnboardbaseAPIKeyRef)
+	in.OnboardbasePasscodeRef.DeepCopyInto(&out.OnboardbasePasscodeRef)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnboardbaseAuthSecretRef.
+func (in *OnboardbaseAuthSecretRef) DeepCopy() *OnboardbaseAuthSecretRef {
+	if in == nil {
+		return nil
+	}
+	out := new(OnboardbaseAuthSecretRef)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OnboardbaseProvider) DeepCopyInto(out *OnboardbaseProvider) {
+	*out = *in
+	if in.Auth != nil {
+		in, out := &in.Auth, &out.Auth
+		*out = new(OnboardbaseAuthSecretRef)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OnboardbaseProvider.
+func (in *OnboardbaseProvider) DeepCopy() *OnboardbaseProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OnboardbaseProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OnePasswordAuth) DeepCopyInto(out *OnePasswordAuth) {
 func (in *OnePasswordAuth) DeepCopyInto(out *OnePasswordAuth) {
 	*out = *in
 	*out = *in
 	if in.SecretRef != nil {
 	if in.SecretRef != nil {
@@ -2109,6 +2146,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
 		*out = new(DopplerProvider)
 		*out = new(DopplerProvider)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.Onboardbase != nil {
+		in, out := &in.Onboardbase, &out.Onboardbase
+		*out = new(OnboardbaseProvider)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.KeeperSecurity != nil {
 	if in.KeeperSecurity != nil {
 		in, out := &in.KeeperSecurity, &out.KeeperSecurity
 		in, out := &in.KeeperSecurity, &out.KeeperSecurity
 		*out = new(KeeperSecurityProvider)
 		*out = new(KeeperSecurityProvider)

+ 72 - 0
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -2968,6 +2968,78 @@ spec:
                     required:
                     required:
                     - auth
                     - auth
                     type: object
                     type: object
+                  onboardbase:
+                    description: Onboardbase configures this store to sync secrets
+                      using the Onboardbase provider
+                    properties:
+                      apiHost:
+                        default: https://public.onboardbase.com/api/v1/
+                        description: APIHost use this to configure the host url for
+                          the API for selfhosted installation, default is https://public.onboardbase.com/api/v1/
+                        type: string
+                      auth:
+                        description: Auth configures how the Operator authenticates
+                          with the Onboardbase API
+                        properties:
+                          apiKeyRef:
+                            description: |-
+                              OnboardbaseAPIKey is the APIKey generated by an admin account.
+                              It is used to recognize and authorize access to a project and environment within onboardbase
+                            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
+                          passcodeRef:
+                            description: OnboardbasePasscode is the passcode attached
+                              to the API Key
+                            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:
+                        - apiKeyRef
+                        - passcodeRef
+                        type: object
+                      environment:
+                        default: development
+                        description: Environment is the name of an environmnent within
+                          a project to pull the secrets from
+                        type: string
+                      project:
+                        default: development
+                        description: Project is an onboardbase project that the secrets
+                          should be pulled from
+                        type: string
+                    required:
+                    - apiHost
+                    - auth
+                    - environment
+                    - project
+                    type: object
                   onepassword:
                   onepassword:
                     description: OnePassword configures this store to sync secrets
                     description: OnePassword configures this store to sync secrets
                       using the 1Password Cloud provider
                       using the 1Password Cloud provider

+ 72 - 0
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -2968,6 +2968,78 @@ spec:
                     required:
                     required:
                     - auth
                     - auth
                     type: object
                     type: object
+                  onboardbase:
+                    description: Onboardbase configures this store to sync secrets
+                      using the Onboardbase provider
+                    properties:
+                      apiHost:
+                        default: https://public.onboardbase.com/api/v1/
+                        description: APIHost use this to configure the host url for
+                          the API for selfhosted installation, default is https://public.onboardbase.com/api/v1/
+                        type: string
+                      auth:
+                        description: Auth configures how the Operator authenticates
+                          with the Onboardbase API
+                        properties:
+                          apiKeyRef:
+                            description: |-
+                              OnboardbaseAPIKey is the APIKey generated by an admin account.
+                              It is used to recognize and authorize access to a project and environment within onboardbase
+                            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
+                          passcodeRef:
+                            description: OnboardbasePasscode is the passcode attached
+                              to the API Key
+                            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:
+                        - apiKeyRef
+                        - passcodeRef
+                        type: object
+                      environment:
+                        default: development
+                        description: Environment is the name of an environmnent within
+                          a project to pull the secrets from
+                        type: string
+                      project:
+                        default: development
+                        description: Project is an onboardbase project that the secrets
+                          should be pulled from
+                        type: string
+                    required:
+                    - apiHost
+                    - auth
+                    - environment
+                    - project
+                    type: object
                   onepassword:
                   onepassword:
                     description: OnePassword configures this store to sync secrets
                     description: OnePassword configures this store to sync secrets
                       using the 1Password Cloud provider
                       using the 1Password Cloud provider

+ 128 - 0
deploy/crds/bundle.yaml

@@ -3366,6 +3366,70 @@ spec:
                       required:
                       required:
                         - auth
                         - auth
                       type: object
                       type: object
+                    onboardbase:
+                      description: Onboardbase configures this store to sync secrets using the Onboardbase provider
+                      properties:
+                        apiHost:
+                          default: https://public.onboardbase.com/api/v1/
+                          description: APIHost use this to configure the host url for the API for selfhosted installation, default is https://public.onboardbase.com/api/v1/
+                          type: string
+                        auth:
+                          description: Auth configures how the Operator authenticates with the Onboardbase API
+                          properties:
+                            apiKeyRef:
+                              description: |-
+                                OnboardbaseAPIKey is the APIKey generated by an admin account.
+                                It is used to recognize and authorize access to a project and environment within onboardbase
+                              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
+                            passcodeRef:
+                              description: OnboardbasePasscode is the passcode attached to the API Key
+                              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:
+                            - apiKeyRef
+                            - passcodeRef
+                          type: object
+                        environment:
+                          default: development
+                          description: Environment is the name of an environmnent within a project to pull the secrets from
+                          type: string
+                        project:
+                          default: development
+                          description: Project is an onboardbase project that the secrets should be pulled from
+                          type: string
+                      required:
+                        - apiHost
+                        - auth
+                        - environment
+                        - project
+                      type: object
                     onepassword:
                     onepassword:
                       description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
                       description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
                       properties:
                       properties:
@@ -8456,6 +8520,70 @@ spec:
                       required:
                       required:
                         - auth
                         - auth
                       type: object
                       type: object
+                    onboardbase:
+                      description: Onboardbase configures this store to sync secrets using the Onboardbase provider
+                      properties:
+                        apiHost:
+                          default: https://public.onboardbase.com/api/v1/
+                          description: APIHost use this to configure the host url for the API for selfhosted installation, default is https://public.onboardbase.com/api/v1/
+                          type: string
+                        auth:
+                          description: Auth configures how the Operator authenticates with the Onboardbase API
+                          properties:
+                            apiKeyRef:
+                              description: |-
+                                OnboardbaseAPIKey is the APIKey generated by an admin account.
+                                It is used to recognize and authorize access to a project and environment within onboardbase
+                              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
+                            passcodeRef:
+                              description: OnboardbasePasscode is the passcode attached to the API Key
+                              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:
+                            - apiKeyRef
+                            - passcodeRef
+                          type: object
+                        environment:
+                          default: development
+                          description: Environment is the name of an environmnent within a project to pull the secrets from
+                          type: string
+                        project:
+                          default: development
+                          description: Project is an onboardbase project that the secrets should be pulled from
+                          type: string
+                      required:
+                        - apiHost
+                        - auth
+                        - environment
+                        - project
+                      type: object
                     onepassword:
                     onepassword:
                       description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
                       description: OnePassword configures this store to sync secrets using the 1Password Cloud provider
                       properties:
                       properties:

+ 126 - 0
docs/api/spec.md

@@ -4510,6 +4510,118 @@ CAProvider
 <p>NoSecretError shall be returned when a GetSecret can not find the
 <p>NoSecretError shall be returned when a GetSecret can not find the
 desired secret. This is used for deletionPolicy.</p>
 desired secret. This is used for deletionPolicy.</p>
 </p>
 </p>
+<h3 id="external-secrets.io/v1beta1.OnboardbaseAuthSecretRef">OnboardbaseAuthSecretRef
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.OnboardbaseProvider">OnboardbaseProvider</a>)
+</p>
+<p>
+<p>OnboardbaseAuthSecretRef holds secret references for onboardbase API Key credentials.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiKeyRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>OnboardbaseAPIKey is the APIKey generated by an admin account.
+It is used to recognize and authorize access to a project and environment within onboardbase</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>passcodeRef</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+<p>OnboardbasePasscode is the passcode attached to the API Key</p>
+</td>
+</tr>
+</tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.OnboardbaseProvider">OnboardbaseProvider
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.SecretStoreProvider">SecretStoreProvider</a>)
+</p>
+<p>
+<p>OnboardbaseProvider configures a store to sync secrets using the Onboardbase provider.
+Project and Config are required if not using a Service Token.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>auth</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.OnboardbaseAuthSecretRef">
+OnboardbaseAuthSecretRef
+</a>
+</em>
+</td>
+<td>
+<p>Auth configures how the Operator authenticates with the Onboardbase API</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>apiHost</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>APIHost use this to configure the host url for the API for selfhosted installation, default is <a href="https://public.onboardbase.com/api/v1/">https://public.onboardbase.com/api/v1/</a></p>
+</td>
+</tr>
+<tr>
+<td>
+<code>project</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Project is an onboardbase project that the secrets should be pulled from</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>environment</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Environment is the name of an environmnent within a project to pull the secrets from</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1beta1.OnePasswordAuth">OnePasswordAuth
 <h3 id="external-secrets.io/v1beta1.OnePasswordAuth">OnePasswordAuth
 </h3>
 </h3>
 <p>
 <p>
@@ -5537,6 +5649,20 @@ DopplerProvider
 </tr>
 </tr>
 <tr>
 <tr>
 <td>
 <td>
+<code>onboardbase</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.OnboardbaseProvider">
+OnboardbaseProvider
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Onboardbase configures this store to sync secrets using the Onboardbase provider</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>keepersecurity</code></br>
 <code>keepersecurity</code></br>
 <em>
 <em>
 <a href="#external-secrets.io/v1beta1.KeeperSecurityProvider">
 <a href="#external-secrets.io/v1beta1.KeeperSecurityProvider">

BIN
docs/pictures/onboardbase-api-key.png


BIN
docs/pictures/onboardbase-create-api-key.png


BIN
docs/pictures/onboardbase-provider.png


+ 70 - 0
docs/provider/onboardbase.md

@@ -0,0 +1,70 @@
+
+![Onboardbase External Secrets Provider](../pictures/onboardbase-provider.png)
+
+## Onboardbase Secret Management
+
+Sync secrets from [Onboardbase](https://www.onboardbase.com/) to Kubernetes using the External Secrets Operator.
+
+## Authentication
+
+### Get an Onboardbase [API Key](https://docs.onboardbase.com/reference/api-auth).
+
+Create the Onboardbase API by opening the organization tab under your account settings:
+
+![Onboardabse API Key](../pictures/onboardbase-api-key.png)
+
+
+
+And view them under the team name in your Account settings
+
+
+![Onboardabse API Key](../pictures/onboardbase-create-api-key.png)
+
+Create an Onboardbase API secret with your API Key and Passcode value:
+
+```sh
+HISTIGNORE='*kubectl*' \
+  kubectl create secret generic onboardbase-auth-secret \
+  --from-literal=API_KEY=*****VZYKYJNMMEMK***** \
+  --from-literal=PASSCODE=api-key-passcode
+```
+
+Then to create a generic `SecretStore`:
+
+```yaml
+{% include 'onboardbase-generic-secret-store.yaml' %}
+```
+
+## Use Cases
+
+The below operations are possible with the Onboardbase provider:
+
+1. [Fetch](#1-fetch)
+2. [Fetch all](#2-fetch-all)
+3. [Filter](#3-filter)
+
+Let's explore each use case using a fictional `auth-api` Onboardbase project.
+
+### 1. Fetch
+
+To sync one or more individual secrets:
+
+```yaml
+{% include 'onboardbase-fetch-secret.yaml' %}
+```
+
+### 2. Fetch all
+
+To sync every secret from a config:
+
+```yaml
+{% include 'onboardbase-fetch-all-secrets.yaml' %}
+```
+
+### 3. Filter
+
+To filter secrets by `path` (path prefix), `name` (regular expression) or a combination of both:
+
+```yaml
+{% include 'onboardbase-filtered-secrets.yaml' %}
+```

+ 15 - 0
docs/snippets/onboardbase-fetch-all-secrets.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: service-name-secrets
+spec:
+  refreshInterval: 10m
+  secretStoreRef:
+    name: onboardbase-external-secret-store
+    kind: SecretStore
+  target:
+    name: service-name-secrets
+  dataFrom:
+    - find:
+        name:
+          regexp: .*

+ 15 - 0
docs/snippets/onboardbase-fetch-secret.yaml

@@ -0,0 +1,15 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: service-name-secrets
+spec:
+  refreshInterval: 10m
+  secretStoreRef:
+    name: onboardbase-external-secret-store
+    kind: SecretStore
+  target:
+    name: service-name-secrets
+  data:
+  - secretKey: DATABASE_URI
+    remoteRef: 
+      key: DATABASE_URI

+ 14 - 0
docs/snippets/onboardbase-filtered-secrets.yaml

@@ -0,0 +1,14 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: service-name-secrets
+spec:
+  refreshInterval: 10m
+  secretStoreRef:
+    name: onboardbase-external-secret-store
+    kind: SecretStore
+  target:
+    name: service-name-secrets
+  dataFrom:
+    - find:
+        path: DATABASE_

+ 16 - 0
docs/snippets/onboardbase-generic-secret-store.yaml

@@ -0,0 +1,16 @@
+apiVersion: external-secrets.io/v1beta1
+kind: SecretStore
+metadata:
+  name: onboardbase-external-secret-store
+spec:
+  provider:
+    onboardbase:
+      project: project-name # can be altered from here
+      environment: development # can be altered from here
+      auth:
+        apiKey:
+          name: onboardbase-auth-secret
+          key: onboardbase-api-key 
+        passcode:
+          name: onboardbase-auth-secret
+          key: onboardbase-passcode

+ 1 - 0
go.mod

@@ -63,6 +63,7 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
 	github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2
 	github.com/DelineaXPM/dsv-sdk-go/v2 v2.1.2
+	github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d
 	github.com/akeylesslabs/akeyless-go/v3 v3.6.1
 	github.com/akeylesslabs/akeyless-go/v3 v3.6.1
 	github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5
 	github.com/alibabacloud-go/darabonba-openapi/v2 v2.0.5
 	github.com/alibabacloud-go/kms-20160120/v3 v3.1.0
 	github.com/alibabacloud-go/kms-20160120/v3 v3.1.0

+ 2 - 0
go.sum

@@ -111,6 +111,8 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
 github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
 github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
 github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
 github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d h1:V7xPdg5XgCcUJgL57zfZSNOIvrDPWA4SpWuRJ0UVwKs=
+github.com/Onboardbase/go-cryptojs-aes-decrypt v0.0.0-20230430095000-27c0d3a9016d/go.mod h1:WI6HYqD62DSW+C0gMS0zHe/vXhZVCUg2ecVosnglPNc=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
 github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
 github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
 github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -114,6 +114,7 @@ nav:
     - Scaleway: provider/scaleway.md
     - Scaleway: provider/scaleway.md
     - Delinea: provider/delinea.md
     - Delinea: provider/delinea.md
     - Pulumi ESC: provider/pulumi.md
     - Pulumi ESC: provider/pulumi.md
+    - Onboardbase: provider/onboardbase.md
   - Examples:
   - Examples:
     - FluxCD: examples/gitops-using-fluxcd.md
     - FluxCD: examples/gitops-using-fluxcd.md
     - Anchore Engine: examples/anchore-engine-credentials.md
     - Anchore Engine: examples/anchore-engine-credentials.md

+ 243 - 0
pkg/provider/onboardbase/client.go

@@ -0,0 +1,243 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package onboardbase
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/tidwall/gjson"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/find"
+	onboardbaseClient "github.com/external-secrets/external-secrets/pkg/provider/onboardbase/client"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errGetSecret                                            = "could not get secret %s: %s"
+	errGetSecrets                                           = "could not get secrets %s"
+	errUnmarshalSecretMap                                   = "unable to unmarshal secret %s: %w"
+	errOnboardbaseAPIKeySecretName                          = "missing auth.secretRef.onboardbaseAPIKey.name"
+	errInvalidClusterStoreMissingOnboardbaseAPIKeyNamespace = "missing auth.secretRef.onboardbaseAPIKey.namespace"
+	errFetchOnboardbaseAPIKeySecret                         = "unable to find find OnboardbaseAPIKey secret: %w"
+	errMissingOnboardbaseAPIKey                             = "auth.secretRef.onboardbaseAPIKey.key '%s' not found in secret '%s'"
+	errMissingOnboardbasePasscode                           = "auth.secretRef.onboardbasePasscode.key '%s' not found in secret '%s'"
+	errSecretKeyFmt                                         = "cannot find property %s in secret data for key: %q"
+)
+
+type Client struct {
+	onboardbase         SecretsClientInterface
+	onboardbaseAPIKey   string
+	onboardbasePasscode string
+	project             string
+	environment         string
+
+	kube      kclient.Client
+	store     *esv1beta1.OnboardbaseProvider
+	namespace string
+	storeKind string
+}
+
+// SecretsClientInterface defines the required Onboardbase Client methods.
+type SecretsClientInterface interface {
+	BaseURL() *url.URL
+	Authenticate() error
+	GetSecret(request onboardbaseClient.SecretRequest) (*onboardbaseClient.SecretResponse, error)
+	DeleteSecret(request onboardbaseClient.SecretRequest) error
+	GetSecrets(request onboardbaseClient.SecretsRequest) (*onboardbaseClient.SecretsResponse, error)
+}
+
+func (c *Client) setAuth(ctx context.Context) error {
+	credentialsSecret := &corev1.Secret{}
+	credentialsSecretName := c.store.Auth.OnboardbaseAPIKeyRef.Name
+	if credentialsSecretName == "" {
+		return fmt.Errorf(errOnboardbaseAPIKeySecretName)
+	}
+	objectKey := types.NamespacedName{
+		Name:      credentialsSecretName,
+		Namespace: c.namespace,
+	}
+	// only ClusterStore is allowed to set namespace (and then it's required)
+	if c.storeKind == esv1beta1.ClusterSecretStoreKind {
+		if c.store.Auth.OnboardbaseAPIKeyRef.Namespace == nil {
+			return fmt.Errorf(errInvalidClusterStoreMissingOnboardbaseAPIKeyNamespace)
+		}
+		objectKey.Namespace = *c.store.Auth.OnboardbaseAPIKeyRef.Namespace
+	}
+
+	err := c.kube.Get(ctx, objectKey, credentialsSecret)
+	if err != nil {
+		return fmt.Errorf(errFetchOnboardbaseAPIKeySecret, err)
+	}
+
+	onboardbaseAPIKey := credentialsSecret.Data[c.store.Auth.OnboardbaseAPIKeyRef.Key]
+	if (onboardbaseAPIKey == nil) || (len(onboardbaseAPIKey) == 0) {
+		return fmt.Errorf(errMissingOnboardbaseAPIKey, c.store.Auth.OnboardbaseAPIKeyRef.Key, credentialsSecretName)
+	}
+	c.onboardbaseAPIKey = string(onboardbaseAPIKey)
+
+	onboardbasePasscode := credentialsSecret.Data[c.store.Auth.OnboardbasePasscodeRef.Key]
+	if (onboardbasePasscode == nil) || (len(onboardbasePasscode) == 0) {
+		return fmt.Errorf(errMissingOnboardbasePasscode, c.store.Auth.OnboardbasePasscodeRef.Key, credentialsSecretName)
+	}
+
+	c.onboardbasePasscode = string(onboardbasePasscode)
+
+	return nil
+}
+
+func (c *Client) Validate() (esv1beta1.ValidationResult, error) {
+	timeout := 15 * time.Second
+	clientURL := c.onboardbase.BaseURL().String()
+
+	if err := utils.NetworkValidate(clientURL, timeout); err != nil {
+		return esv1beta1.ValidationResultError, err
+	}
+
+	if err := c.onboardbase.Authenticate(); err != nil {
+		return esv1beta1.ValidationResultError, err
+	}
+
+	return esv1beta1.ValidationResultReady, nil
+}
+
+func (c *Client) DeleteSecret(_ context.Context, _ esv1beta1.PushSecretRemoteRef) error {
+	// not implemented
+	return nil
+}
+
+func (c *Client) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1beta1.PushSecretData) error {
+	// not implemented
+	return nil
+}
+
+func (c *Client) GetSecret(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
+	request := onboardbaseClient.SecretRequest{
+		Project:     c.project,
+		Environment: c.environment,
+		Name:        ref.Key,
+	}
+
+	secret, err := c.onboardbase.GetSecret(request)
+	if err != nil {
+		return nil, fmt.Errorf(errGetSecret, ref.Key, err)
+	}
+
+	value := secret.Value
+
+	if len(ref.Property) > 0 {
+		jsonRes := gjson.Get(secret.Value, ref.Property)
+		if !jsonRes.Exists() {
+			return nil, fmt.Errorf(errSecretKeyFmt, ref.Property, ref.Key)
+		}
+		value = jsonRes.Raw
+	}
+
+	return []byte(value), nil
+}
+
+func (c *Client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	data, err := c.GetSecret(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	kv := make(map[string]json.RawMessage)
+	err = json.Unmarshal(data, &kv)
+	if err != nil {
+		return nil, fmt.Errorf(errUnmarshalSecretMap, ref.Key, err)
+	}
+
+	secretData := make(map[string][]byte)
+	for k, v := range kv {
+		var strVal string
+		err = json.Unmarshal(v, &strVal)
+		if err == nil {
+			secretData[k] = []byte(strVal)
+		} else {
+			secretData[k] = v
+		}
+	}
+	return secretData, nil
+}
+
+func (c *Client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
+	if len(ref.Tags) > 0 {
+		return nil, fmt.Errorf("find by tags not supported")
+	}
+
+	secrets, err := c.getSecrets(ctx)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if ref.Name == nil && ref.Path == nil {
+		return secrets, nil
+	}
+
+	var matcher *find.Matcher
+	if ref.Name != nil {
+		m, err := find.New(*ref.Name)
+		if err != nil {
+			return nil, err
+		}
+		matcher = m
+	}
+
+	selected := map[string][]byte{}
+	for key, value := range secrets {
+		if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
+			continue
+		}
+		selected[key] = value
+	}
+
+	return selected, nil
+}
+
+func (c *Client) Close(_ context.Context) error {
+	return nil
+}
+
+func (c *Client) getSecrets(_ context.Context) (map[string][]byte, error) {
+	request := onboardbaseClient.SecretsRequest{
+		Project:     c.project,
+		Environment: c.environment,
+	}
+
+	response, err := c.onboardbase.GetSecrets(request)
+	if err != nil {
+		return nil, fmt.Errorf(errGetSecrets, err)
+	}
+
+	return externalSecretsFormat(response.Secrets), nil
+}
+
+func externalSecretsFormat(secrets onboardbaseClient.Secrets) map[string][]byte {
+	converted := make(map[string][]byte, len(secrets))
+	for key, value := range secrets {
+		converted[key] = []byte(value)
+	}
+	return converted
+}

+ 436 - 0
pkg/provider/onboardbase/client/client.go

@@ -0,0 +1,436 @@
+/*
+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 client
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	aesdecrypt "github.com/Onboardbase/go-cryptojs-aes-decrypt/decrypt"
+)
+
+const HTTPTimeoutDuration = 20 * time.Second
+const ObbSecretsEndpointPath = "/secrets"
+
+const errUnableToDecrtypt = "unable to decrypt secret payload"
+
+type OnboardbaseClient struct {
+	baseURL             *url.URL
+	OnboardbaseAPIKey   string
+	VerifyTLS           bool
+	UserAgent           string
+	OnboardbasePassCode string
+	httpClient          *http.Client
+}
+
+type queryParams map[string]string
+
+type headers map[string]string
+type DeleteSecretsRequest struct {
+	SecretID string `json:"secretId,omitempty"`
+}
+
+type httpRequestBody []byte
+
+type Secrets map[string]string
+
+type RawSecret struct {
+	Key   string `json:"key,omitempty"`
+	Value string `json:"value,omitempty"`
+}
+
+type RawSecrets []RawSecret
+
+type APIError struct {
+	Err     error
+	Message string
+	Data    string
+}
+
+type apiResponse struct {
+	HTTPResponse *http.Response
+	Body         []byte
+}
+
+type apiErrorResponse struct {
+	Messages []string
+	Success  bool
+}
+
+type SecretRequest struct {
+	Environment string
+	Project     string
+	Name        string
+}
+
+type SecretsRequest struct {
+	Environment string
+	Project     string
+}
+
+type secretResponseBodyObject struct {
+	Title string `json:"title,omitempty"`
+	ID    string `json:"id,omitempty"`
+}
+
+type secretResponseSecrets struct {
+	ID    string `json:"id"`
+	Key   string `json:"key"`
+	Value string `json:"value"`
+}
+
+type secretResponseBodyData struct {
+	Project     secretResponseBodyObject `json:"project,omitempty"`
+	Environment secretResponseBodyObject `json:"environment,omitempty"`
+	Team        secretResponseBodyObject `json:"team,omitempty"`
+	Secrets     []secretResponseSecrets  `json:"secrets,omitempty"`
+	Status      string                   `json:"status"`
+	Message     string                   `json:"string"`
+}
+
+type secretResponseBody struct {
+	Data    secretResponseBodyData `json:"data,omitempty"`
+	Message string                 `json:"message,omitempty"`
+	Status  string                 `json:"status,omitempty"`
+}
+
+type SecretResponse struct {
+	Name  string
+	Value string
+}
+
+type SecretsResponse struct {
+	Secrets Secrets
+	Body    []byte
+}
+
+func NewOnboardbaseClient(onboardbaseAPIKey, onboardbasePasscode string) (*OnboardbaseClient, error) {
+	tlsConfig := &tls.Config{
+		MinVersion: tls.VersionTLS12,
+	}
+	httpTransport := &http.Transport{
+		DisableKeepAlives: true,
+		TLSClientConfig:   tlsConfig,
+	}
+	client := &OnboardbaseClient{
+		OnboardbaseAPIKey:   onboardbaseAPIKey,
+		OnboardbasePassCode: onboardbasePasscode,
+		VerifyTLS:           true,
+		UserAgent:           "onboardbase-external-secrets",
+		httpClient: &http.Client{
+			Timeout:   HTTPTimeoutDuration,
+			Transport: httpTransport,
+		},
+	}
+
+	if err := client.SetBaseURL("https://public.onboardbase.com/api/v1/"); err != nil {
+		return nil, &APIError{Err: err, Message: "setting base URL failed"}
+	}
+
+	return client, nil
+}
+
+func (c *OnboardbaseClient) BaseURL() *url.URL {
+	u := *c.baseURL
+	return &u
+}
+
+func (c *OnboardbaseClient) SetBaseURL(urlStr string) error {
+	baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
+
+	if err != nil {
+		return err
+	}
+	c.baseURL = baseURL
+	return nil
+}
+
+func (c *OnboardbaseClient) Authenticate() error {
+	_, err := c.performRequest(
+		&performRequestConfig{
+			path:    "/team/members",
+			method:  "GET",
+			headers: headers{},
+			params:  queryParams{},
+			body:    httpRequestBody{},
+		})
+
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *OnboardbaseClient) getSecretsFromPayload(data secretResponseBodyData) (map[string]string, error) {
+	kv := make(map[string]string)
+	for _, secret := range data.Secrets {
+		passphrase := c.OnboardbasePassCode
+		key, err := aesdecrypt.Run(secret.Key, passphrase)
+		if err != nil {
+			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
+		}
+		value, err := aesdecrypt.Run(secret.Value, passphrase)
+		if err != nil {
+			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Value}
+		}
+		kv[key] = value
+	}
+	return kv, nil
+}
+
+func (c *OnboardbaseClient) mapSecretsByPlainKey(data secretResponseBodyData) (map[string]secretResponseSecrets, error) {
+	kv := make(map[string]secretResponseSecrets)
+	for _, secret := range data.Secrets {
+		passphrase := c.OnboardbasePassCode
+		key, err := aesdecrypt.Run(secret.Key, passphrase)
+		if err != nil {
+			return nil, &APIError{Err: err, Message: errUnableToDecrtypt, Data: secret.Key}
+		}
+		kv[key] = secret
+	}
+	return kv, nil
+}
+
+func (c *OnboardbaseClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
+	response, err := c.performRequest(
+		&performRequestConfig{
+			path:    ObbSecretsEndpointPath,
+			method:  "GET",
+			headers: headers{},
+			params:  request.buildQueryParams(),
+			body:    httpRequestBody{},
+		})
+	if err != nil {
+		return nil, err
+	}
+
+	var data secretResponseBody
+	if err := json.Unmarshal(response.Body, &data); err != nil {
+		return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
+	}
+
+	secrets, _ := c.getSecretsFromPayload(data.Data)
+	secret := secrets[request.Name]
+
+	if secret == "" {
+		return nil, &APIError{Message: fmt.Sprintf("secret %s for project '%s' and environment '%s' not found", request.Name, request.Project, request.Environment)}
+	}
+
+	return &SecretResponse{Name: request.Name, Value: secrets[request.Name]}, nil
+}
+
+func (c *OnboardbaseClient) DeleteSecret(request SecretRequest) error {
+	secretsrequest := SecretsRequest{
+		Project:     request.Project,
+		Environment: request.Environment,
+	}
+
+	secretsData, _, err := c.makeGetSecretsRequest(secretsrequest)
+	if err != nil {
+		return err
+	}
+	secrets, err := c.mapSecretsByPlainKey(secretsData.Data)
+	if err != nil {
+		return err
+	}
+	secret, ok := secrets[request.Name]
+	if !ok || secret.ID == "" {
+		return nil
+	}
+
+	params := request.buildQueryParams()
+	deleteSecretDto := &DeleteSecretsRequest{
+		SecretID: secret.ID,
+	}
+	body, jsonErr := json.Marshal(deleteSecretDto)
+	if jsonErr != nil {
+		return &APIError{Err: jsonErr, Message: "unable to unmarshal delete secrets payload"}
+	}
+	_, err = c.performRequest(&performRequestConfig{
+		path:    ObbSecretsEndpointPath,
+		method:  "DELETE",
+		headers: headers{},
+		params:  params,
+		body:    body,
+	})
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *OnboardbaseClient) makeGetSecretsRequest(request SecretsRequest) (*secretResponseBody, *apiResponse, error) {
+	response, apiErr := c.performRequest(&performRequestConfig{
+		path:    ObbSecretsEndpointPath,
+		method:  "GET",
+		headers: headers{},
+		params:  request.buildQueryParams(),
+		body:    httpRequestBody{},
+	})
+	if apiErr != nil {
+		return nil, nil, apiErr
+	}
+
+	var data *secretResponseBody
+	if err := json.Unmarshal(response.Body, &data); err != nil {
+		return nil, nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
+	}
+	return data, response, nil
+}
+
+func (c *OnboardbaseClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
+	data, response, err := c.makeGetSecretsRequest(request)
+	if err != nil {
+		return nil, err
+	}
+
+	secrets, _ := c.getSecretsFromPayload(data.Data)
+	return &SecretsResponse{Secrets: secrets, Body: response.Body}, nil
+}
+
+func (r *SecretsRequest) buildQueryParams() queryParams {
+	params := queryParams{}
+
+	if r.Project != "" {
+		params["project"] = r.Project
+	}
+
+	if r.Environment != "" {
+		params["environment"] = r.Environment
+	}
+
+	return params
+}
+
+func (r *SecretRequest) buildQueryParams() queryParams {
+	params := queryParams{}
+
+	if r.Project != "" {
+		params["project"] = r.Project
+	}
+
+	if r.Environment != "" {
+		params["environment"] = r.Environment
+	}
+
+	return params
+}
+
+type performRequestConfig struct {
+	path    string
+	method  string
+	headers headers
+	params  queryParams
+	body    httpRequestBody
+}
+
+func (c *OnboardbaseClient) performRequest(config *performRequestConfig) (*apiResponse, error) {
+	urlStr := c.BaseURL().String() + config.path
+	reqURL, err := url.Parse(urlStr)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
+	}
+
+	var bodyReader io.Reader
+	if config.body != nil {
+		bodyReader = bytes.NewReader(config.body)
+	} else {
+		bodyReader = http.NoBody
+	}
+
+	// timeout this request after 20 seconds
+	ctx, cancel := context.WithTimeout(context.Background(), HTTPTimeoutDuration)
+	defer cancel()
+
+	req, err := http.NewRequestWithContext(ctx, config.method, reqURL.String(), bodyReader)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
+	}
+
+	req.Header.Set("content-type", "application/json")
+	req.Header.Set("user-agent", c.UserAgent)
+	req.Header.Set("api_key", c.OnboardbaseAPIKey)
+
+	for key, value := range config.headers {
+		req.Header.Set(key, value)
+	}
+
+	query := req.URL.Query()
+	for key, value := range config.params {
+		query.Add(key, value)
+	}
+	req.URL.RawQuery = query.Encode()
+
+	r, err := c.httpClient.Do(req)
+
+	if err != nil {
+		return nil, &APIError{Err: err, Message: "unable to load response"}
+	}
+	defer r.Body.Close()
+
+	bodyResponse, err := io.ReadAll(r.Body)
+	if err != nil {
+		return nil, &APIError{Err: err, Message: "unable to read entire response body"}
+	}
+
+	response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
+	success := isSuccess(r.StatusCode)
+
+	if !success {
+		return handlePerformRequestFailure(response)
+	}
+
+	if success && err != nil {
+		return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
+	}
+	return response, nil
+}
+
+func handlePerformRequestFailure(response *apiResponse) (*apiResponse, *APIError) {
+	if contentType := response.HTTPResponse.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
+		var errResponse apiErrorResponse
+		err := json.Unmarshal(response.Body, &errResponse)
+		if err != nil {
+			return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
+		}
+		return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
+	}
+	return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", response.HTTPResponse.StatusCode, len(response.Body)), Message: "unable to load response"}
+}
+
+func isSuccess(statusCode int) bool {
+	return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
+}
+
+func (e *APIError) Error() string {
+	message := fmt.Sprintf("Onboardbase API Client Error: %s", e.Message)
+	if underlyingError := e.Err; underlyingError != nil {
+		message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
+	}
+	if e.Data != "" {
+		message = fmt.Sprintf("%s\nData: %s", message, e.Data)
+	}
+	return message
+}

+ 59 - 0
pkg/provider/onboardbase/fake/fake.go

@@ -0,0 +1,59 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package fake
+
+import (
+	"fmt"
+	"net/url"
+
+	"github.com/google/go-cmp/cmp"
+
+	"github.com/external-secrets/external-secrets/pkg/provider/onboardbase/client"
+)
+
+type OnboardbaseClient struct {
+	getSecret func(request client.SecretRequest) (*client.SecretResponse, error)
+}
+
+func (obbc *OnboardbaseClient) BaseURL() *url.URL {
+	return &url.URL{Scheme: "https", Host: "public.onboardbase.com"}
+}
+
+func (obbc *OnboardbaseClient) Authenticate() error {
+	return nil
+}
+
+func (obbc *OnboardbaseClient) GetSecret(request client.SecretRequest) (*client.SecretResponse, error) {
+	return obbc.getSecret(request)
+}
+
+func (obbc *OnboardbaseClient) GetSecrets(_ client.SecretsRequest) (*client.SecretsResponse, error) {
+	return &client.SecretsResponse{}, nil
+}
+
+func (obbc *OnboardbaseClient) DeleteSecret(_ client.SecretRequest) error {
+	return nil
+}
+
+func (obbc *OnboardbaseClient) WithValue(request client.SecretRequest, response *client.SecretResponse, err error) {
+	if obbc != nil {
+		obbc.getSecret = func(requestIn client.SecretRequest) (*client.SecretResponse, error) {
+			if !cmp.Equal(requestIn, request) {
+				return nil, fmt.Errorf("unexpected test argument")
+			}
+			return response, err
+		}
+	}
+}

+ 358 - 0
pkg/provider/onboardbase/onboardbase_test.go

@@ -0,0 +1,358 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package onboardbase
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	v1 "github.com/external-secrets/external-secrets/apis/meta/v1"
+	"github.com/external-secrets/external-secrets/pkg/provider/onboardbase/client"
+	"github.com/external-secrets/external-secrets/pkg/provider/onboardbase/fake"
+)
+
+const (
+	validSecretName        = "API_KEY"
+	validSecretValue       = "3a3ea4f5"
+	onboardbaseProject     = "ONBOARDBASE_PROJECT"
+	onboardbaseEnvironment = "development"
+	onboardbaseProjectVal  = "payments-service"
+	missingSecret          = "INVALID_NAME"
+	invalidSecret          = "unknown_project"
+	missingSecretErr       = "could not get secret"
+)
+
+type onboardbaseTestCase struct {
+	label               string
+	fakeClient          *fake.OnboardbaseClient
+	request             client.SecretRequest
+	response            *client.SecretResponse
+	remoteRef           *esv1beta1.ExternalSecretDataRemoteRef
+	PushSecretRemoteRef esv1beta1.PushSecretRemoteRef
+	apiErr              error
+	expectError         string
+	expectedSecret      string
+	expectedData        map[string][]byte
+}
+
+func makeValidAPIRequest() client.SecretRequest {
+	return client.SecretRequest{
+		Name: validSecretName,
+	}
+}
+
+func makeValidAPIOutput() *client.SecretResponse {
+	return &client.SecretResponse{
+		Name:  validSecretName,
+		Value: validSecretValue,
+	}
+}
+
+func makeValidRemoteRef() *esv1beta1.ExternalSecretDataRemoteRef {
+	return &esv1beta1.ExternalSecretDataRemoteRef{
+		Key: validSecretName,
+	}
+}
+
+type pushRemoteRef struct {
+	secretKey string
+}
+
+func (pRef pushRemoteRef) GetProperty() string {
+	return ""
+}
+
+func (pRef pushRemoteRef) GetRemoteKey() string {
+	return pRef.secretKey
+}
+
+func makeValidPushRemoteRef(key string) esv1beta1.PushSecretRemoteRef {
+	return pushRemoteRef{
+		secretKey: key,
+	}
+}
+
+func makeValidOnboardbaseTestCase() *onboardbaseTestCase {
+	return &onboardbaseTestCase{
+		fakeClient:          &fake.OnboardbaseClient{},
+		request:             makeValidAPIRequest(),
+		response:            makeValidAPIOutput(),
+		remoteRef:           makeValidRemoteRef(),
+		PushSecretRemoteRef: makeValidPushRemoteRef(validSecretName),
+		apiErr:              nil,
+		expectError:         "",
+		expectedSecret:      "",
+		expectedData:        make(map[string][]byte),
+	}
+}
+
+func makeValidOnboardbaseTestCaseCustom(tweaks ...func(pstc *onboardbaseTestCase)) *onboardbaseTestCase {
+	pstc := makeValidOnboardbaseTestCase()
+	for _, fn := range tweaks {
+		fn(pstc)
+	}
+	pstc.fakeClient.WithValue(pstc.request, pstc.response, pstc.apiErr)
+	return pstc
+}
+
+func TestGetSecret(t *testing.T) {
+	setSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "set secret"
+		pstc.request.Name = onboardbaseProject
+		pstc.response.Name = onboardbaseProject
+		pstc.response.Value = onboardbaseProjectVal
+		pstc.expectedSecret = onboardbaseProjectVal
+		pstc.remoteRef.Key = onboardbaseProject
+	}
+
+	setMissingSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid missing secret"
+		pstc.remoteRef.Key = missingSecret
+		pstc.request.Name = missingSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setInvalidSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid secret name format"
+		pstc.remoteRef.Key = invalidSecret
+		pstc.request.Name = invalidSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setClientError := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid client error"
+		pstc.response = &client.SecretResponse{}
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*onboardbaseTestCase{
+		makeValidOnboardbaseTestCaseCustom(setSecret),
+		makeValidOnboardbaseTestCaseCustom(setMissingSecret),
+		makeValidOnboardbaseTestCaseCustom(setInvalidSecret),
+		makeValidOnboardbaseTestCaseCustom(setClientError),
+	}
+
+	c := Client{}
+	for k, tc := range testCases {
+		c.onboardbase = tc.fakeClient
+		out, err := c.GetSecret(context.Background(), *tc.remoteRef)
+		if !ErrorContains(err, tc.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
+		}
+		if err == nil && !cmp.Equal(string(out), tc.expectedSecret) {
+			t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedSecret, string(out))
+		}
+	}
+}
+
+func TestDeleteSecret(t *testing.T) {
+	setMissingSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid missing secret"
+		pstc.remoteRef.Key = missingSecret
+		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(missingSecret)
+		pstc.request.Name = missingSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	setInvalidSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid secret name format"
+		pstc.remoteRef.Key = invalidSecret
+		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(invalidSecret)
+		pstc.request.Name = invalidSecret
+		pstc.response = nil
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	deleteSecret := func(pstc *onboardbaseTestCase) {
+		pstc.label = "delete secret successfully"
+		pstc.remoteRef.Key = validSecretName
+		pstc.PushSecretRemoteRef = makeValidPushRemoteRef(validSecretName)
+		pstc.request.Name = validSecretName
+		pstc.response = nil
+		pstc.apiErr = nil
+	}
+
+	testCases := []*onboardbaseTestCase{
+		makeValidOnboardbaseTestCaseCustom(setMissingSecret),
+		makeValidOnboardbaseTestCaseCustom(setInvalidSecret),
+		makeValidOnboardbaseTestCaseCustom(deleteSecret),
+	}
+
+	c := Client{}
+	for k, tc := range testCases {
+		c.onboardbase = tc.fakeClient
+		err := c.DeleteSecret(context.Background(), tc.PushSecretRemoteRef)
+		if err != nil && !ErrorContains(err, tc.expectError) {
+			t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), tc.expectError)
+		}
+	}
+}
+
+func TestGetSecretMap(t *testing.T) {
+	simpleJSON := func(pstc *onboardbaseTestCase) {
+		pstc.label = "valid unmarshalling"
+		pstc.response.Value = `{"API_KEY":"3a3ea4f5"}`
+		pstc.expectedData["API_KEY"] = []byte("3a3ea4f5")
+	}
+
+	complexJSON := func(pstc *onboardbaseTestCase) {
+		pstc.label = "valid unmarshalling for nested json"
+		pstc.response.Value = `{"API_KEY": "3a3ea4fs5", "AUTH_SA": {"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}}`
+		pstc.expectedData["API_KEY"] = []byte("3a3ea4fs5")
+		pstc.expectedData["AUTH_SA"] = []byte(`{"appID": "a1ea-48bd-8749-b6f5ec3c5a1f"}`)
+	}
+
+	setInvalidJSON := func(pstc *onboardbaseTestCase) {
+		pstc.label = "invalid json"
+		pstc.response.Value = `{"API_KEY": "3a3ea4f`
+		pstc.expectError = "unable to unmarshal secret"
+	}
+
+	setAPIError := func(pstc *onboardbaseTestCase) {
+		pstc.label = "client error"
+		pstc.response = &client.SecretResponse{}
+		pstc.expectError = missingSecretErr
+		pstc.apiErr = fmt.Errorf("")
+	}
+
+	testCases := []*onboardbaseTestCase{
+		makeValidOnboardbaseTestCaseCustom(simpleJSON),
+		makeValidOnboardbaseTestCaseCustom(complexJSON),
+		makeValidOnboardbaseTestCaseCustom(setInvalidJSON),
+		makeValidOnboardbaseTestCaseCustom(setAPIError),
+	}
+
+	d := Client{}
+	for k, tc := range testCases {
+		t.Run(tc.label, func(t *testing.T) {
+			d.onboardbase = tc.fakeClient
+			out, err := d.GetSecretMap(context.Background(), *tc.remoteRef)
+			if !ErrorContains(err, tc.expectError) {
+				t.Errorf("[%d] unexpected error: %q, expected: %q", k, err.Error(), tc.expectError)
+			}
+			if err == nil && !cmp.Equal(out, tc.expectedData) {
+				t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, tc.expectedData, out)
+			}
+		})
+	}
+}
+
+func ErrorContains(out error, want string) bool {
+	if out == nil {
+		return want == ""
+	}
+	if want == "" {
+		return false
+	}
+	return strings.Contains(out.Error(), want)
+}
+
+type storeModifier func(*esv1beta1.SecretStore) *esv1beta1.SecretStore
+
+func makeSecretStore(fn ...storeModifier) *esv1beta1.SecretStore {
+	store := &esv1beta1.SecretStore{
+		Spec: esv1beta1.SecretStoreSpec{
+			Provider: &esv1beta1.SecretStoreProvider{
+				Onboardbase: &esv1beta1.OnboardbaseProvider{
+					Auth: &esv1beta1.OnboardbaseAuthSecretRef{},
+				},
+			},
+		},
+	}
+	for _, f := range fn {
+		store = f(store)
+	}
+	return store
+}
+
+func withAuth(name, key string, namespace *string, passcode string) storeModifier {
+	return func(store *esv1beta1.SecretStore) *esv1beta1.SecretStore {
+		store.Spec.Provider.Onboardbase.Auth.OnboardbaseAPIKeyRef = v1.SecretKeySelector{
+			Name:      name,
+			Key:       key,
+			Namespace: namespace,
+		}
+		store.Spec.Provider.Onboardbase.Auth.OnboardbasePasscodeRef = v1.SecretKeySelector{
+			Name:      passcode,
+			Key:       passcode,
+			Namespace: namespace,
+		}
+		return store
+	}
+}
+
+type ValidateStoreTestCase struct {
+	label string
+	store *esv1beta1.SecretStore
+	err   error
+}
+
+func TestValidateStore(t *testing.T) {
+	namespace := "ns"
+	secretName := "onboardbase-api-key-secret"
+	testCases := []ValidateStoreTestCase{
+		{
+			label: "invalid store missing onboardbaseAPIKey.name",
+			store: makeSecretStore(withAuth("", "", nil, "")),
+			err:   fmt.Errorf("invalid store: onboardbaseAPIKey.name cannot be empty"),
+		},
+		{
+			label: "invalid store missing onboardbasePasscode.name",
+			store: makeSecretStore(withAuth(secretName, "", nil, "")),
+			err:   fmt.Errorf("invalid store: onboardbasePasscode.name cannot be empty"),
+		},
+		{
+			label: "invalid store namespace not allowed",
+			store: makeSecretStore(withAuth(secretName, "", &namespace, "passcode")),
+			err:   fmt.Errorf("invalid store: namespace not allowed with namespaced SecretStore"),
+		},
+		{
+			label: "valid provide optional onboardbaseAPIKey.key",
+			store: makeSecretStore(withAuth(secretName, "customSecretKey", nil, "passcode")),
+			err:   nil,
+		},
+		{
+			label: "valid namespace not set",
+			store: makeSecretStore(withAuth(secretName, "", nil, "passcode")),
+			err:   nil,
+		},
+	}
+	p := Provider{}
+	for _, tc := range testCases {
+		t.Run(tc.label, func(t *testing.T) {
+			_, 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)
+			}
+		})
+	}
+}

+ 106 - 0
pkg/provider/onboardbase/provider.go

@@ -0,0 +1,106 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliec.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package onboardbase
+
+import (
+	"context"
+	"fmt"
+
+	kclient "sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	oClient "github.com/external-secrets/external-secrets/pkg/provider/onboardbase/client"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errNewClient        = "unable to create OnboardbaseClient : %s"
+	errInvalidStore     = "invalid store: %s"
+	errOnboardbaseStore = "missing or invalid Onboardbase SecretStore"
+)
+
+// Provider is a Onboardbase secrets provider implementing NewClient and ValidateStore for the esv1beta1.Provider interface.
+type Provider struct{}
+
+// https://github.com/external-secrets/external-secrets/issues/644
+var _ esv1beta1.SecretsClient = &Client{}
+var _ esv1beta1.Provider = &Provider{}
+
+func init() {
+	esv1beta1.Register(&Provider{}, &esv1beta1.SecretStoreProvider{
+		Onboardbase: &esv1beta1.OnboardbaseProvider{},
+	})
+}
+
+func (p *Provider) Capabilities() esv1beta1.SecretStoreCapabilities {
+	return esv1beta1.SecretStoreReadOnly
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Onboardbase == nil {
+		return nil, fmt.Errorf(errOnboardbaseStore)
+	}
+
+	onboardbaseStoreSpec := storeSpec.Provider.Onboardbase
+
+	client := &Client{
+		kube:      kube,
+		store:     onboardbaseStoreSpec,
+		namespace: namespace,
+		storeKind: store.GetObjectKind().GroupVersionKind().Kind,
+	}
+
+	if err := client.setAuth(ctx); err != nil {
+		return nil, err
+	}
+
+	onboardbaseClient, err := oClient.NewOnboardbaseClient(client.onboardbaseAPIKey, client.onboardbasePasscode)
+	if err != nil {
+		return nil, fmt.Errorf(errNewClient, err)
+	}
+
+	client.onboardbase = onboardbaseClient
+	client.project = client.store.Project
+	client.environment = client.store.Environment
+
+	return client, nil
+}
+
+func (p *Provider) ValidateStore(store esv1beta1.GenericStore) (admission.Warnings, error) {
+	storeSpec := store.GetSpec()
+	onboardbaseStoreSpec := storeSpec.Provider.Onboardbase
+	onboardbaseAPIKeySecretRef := onboardbaseStoreSpec.Auth.OnboardbaseAPIKeyRef
+	if err := utils.ValidateSecretSelector(store, onboardbaseAPIKeySecretRef); err != nil {
+		return nil, fmt.Errorf(errInvalidStore, err)
+	}
+
+	if onboardbaseAPIKeySecretRef.Name == "" {
+		return nil, fmt.Errorf(errInvalidStore, "onboardbaseAPIKey.name cannot be empty")
+	}
+
+	onboardbasePasscodeKeySecretRef := onboardbaseStoreSpec.Auth.OnboardbasePasscodeRef
+	if err := utils.ValidateSecretSelector(store, onboardbasePasscodeKeySecretRef); err != nil {
+		return nil, fmt.Errorf(errInvalidStore, err)
+	}
+
+	if onboardbasePasscodeKeySecretRef.Name == "" {
+		return nil, fmt.Errorf(errInvalidStore, "onboardbasePasscode.name cannot be empty")
+	}
+
+	return nil, nil
+}

+ 1 - 0
pkg/provider/register/register.go

@@ -33,6 +33,7 @@ import (
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/keepersecurity"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/kubernetes"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/onboardbase"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/onepassword"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/oracle"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/pulumi"
 	_ "github.com/external-secrets/external-secrets/pkg/provider/pulumi"