Explorar el Código

feat(provider/infisical): azure auth and refactor to go sdk (#4854)

Daniel Hougaard hace 11 meses
padre
commit
0c8bff827d

+ 9 - 0
apis/externalsecrets/v1/secretsstore_infisical_types.go

@@ -25,9 +25,18 @@ type UniversalAuthCredentials struct {
 	ClientSecret esmeta.SecretKeySelector `json:"clientSecret"`
 }
 
+type AzureAuthCredentials struct {
+	// +kubebuilder:validation:Required
+	IdentityID esmeta.SecretKeySelector `json:"identityId"`
+	// +optional
+	Resource esmeta.SecretKeySelector `json:"resource"`
+}
+
 type InfisicalAuth struct {
 	// +optional
 	UniversalAuthCredentials *UniversalAuthCredentials `json:"universalAuthCredentials,omitempty"`
+	// +optional
+	AzureAuthCredentials *AzureAuthCredentials `json:"azureAuthCredentials,omitempty"`
 }
 
 type MachineIdentityScopeInWorkspace struct {

+ 22 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -326,6 +326,23 @@ func (in *AuthorizationProtocol) DeepCopy() *AuthorizationProtocol {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AzureAuthCredentials) DeepCopyInto(out *AzureAuthCredentials) {
+	*out = *in
+	in.IdentityID.DeepCopyInto(&out.IdentityID)
+	in.Resource.DeepCopyInto(&out.Resource)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AzureAuthCredentials.
+func (in *AzureAuthCredentials) DeepCopy() *AzureAuthCredentials {
+	if in == nil {
+		return nil
+	}
+	out := new(AzureAuthCredentials)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AzureKVAuth) DeepCopyInto(out *AzureKVAuth) {
 	*out = *in
 	if in.ClientID != nil {
@@ -2008,6 +2025,11 @@ func (in *InfisicalAuth) DeepCopyInto(out *InfisicalAuth) {
 		*out = new(UniversalAuthCredentials)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.AzureAuthCredentials != nil {
+		in, out := &in.AzureAuthCredentials, &out.AzureAuthCredentials
+		*out = new(AzureAuthCredentials)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalAuth.

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

@@ -2174,6 +2174,69 @@ spec:
                         description: Auth configures how the Operator authenticates
                           with the Infisical API
                         properties:
+                          azureAuthCredentials:
+                            properties:
+                              identityId:
+                                description: |-
+                                  A reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              resource:
+                                description: |-
+                                  A reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - identityId
+                            type: object
                           universalAuthCredentials:
                             properties:
                               clientId:

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

@@ -2174,6 +2174,69 @@ spec:
                         description: Auth configures how the Operator authenticates
                           with the Infisical API
                         properties:
+                          azureAuthCredentials:
+                            properties:
+                              identityId:
+                                description: |-
+                                  A reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                              resource:
+                                description: |-
+                                  A reference to a specific 'key' within a Secret resource.
+                                  In some instances, `key` is a required field.
+                                properties:
+                                  key:
+                                    description: |-
+                                      A key in the referenced Secret.
+                                      Some instances of this field may be defaulted, in others it may be required.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[-._a-zA-Z0-9]+$
+                                    type: string
+                                  name:
+                                    description: The name of the Secret resource being
+                                      referred to.
+                                    maxLength: 253
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                    type: string
+                                  namespace:
+                                    description: |-
+                                      The namespace of the Secret resource being referred to.
+                                      Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                    maxLength: 63
+                                    minLength: 1
+                                    pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                    type: string
+                                type: object
+                            required:
+                            - identityId
+                            type: object
                           universalAuthCredentials:
                             properties:
                               clientId:

+ 122 - 0
deploy/crds/bundle.yaml

@@ -4011,6 +4011,67 @@ spec:
                         auth:
                           description: Auth configures how the Operator authenticates with the Infisical API
                           properties:
+                            azureAuthCredentials:
+                              properties:
+                                identityId:
+                                  description: |-
+                                    A reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                resource:
+                                  description: |-
+                                    A reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - identityId
+                              type: object
                             universalAuthCredentials:
                               properties:
                                 clientId:
@@ -14161,6 +14222,67 @@ spec:
                         auth:
                           description: Auth configures how the Operator authenticates with the Infisical API
                           properties:
+                            azureAuthCredentials:
+                              properties:
+                                identityId:
+                                  description: |-
+                                    A reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                                resource:
+                                  description: |-
+                                    A reference to a specific 'key' within a Secret resource.
+                                    In some instances, `key` is a required field.
+                                  properties:
+                                    key:
+                                      description: |-
+                                        A key in the referenced Secret.
+                                        Some instances of this field may be defaulted, in others it may be required.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[-._a-zA-Z0-9]+$
+                                      type: string
+                                    name:
+                                      description: The name of the Secret resource being referred to.
+                                      maxLength: 253
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                                      type: string
+                                    namespace:
+                                      description: |-
+                                        The namespace of the Secret resource being referred to.
+                                        Ignored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.
+                                      maxLength: 63
+                                      minLength: 1
+                                      pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                                      type: string
+                                  type: object
+                              required:
+                                - identityId
+                              type: object
                             universalAuthCredentials:
                               properties:
                                 clientId:

+ 56 - 0
docs/api/spec.md

@@ -796,6 +796,49 @@ NTLMProtocol
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1.AzureAuthCredentials">AzureAuthCredentials
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.InfisicalAuth">InfisicalAuth</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>identityId</code></br>
+<em>
+<a href="https://pkg.go.dev/github.com/external-secrets/external-secrets/apis/meta/v1#SecretKeySelector">
+External Secrets meta/v1.SecretKeySelector
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>resource</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>
+<em>(Optional)</em>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.AzureAuthType">AzureAuthType
 (<code>string</code> alias)</p></h3>
 <p>
@@ -5488,6 +5531,19 @@ UniversalAuthCredentials
 <em>(Optional)</em>
 </td>
 </tr>
+<tr>
+<td>
+<code>azureAuthCredentials</code></br>
+<em>
+<a href="#external-secrets.io/v1.AzureAuthCredentials">
+AzureAuthCredentials
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
 </tbody>
 </table>
 <h3 id="external-secrets.io/v1.InfisicalProvider">InfisicalProvider

+ 3 - 0
go.mod

@@ -101,6 +101,7 @@ require (
 	github.com/hashicorp/golang-lru v1.0.2
 	github.com/hashicorp/vault/api/auth/aws v0.10.0
 	github.com/hashicorp/vault/api/auth/userpass v0.10.0
+	github.com/infisical/go-sdk v0.5.97
 	github.com/keeper-security/secrets-manager-go/core v1.6.4
 	github.com/lestrrat-go/jwx/v2 v2.1.6
 	github.com/maxbrunsfeld/counterfeiter/v6 v6.11.2
@@ -181,6 +182,7 @@ require (
 	github.com/go-openapi/spec v0.21.0 // indirect
 	github.com/go-openapi/validate v0.24.0 // indirect
 	github.com/go-playground/validator/v10 v10.26.0 // indirect
+	github.com/go-resty/resty/v2 v2.13.1 // indirect
 	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
 	github.com/godbus/dbus/v5 v5.1.0 // indirect
@@ -193,6 +195,7 @@ require (
 	github.com/google/s2a-go v0.1.9 // indirect
 	github.com/hashicorp/go-secure-stdlib/awsutil v0.3.0 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
+	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
 	github.com/hashicorp/hcl/v2 v2.23.0 // indirect
 	github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect

+ 7 - 0
go.sum

@@ -430,6 +430,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
 github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-resty/resty/v2 v2.13.1 h1:x+LHXBI2nMB1vqndymf26quycC4aggYJ7DECYbiz03g=
+github.com/go-resty/resty/v2 v2.13.1/go.mod h1:GznXlLxkq6Nh4sU59rPmUw3VtgpO3aS96ORAI6Q7d+0=
 github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
 github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
 github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
@@ -591,6 +593,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
+github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
 github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
 github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
@@ -616,6 +620,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx8
 github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/infisical/go-sdk v0.5.97 h1:veOi6Hduda6emtwjdUI5SBg2qd2iDQc5xLKqZ15KSoM=
+github.com/infisical/go-sdk v0.5.97/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
@@ -1201,6 +1207,7 @@ golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

+ 0 - 307
pkg/provider/infisical/api/api.go

@@ -1,307 +0,0 @@
-/*
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-	http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or impliec.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package api
-
-import (
-	"bytes"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"strconv"
-
-	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
-	"github.com/external-secrets/external-secrets/pkg/metrics"
-	"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
-)
-
-type InfisicalClient struct {
-	BaseURL *url.URL
-	client  *http.Client
-	token   string
-}
-
-type InfisicalApis interface {
-	MachineIdentityLoginViaUniversalAuth(data MachineIdentityUniversalAuthLoginRequest) (*MachineIdentityDetailsResponse, error)
-	GetSecretsV3(data GetSecretsV3Request) (map[string]string, error)
-	GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error)
-	RevokeAccessToken() error
-}
-
-const (
-	machineIdentityLoginViaUniversalAuth = "MachineIdentityLoginViaUniversalAuth"
-	getSecretsV3                         = "GetSecretsV3"
-	getSecretByKeyV3                     = "GetSecretByKeyV3"
-	revokeAccessToken                    = "RevokeAccessToken"
-)
-
-const UserAgentName = "k8-external-secrets-operator"
-
-var errJSONUnmarshal = errors.New("unable to unmarshal API response")
-var errNoAccessToken = errors.New("unexpected error: no access token available to revoke")
-var errAccessTokenAlreadyRetrieved = errors.New("unexpected error: access token was already retrieved")
-
-type InfisicalAPIError struct {
-	StatusCode int
-	Err        any
-	Message    any
-	Details    any
-}
-
-func (e *InfisicalAPIError) Error() string {
-	if e.Details != nil {
-		detailsJSON, _ := json.Marshal(e.Details)
-		return fmt.Sprintf("API error (%d): error=%v message=%v, details=%s", e.StatusCode, e.Err, e.Message, string(detailsJSON))
-	} else {
-		return fmt.Sprintf("API error (%d): error=%v message=%v", e.StatusCode, e.Err, e.Message)
-	}
-}
-
-// checkError checks for an error on the http response and generates an appropriate error if one is
-// found.
-func checkError(resp *http.Response) error {
-	if resp.StatusCode >= 200 && resp.StatusCode < 400 {
-		return nil
-	}
-
-	var buf bytes.Buffer
-	_, err := buf.ReadFrom(resp.Body)
-	if err != nil {
-		return fmt.Errorf("API error (%d) and failed to read response body: %w", resp.StatusCode, err)
-	}
-
-	// Attempt to unmarshal the response body into an InfisicalAPIErrorResponse.
-	var errRes InfisicalAPIErrorResponse
-	err = json.Unmarshal(buf.Bytes(), &errRes)
-	// Non-200 errors that cannot be unmarshaled must be handled, as errors could come from outside of
-	// Infisical.
-	if err != nil {
-		return fmt.Errorf("API error (%d), could not unmarshal error response: %w", resp.StatusCode, err)
-	} else if errRes.StatusCode == 0 {
-		// When the InfisicalResponse has a zero-value status code, then the
-		// response was either malformed or not from Infisical. Instead, just return
-		// the error string from the response.
-		return fmt.Errorf("API error (%d): %s", resp.StatusCode, buf.String())
-	} else {
-		return &InfisicalAPIError{
-			StatusCode: resp.StatusCode,
-			Message:    errRes.Message,
-			Err:        errRes.Error,
-			Details:    errRes.Details,
-		}
-	}
-}
-
-func NewAPIClient(baseURL string, client *http.Client) (*InfisicalClient, error) {
-	baseParsedURL, err := url.Parse(baseURL)
-	if err != nil {
-		return nil, err
-	}
-
-	api := &InfisicalClient{
-		BaseURL: baseParsedURL,
-		client:  client,
-	}
-
-	return api, nil
-}
-
-func (a *InfisicalClient) SetTokenViaMachineIdentity(clientID, clientSecret string) error {
-	if a.token != "" {
-		return errAccessTokenAlreadyRetrieved
-	}
-
-	var loginResponse MachineIdentityDetailsResponse
-	err := a.do(
-		"api/v1/auth/universal-auth/login",
-		http.MethodPost,
-		map[string]string{},
-		MachineIdentityUniversalAuthLoginRequest{
-			ClientID:     clientID,
-			ClientSecret: clientSecret,
-		},
-		&loginResponse,
-	)
-	metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaUniversalAuth, err)
-
-	if err != nil {
-		return err
-	}
-
-	a.token = loginResponse.AccessToken
-	return nil
-}
-
-func (a *InfisicalClient) RevokeAccessToken() error {
-	if a.token == "" {
-		return errNoAccessToken
-	}
-
-	var revokeResponse RevokeMachineIdentityAccessTokenResponse
-	err := a.do(
-		"api/v1/auth/token/revoke",
-		http.MethodPost,
-		map[string]string{},
-		RevokeMachineIdentityAccessTokenRequest{AccessToken: a.token},
-		&revokeResponse,
-	)
-	metrics.ObserveAPICall(constants.ProviderName, revokeAccessToken, err)
-
-	if err != nil {
-		return err
-	}
-
-	a.token = ""
-	return nil
-}
-
-func (a *InfisicalClient) resolveEndpoint(path string) string {
-	return a.BaseURL.ResolveReference(&url.URL{Path: path}).String()
-}
-
-func (a *InfisicalClient) addHeaders(r *http.Request) {
-	if a.token != "" {
-		r.Header.Add("Authorization", "Bearer "+a.token)
-	}
-	r.Header.Add("User-Agent", UserAgentName)
-	r.Header.Add("Content-Type", "application/json")
-}
-
-// do is a generic function that makes an API call to the Infisical API, and handle the response
-// (including if an API error is returned).
-func (a *InfisicalClient) do(endpoint, method string, params map[string]string, body, response any) error {
-	endpointURL := a.resolveEndpoint(endpoint)
-
-	bodyReader, err := MarshalReqBody(body)
-	if err != nil {
-		return err
-	}
-
-	r, err := http.NewRequest(method, endpointURL, bodyReader)
-	if err != nil {
-		return err
-	}
-
-	a.addHeaders(r)
-
-	q := r.URL.Query()
-	for key, value := range params {
-		q.Add(key, value)
-	}
-	r.URL.RawQuery = q.Encode()
-
-	resp, err := a.client.Do(r)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		_ = resp.Body.Close()
-	}()
-
-	if err := checkError(resp); err != nil {
-		return err
-	}
-
-	bodyBytes, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return err
-	}
-
-	err = json.Unmarshal(bodyBytes, response)
-	if err != nil {
-		// Importantly, we do not include the response in the actual error to avoid
-		// leaking anything sensitive.
-		return errJSONUnmarshal
-	}
-
-	return nil
-}
-
-func (a *InfisicalClient) GetSecretsV3(data GetSecretsV3Request) (map[string]string, error) {
-	params := map[string]string{
-		"workspaceSlug":          data.ProjectSlug,
-		"environment":            data.EnvironmentSlug,
-		"secretPath":             data.SecretPath,
-		"include_imports":        "true",
-		"expandSecretReferences": strconv.FormatBool(data.ExpandSecretReferences),
-		"recursive":              strconv.FormatBool(data.Recursive),
-	}
-
-	res := GetSecretsV3Response{}
-	err := a.do(
-		"api/v3/secrets/raw",
-		http.MethodGet,
-		params,
-		http.NoBody,
-		&res,
-	)
-	metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
-	if err != nil {
-		return nil, err
-	}
-
-	secrets := make(map[string]string)
-	for _, s := range res.ImportedSecrets {
-		for _, el := range s.Secrets {
-			secrets[el.SecretKey] = el.SecretValue
-		}
-	}
-	for _, el := range res.Secrets {
-		secrets[el.SecretKey] = el.SecretValue
-	}
-
-	return secrets, nil
-}
-
-func (a *InfisicalClient) GetSecretByKeyV3(data GetSecretByKeyV3Request) (string, error) {
-	params := map[string]string{
-		"workspaceSlug":          data.ProjectSlug,
-		"environment":            data.EnvironmentSlug,
-		"secretPath":             data.SecretPath,
-		"include_imports":        "true",
-		"expandSecretReferences": strconv.FormatBool(data.ExpandSecretReferences),
-	}
-
-	endpointURL := fmt.Sprintf("api/v3/secrets/raw/%s", data.SecretKey)
-
-	res := GetSecretByKeyV3Response{}
-	err := a.do(
-		endpointURL,
-		http.MethodGet,
-		params,
-		http.NoBody,
-		&res,
-	)
-	metrics.ObserveAPICall(constants.ProviderName, getSecretByKeyV3, err)
-	if err != nil {
-		var apiErr *InfisicalAPIError
-		if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
-			return "", esv1.NoSecretError{}
-		}
-		return "", err
-	}
-
-	return res.Secret.SecretValue, nil
-}
-
-func MarshalReqBody(data any) (*bytes.Reader, error) {
-	body, err := json.Marshal(data)
-	if err != nil {
-		return nil, err
-	}
-	return bytes.NewReader(body), nil
-}

+ 52 - 4
pkg/provider/infisical/api/api_fake.go

@@ -15,9 +15,16 @@ limitations under the License.
 package api
 
 import (
+	"context"
+	"crypto/x509"
 	"encoding/json"
+	"encoding/pem"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
+
+	infisical "github.com/infisical/go-sdk"
+	infisicalSdk "github.com/infisical/go-sdk"
 )
 
 func newMockServer(status int, data any) *httptest.Server {
@@ -27,6 +34,7 @@ func newMockServer(status int, data any) *httptest.Server {
 	}
 
 	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(status)
 		_, err := w.Write(body)
 		if err != nil {
@@ -37,11 +45,51 @@ func newMockServer(status int, data any) *httptest.Server {
 
 // NewMockClient creates an InfisicalClient with a mocked HTTP client that has a
 // fixed response.
-func NewMockClient(status int, data any) (*InfisicalClient, func()) {
+func NewMockClient(status int, data any) (infisicalSdk.InfisicalClientInterface, func()) {
 	server := newMockServer(status, data)
-	client, err := NewAPIClient(server.URL, server.Client())
+	caCert := server.Certificate()
+
+	infisicalConfig := infisicalSdk.Config{
+		SiteUrl: server.URL,
+	}
+
+	if caCert != nil {
+		infisicalConfig.CaCertificate = string(pem.EncodeToMemory(&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: caCert.Raw,
+		}))
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	infisicalSdk := infisicalSdk.NewInfisicalClient(ctx, infisicalConfig)
+
+	closeFunc := func() {
+		cancel()
+		server.Close()
+	}
+
+	return infisicalSdk, closeFunc
+}
+
+func NewAPIClient(baseURL string, certificate *x509.Certificate) (infisicalSdk.InfisicalClientInterface, context.CancelFunc, error) {
+	baseParsedURL, err := url.Parse(baseURL)
 	if err != nil {
-		panic(err)
+		return nil, nil, err
+	}
+
+	infisicalConfig := infisical.Config{
+		SiteUrl: baseParsedURL.String(),
 	}
-	return client, server.Close
+
+	if certificate != nil {
+		infisicalConfig.CaCertificate = string(pem.EncodeToMemory(&pem.Block{
+			Type:  "CERTIFICATE",
+			Bytes: certificate.Raw,
+		}))
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	infisicalSdk := infisicalSdk.NewInfisicalClient(ctx, infisicalConfig)
+
+	return infisicalSdk, cancel, nil
 }

+ 20 - 25
pkg/provider/infisical/api/api_models.go

@@ -14,10 +14,30 @@ limitations under the License.
 
 package api
 
+import (
+	"encoding/json"
+	"fmt"
+)
+
 type MachineIdentityUniversalAuthRefreshRequest struct {
 	AccessToken string `json:"accessToken"`
 }
 
+type InfisicalAPIError struct {
+	StatusCode int
+	Err        any
+	Message    any
+	Details    any
+}
+
+func (e *InfisicalAPIError) Error() string {
+	if e.Details != nil {
+		detailsJSON, _ := json.Marshal(e.Details)
+		return fmt.Sprintf("API error (%d): error=%v message=%v, details=%s", e.StatusCode, e.Err, e.Message, string(detailsJSON))
+	}
+	return fmt.Sprintf("API error (%d): error=%v message=%v", e.StatusCode, e.Err, e.Message)
+}
+
 type MachineIdentityDetailsResponse struct {
 	AccessToken       string `json:"accessToken"`
 	ExpiresIn         int    `json:"expiresIn"`
@@ -25,39 +45,14 @@ type MachineIdentityDetailsResponse struct {
 	TokenType         string `json:"tokenType"`
 }
 
-type MachineIdentityUniversalAuthLoginRequest struct {
-	ClientID     string `json:"clientId"`
-	ClientSecret string `json:"clientSecret"`
-}
-
-type RevokeMachineIdentityAccessTokenRequest struct {
-	AccessToken string `json:"accessToken"`
-}
-
 type RevokeMachineIdentityAccessTokenResponse struct {
 	Message string `json:"message"`
 }
 
-type GetSecretByKeyV3Request struct {
-	EnvironmentSlug        string `json:"environment"`
-	ProjectSlug            string `json:"workspaceSlug"`
-	SecretPath             string `json:"secretPath"`
-	SecretKey              string `json:"secretKey"`
-	ExpandSecretReferences bool   `json:"expandSecretReferences"`
-}
-
 type GetSecretByKeyV3Response struct {
 	Secret SecretsV3 `json:"secret"`
 }
 
-type GetSecretsV3Request struct {
-	EnvironmentSlug        string `json:"environment"`
-	ProjectSlug            string `json:"workspaceSlug"`
-	Recursive              bool   `json:"recursive"`
-	SecretPath             string `json:"secretPath"`
-	ExpandSecretReferences bool   `json:"expandSecretReferences"`
-}
-
 type GetSecretsV3Response struct {
 	Secrets         []SecretsV3        `json:"secrets"`
 	ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`

+ 170 - 174
pkg/provider/infisical/api/api_test.go

@@ -16,145 +16,86 @@ package api
 
 import (
 	"errors"
-	"reflect"
+	"fmt"
+	"regexp"
+	"strconv"
 	"testing"
 
+	infisical "github.com/infisical/go-sdk"
 	"github.com/stretchr/testify/assert"
-
-	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 )
 
-const (
-	fakeClientID        = "client-id"
-	fakeClientSecret    = "client-secret"
-	fakeToken           = "token"
-	fakeProjectSlug     = "first-project"
-	fakeEnvironmentSlug = "dev"
-)
+func parseInfisicalAPIError(err error, t *testing.T) (int, string, error) {
+	var apiErr *infisical.APIError
+	assert.True(t, errors.As(err, &apiErr))
+
+	// Regex to extract status-code
+	statusRegex := regexp.MustCompile(`\[status-code=(\d+)\]`)
+	statusMatch := statusRegex.FindStringSubmatch(apiErr.Error())
 
-func TestAPIClientDo(t *testing.T) {
-	apiURL := "foo"
-	httpMethod := "bar"
-
-	testCases := []struct {
-		Name             string
-		MockStatusCode   int
-		MockResponse     any
-		ExpectedResponse any
-		ExpectedError    error
-	}{
-		{
-			Name:           "Success",
-			MockStatusCode: 200,
-			MockResponse: MachineIdentityDetailsResponse{
-				AccessToken: "foobar",
-			},
-			ExpectedResponse: MachineIdentityDetailsResponse{
-				AccessToken: "foobar",
-			},
-			ExpectedError: nil,
-		},
-		{
-			Name:           "Error when response cannot be unmarshalled",
-			MockStatusCode: 500,
-			MockResponse:   []byte("not-json"),
-			ExpectedError:  errors.New("API error (500), could not unmarshal error response: json: cannot unmarshal string into Go value of type api.InfisicalAPIErrorResponse"),
-		},
-		{
-			Name:           "Error when non-Infisical error response received",
-			MockStatusCode: 500,
-			MockResponse:   map[string]string{"foo": "bar"},
-			ExpectedError:  errors.New("API error (500): {\"foo\":\"bar\"}"),
-		},
-		{
-			Name:           "Do: Error when non-200 response received",
-			MockStatusCode: 401,
-			MockResponse: InfisicalAPIErrorResponse{
-				StatusCode: 401,
-				Error:      "Unauthorized",
-			},
-			ExpectedError: &InfisicalAPIError{StatusCode: 401, Err: "Unauthorized", Message: ""},
-		},
-		{
-			Name:           "Error when arbitrary details are returned",
-			MockStatusCode: 401,
-			MockResponse: InfisicalAPIErrorResponse{
-				StatusCode: 401,
-				Error:      "Unauthorized",
-				Details:    map[string]string{"foo": "details"},
-			},
-			ExpectedError: &InfisicalAPIError{StatusCode: 401, Err: "Unauthorized", Message: "", Details: map[string]string{"foo": "details"}},
-		},
+	// Regex to extract message (handles quoted content)
+	messageRegex := regexp.MustCompile(`\[message="([^"]*)"\]`)
+	messageMatch := messageRegex.FindStringSubmatch(apiErr.Error())
+
+	if len(statusMatch) < 2 {
+		return 0, "", fmt.Errorf("status-code not found in error string")
 	}
 
-	for _, tc := range testCases {
-		t.Run(tc.Name, func(t *testing.T) {
-			apiClient, closeFunc := NewMockClient(tc.MockStatusCode, tc.MockResponse)
-			defer closeFunc()
-
-			// Automatically pluck out the expected response type using reflection to create a new empty value for unmarshalling.
-			var actualResponse any
-			if tc.ExpectedResponse != nil {
-				actualResponse = reflect.New(reflect.TypeOf(tc.ExpectedResponse)).Interface()
-			}
-
-			err := apiClient.do(apiURL, httpMethod, nil, nil, actualResponse)
-			if tc.ExpectedError != nil {
-				assert.Error(t, err)
-				assert.Equal(t, tc.ExpectedError.Error(), err.Error())
-			} else {
-				assert.NoError(t, err)
-				assert.Equal(t, tc.ExpectedResponse, reflect.ValueOf(actualResponse).Elem().Interface())
-			}
-		})
+	if len(messageMatch) < 2 {
+		return 0, "", fmt.Errorf("message not found in error string")
 	}
-}
 
-// TestAPIClientDoInvalidResponse tests the case where the response is a 200 but does not unmarshal
-// correctly.
-func TestAPIClientDoInvalidResponse(t *testing.T) {
-	apiClient, closeFunc := NewMockClient(200, []byte("not-json"))
-	defer closeFunc()
+	statusCode, err := strconv.Atoi(statusMatch[1])
+	if err != nil {
+		return 0, "", fmt.Errorf("invalid status code: %w", err)
+	}
 
-	err := apiClient.do("foo", "bar", nil, nil, nil)
-	assert.ErrorIs(t, err, errJSONUnmarshal)
+	return statusCode, messageMatch[1], nil
 }
 
+const errNoAccessToken = "sdk client is not authenticated, cannot revoke access token"
+
+const (
+	fakeClientID        = "client-id"
+	fakeClientSecret    = "client-secret"
+	fakeToken           = "token"
+	fakeProjectSlug     = "first-project"
+	fakeEnvironmentSlug = "dev"
+)
+
 func TestSetTokenViaMachineIdentity(t *testing.T) {
 	t.Run("Success", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(200, MachineIdentityDetailsResponse{
-			AccessToken: "foobar",
+			AccessToken:       "foobar",
+			ExpiresIn:         2592000,
+			AccessTokenMaxTTL: 2592000,
+			TokenType:         "Bearer",
 		})
 		defer closeFunc()
 
-		err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
+		_, err := apiClient.Auth().UniversalAuthLogin(fakeClientID, fakeClientSecret)
+
 		assert.NoError(t, err)
-		assert.Equal(t, apiClient.token, "foobar")
+		assert.Equal(t, apiClient.Auth().GetAccessToken(), "foobar")
 	})
 
 	t.Run("SetTokenViaMachineIdentity: Error when non-200 response received", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
 			StatusCode: 401,
-			Error:      "Unauthorized",
+			Message:    "Unauthorized",
 		})
 		defer closeFunc()
 
-		err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
+		_, err := apiClient.Auth().UniversalAuthLogin(fakeClientID, fakeClientSecret)
 		assert.Error(t, err)
-		var apiErr *InfisicalAPIError
-		assert.True(t, errors.As(err, &apiErr))
-		assert.Equal(t, 401, apiErr.StatusCode)
-		assert.Equal(t, "Unauthorized", apiErr.Err)
-	})
-
-	t.Run("Error when token already set", func(t *testing.T) {
-		apiClient, closeFunc := NewMockClient(401, nil)
-		defer closeFunc()
 
-		apiClient.token = fakeToken
+		apiErrorStatusCode, apiErrorMessage, err := parseInfisicalAPIError(err, t)
+		if err != nil {
+			t.Fatalf("Error parsing infisical API error: %v", err)
+		}
 
-		err := apiClient.SetTokenViaMachineIdentity(fakeClientID, fakeClientSecret)
-		assert.ErrorIs(t, err, errAccessTokenAlreadyRetrieved)
+		assert.Equal(t, 401, apiErrorStatusCode)
+		assert.Equal(t, "Unauthorized", apiErrorMessage)
 	})
 }
 
@@ -165,153 +106,208 @@ func TestRevokeAccessToken(t *testing.T) {
 		})
 		defer closeFunc()
 
-		apiClient.token = fakeToken
+		apiClient.Auth().SetAccessToken(fakeToken)
+
+		err := apiClient.Auth().RevokeAccessToken()
 
-		err := apiClient.RevokeAccessToken()
 		assert.NoError(t, err)
 		// Verify that the access token was unset.
-		assert.Equal(t, apiClient.token, "")
+		assert.Equal(t, apiClient.Auth().GetAccessToken(), "")
 	})
 
 	t.Run("RevokeAccessToken: Error when non-200 response received", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
 			StatusCode: 401,
-			Error:      "Unauthorized",
+			Message:    "Unauthorized",
 		})
 		defer closeFunc()
 
-		apiClient.token = fakeToken
+		apiClient.Auth().SetAccessToken(fakeToken)
 
-		err := apiClient.RevokeAccessToken()
+		err := apiClient.Auth().RevokeAccessToken()
 		assert.Error(t, err)
-		var apiErr *InfisicalAPIError
-		assert.True(t, errors.As(err, &apiErr))
-		assert.Equal(t, 401, apiErr.StatusCode)
-		assert.Equal(t, "Unauthorized", apiErr.Err)
+
+		apiErrorStatusCode, apiErrorMessage, err := parseInfisicalAPIError(err, t)
+		if err != nil {
+			t.Fatalf("Error parsing infisical API error: %v", err)
+		}
+		assert.Equal(t, 401, apiErrorStatusCode)
+		assert.Equal(t, "Unauthorized", apiErrorMessage)
 	})
 
 	t.Run("Error when no access token is set", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(401, nil)
 		defer closeFunc()
 
-		err := apiClient.RevokeAccessToken()
-		assert.ErrorIs(t, err, errNoAccessToken)
+		err := apiClient.Auth().RevokeAccessToken()
+
+		assert.EqualError(t, err, errNoAccessToken)
 	})
 }
 
 func TestGetSecretsV3(t *testing.T) {
 	t.Run("Works with secrets", func(t *testing.T) {
+		secrets := []SecretsV3{
+			{SecretKey: "foo", SecretValue: "bar"},
+		}
+
 		apiClient, closeFunc := NewMockClient(200, GetSecretsV3Response{
-			Secrets: []SecretsV3{
-				{SecretKey: "foo", SecretValue: "bar"},
-			},
+			Secrets: secrets,
 		})
+
+		var sdkFormattedSecrets []infisical.Secret
+
+		for _, secret := range secrets {
+			sdkFormattedSecrets = append(sdkFormattedSecrets, infisical.Secret{
+				SecretKey:   secret.SecretKey,
+				SecretValue: secret.SecretValue,
+			})
+		}
+
 		defer closeFunc()
 
-		secrets, err := apiClient.GetSecretsV3(GetSecretsV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			Recursive:       true,
+		sdkSecrets, err := apiClient.Secrets().List(infisical.ListSecretsOptions{
+			ProjectSlug: fakeProjectSlug,
+			Environment: fakeEnvironmentSlug,
+			SecretPath:  "/",
+			Recursive:   true,
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, secrets, map[string]string{"foo": "bar"})
+		assert.Equal(t, sdkSecrets, sdkFormattedSecrets)
 	})
 
 	t.Run("Works with imported secrets", func(t *testing.T) {
+		secrets := []SecretsV3{
+			{SecretKey: "foo", SecretValue: "bar"},
+		}
+
 		apiClient, closeFunc := NewMockClient(200, GetSecretsV3Response{
 			ImportedSecrets: []ImportedSecretV3{{
-				Secrets: []SecretsV3{{SecretKey: "foo", SecretValue: "bar"}},
+				Secrets: secrets,
 			}},
 		})
 		defer closeFunc()
 
-		secrets, err := apiClient.GetSecretsV3(GetSecretsV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			Recursive:       true,
+		var sdkFormattedSecrets []infisical.Secret
+
+		for _, secret := range secrets {
+			sdkFormattedSecrets = append(sdkFormattedSecrets, infisical.Secret{
+				SecretKey:   secret.SecretKey,
+				SecretValue: secret.SecretValue,
+			})
+		}
+
+		sdkSecrets, err := apiClient.Secrets().List(infisical.ListSecretsOptions{
+			ProjectSlug:    fakeProjectSlug,
+			Environment:    fakeEnvironmentSlug,
+			IncludeImports: true,
+			SecretPath:     "/",
+			Recursive:      true,
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, secrets, map[string]string{"foo": "bar"})
+		assert.Equal(t, sdkSecrets, sdkFormattedSecrets)
 	})
 
 	t.Run("GetSecretsV3: Error when non-200 response received", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
 			StatusCode: 401,
-			Error:      "Unauthorized",
+			Message:    "Unauthorized",
 		})
 		defer closeFunc()
 
-		_, err := apiClient.GetSecretsV3(GetSecretsV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			Recursive:       true,
+		_, err := apiClient.Secrets().List(infisical.ListSecretsOptions{
+			ProjectSlug: fakeProjectSlug,
+			Environment: fakeEnvironmentSlug,
+			SecretPath:  "/",
+			Recursive:   true,
 		})
 		assert.Error(t, err)
-		var apiErr *InfisicalAPIError
-		assert.True(t, errors.As(err, &apiErr))
-		assert.Equal(t, 401, apiErr.StatusCode)
-		assert.Equal(t, "Unauthorized", apiErr.Err)
+
+		apiErrorStatusCode, apiErrorMessage, err := parseInfisicalAPIError(err, t)
+		if err != nil {
+			t.Fatalf("Error parsing infisical API error: %v", err)
+		}
+
+		assert.Equal(t, 401, apiErrorStatusCode)
+		assert.Equal(t, "Unauthorized", apiErrorMessage)
 	})
 }
 func TestGetSecretByKeyV3(t *testing.T) {
 	t.Run("Works", func(t *testing.T) {
+		secret := SecretsV3{
+			SecretKey:   "foo",
+			SecretValue: "bar",
+		}
+
+		sdkFormattedSecret := infisical.Secret{
+			SecretKey:   secret.SecretKey,
+			SecretValue: secret.SecretValue,
+		}
+
 		apiClient, closeFunc := NewMockClient(200, GetSecretByKeyV3Response{
-			Secret: SecretsV3{
-				SecretKey:   "foo",
-				SecretValue: "bar",
-			},
+			Secret: secret,
 		})
 		defer closeFunc()
 
-		secret, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			SecretKey:       "foo",
+		sdkSecret, err := apiClient.Secrets().Retrieve(infisical.RetrieveSecretOptions{
+			ProjectSlug:    fakeProjectSlug,
+			Environment:    fakeEnvironmentSlug,
+			SecretPath:     "/",
+			IncludeImports: true,
+			SecretKey:      "foo",
 		})
 		assert.NoError(t, err)
-		assert.Equal(t, "bar", secret)
+		assert.Equal(t, sdkSecret, sdkFormattedSecret)
 	})
 
 	t.Run("Error when secret is not found", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(404, InfisicalAPIErrorResponse{
 			StatusCode: 404,
-			Error:      "Not Found",
+			Message:    "Not Found",
 		})
 		defer closeFunc()
 
-		_, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			SecretKey:       "foo",
+		_, err := apiClient.Secrets().Retrieve(infisical.RetrieveSecretOptions{
+			ProjectSlug:    fakeProjectSlug,
+			Environment:    fakeEnvironmentSlug,
+			SecretPath:     "/",
+			IncludeImports: true,
+			SecretKey:      "foo",
 		})
 		assert.Error(t, err)
-		// Importantly, we return the standard error for no secrets found.
-		assert.ErrorIs(t, err, esv1.NoSecretError{})
+
+		apiErrorStatusCode, apiErrorMessage, err := parseInfisicalAPIError(err, t)
+		if err != nil {
+			t.Fatalf("Error parsing infisical API error: %v", err)
+		}
+
+		assert.Equal(t, 404, apiErrorStatusCode)
+		assert.Equal(t, "Not Found", apiErrorMessage)
 	})
 
 	// Test case where the request is unauthorized
 	t.Run("ErrorHandlingUnauthorized", func(t *testing.T) {
 		apiClient, closeFunc := NewMockClient(401, InfisicalAPIErrorResponse{
 			StatusCode: 401,
-			Error:      "Unauthorized",
+			Message:    "Unauthorized",
 		})
 		defer closeFunc()
 
-		_, err := apiClient.GetSecretByKeyV3(GetSecretByKeyV3Request{
-			ProjectSlug:     fakeProjectSlug,
-			EnvironmentSlug: fakeEnvironmentSlug,
-			SecretPath:      "/",
-			SecretKey:       "foo",
+		_, err := apiClient.Secrets().Retrieve(infisical.RetrieveSecretOptions{
+			ProjectSlug:    fakeProjectSlug,
+			Environment:    fakeEnvironmentSlug,
+			SecretPath:     "/",
+			IncludeImports: true,
+			SecretKey:      "foo",
 		})
 		assert.Error(t, err)
-		var apiErr *InfisicalAPIError
-		assert.True(t, errors.As(err, &apiErr))
-		assert.Equal(t, 401, apiErr.StatusCode)
-		assert.Equal(t, "Unauthorized", apiErr.Err)
+
+		apiErrorStatusCode, apiErrorMessage, err := parseInfisicalAPIError(err, t)
+		if err != nil {
+			t.Fatalf("Error parsing infisical API error: %v", err)
+		}
+
+		assert.Equal(t, 401, apiErrorStatusCode)
+		assert.Equal(t, "Unauthorized", apiErrorMessage)
 	})
 }

+ 26 - 14
pkg/provider/infisical/client.go

@@ -21,12 +21,14 @@ import (
 	"fmt"
 	"strings"
 
+	infisical "github.com/infisical/go-sdk"
 	"github.com/tidwall/gjson"
 	corev1 "k8s.io/api/core/v1"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	"github.com/external-secrets/external-secrets/pkg/find"
-	"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
 )
 
 var (
@@ -35,6 +37,11 @@ var (
 	errTagsNotImplemented = errors.New("find by tags not supported")
 )
 
+const (
+	getSecretsV3     = "GetSecretsV3"
+	getSecretByKeyV3 = "GetSecretByKeyV3"
+)
+
 func getPropertyValue(jsonData, propertyName, keyName string) ([]byte, error) {
 	result := gjson.Get(jsonData, propertyName)
 	if !result.Exists() {
@@ -74,20 +81,22 @@ func (p *Provider) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRem
 		return nil, err
 	}
 
-	secret, err := p.apiClient.GetSecretByKeyV3(api.GetSecretByKeyV3Request{
-		EnvironmentSlug:        p.apiScope.EnvironmentSlug,
+	secret, err := p.sdkClient.Secrets().Retrieve(infisical.RetrieveSecretOptions{
+		Environment:            p.apiScope.EnvironmentSlug,
 		ProjectSlug:            p.apiScope.ProjectSlug,
 		SecretKey:              key,
 		SecretPath:             path,
+		IncludeImports:         true,
 		ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
 	})
+	metrics.ObserveAPICall(constants.ProviderName, getSecretByKeyV3, err)
 
 	if err != nil {
 		return nil, err
 	}
 
 	if ref.Property != "" {
-		propertyValue, err := getPropertyValue(secret, ref.Property, ref.Key)
+		propertyValue, err := getPropertyValue(secret.SecretValue, ref.Property, ref.Key)
 		if err != nil {
 			return nil, err
 		}
@@ -95,7 +104,7 @@ func (p *Provider) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRem
 		return propertyValue, nil
 	}
 
-	return []byte(secret), nil
+	return []byte(secret.SecretValue), nil
 }
 
 // GetSecretMap returns multiple k/v pairs from the provider.
@@ -129,20 +138,22 @@ func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFin
 		return nil, errTagsNotImplemented
 	}
 
-	secrets, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
-		EnvironmentSlug:        p.apiScope.EnvironmentSlug,
+	secrets, err := p.sdkClient.Secrets().List(infisical.ListSecretsOptions{
+		Environment:            p.apiScope.EnvironmentSlug,
 		ProjectSlug:            p.apiScope.ProjectSlug,
 		SecretPath:             p.apiScope.SecretPath,
 		Recursive:              p.apiScope.Recursive,
 		ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
+		IncludeImports:         true,
 	})
+	metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
 	if err != nil {
 		return nil, err
 	}
 
 	secretMap := make(map[string][]byte)
-	for key, value := range secrets {
-		secretMap[key] = []byte(value)
+	for _, secret := range secrets {
+		secretMap[secret.SecretKey] = []byte(secret.SecretValue)
 	}
 	if ref.Name == nil && ref.Path == nil {
 		return secretMap, nil
@@ -158,11 +169,11 @@ func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFin
 	}
 
 	selected := map[string][]byte{}
-	for key, value := range secrets {
-		if (matcher != nil && !matcher.MatchName(key)) || (ref.Path != nil && !strings.HasPrefix(key, *ref.Path)) {
+	for _, secret := range secrets {
+		if (matcher != nil && !matcher.MatchName(secret.SecretKey)) || (ref.Path != nil && !strings.HasPrefix(secret.SecretKey, *ref.Path)) {
 			continue
 		}
-		selected[key] = []byte(value)
+		selected[secret.SecretKey] = []byte(secret.SecretValue)
 	}
 	return selected, nil
 }
@@ -172,13 +183,14 @@ func (p *Provider) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFin
 // If the validation result is unknown it will be ignored.
 func (p *Provider) Validate() (esv1.ValidationResult, error) {
 	// try to fetch the secrets to ensure provided credentials has access to read secrets
-	_, err := p.apiClient.GetSecretsV3(api.GetSecretsV3Request{
-		EnvironmentSlug:        p.apiScope.EnvironmentSlug,
+	_, err := p.sdkClient.Secrets().List(infisical.ListSecretsOptions{
+		Environment:            p.apiScope.EnvironmentSlug,
 		ProjectSlug:            p.apiScope.ProjectSlug,
 		Recursive:              p.apiScope.Recursive,
 		SecretPath:             p.apiScope.SecretPath,
 		ExpandSecretReferences: p.apiScope.ExpandSecretReferences,
 	})
+	metrics.ObserveAPICall(constants.ProviderName, getSecretsV3, err)
 
 	if err != nil {
 		return esv1.ValidationResultError, fmt.Errorf("cannot read secrets with provided project scope project:%s environment:%s secret-path:%s recursive:%t, %w", p.apiScope.ProjectSlug, p.apiScope.EnvironmentSlug, p.apiScope.SecretPath, p.apiScope.Recursive, err)

+ 97 - 44
pkg/provider/infisical/provider.go

@@ -18,22 +18,29 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/http"
-	"time"
 
+	infisicalSdk "github.com/infisical/go-sdk"
 	kclient "sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
 
 	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
-	"github.com/external-secrets/external-secrets/pkg/provider/infisical/api"
+	"github.com/external-secrets/external-secrets/pkg/metrics"
+	"github.com/external-secrets/external-secrets/pkg/provider/infisical/constants"
 	"github.com/external-secrets/external-secrets/pkg/utils"
 	"github.com/external-secrets/external-secrets/pkg/utils/resolvers"
 )
 
+const (
+	machineIdentityLoginViaUniversalAuth = "MachineIdentityLoginViaUniversalAuth"
+	machineIdentityLoginViaAzureAuth     = "MachineIdentityLoginViaAzureAuth"
+	revokeAccessToken                    = "RevokeAccessToken"
+)
+
 type Provider struct {
-	apiClient *api.InfisicalClient
-	apiScope  *InfisicalClientScope
+	cancelSdkClient context.CancelFunc
+	sdkClient       infisicalSdk.InfisicalClientInterface
+	apiScope        *InfisicalClientScope
 }
 
 type InfisicalClientScope struct {
@@ -58,63 +65,109 @@ func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
 	return esv1.SecretStoreReadOnly
 }
 
-func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
-	storeSpec := store.GetSpec()
+func performUniversalAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
+	universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
+	clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
+	if err != nil {
+		return err
+	}
 
-	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Infisical == nil {
-		return nil, errors.New("invalid infisical store")
+	clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
+	if err != nil {
+		return err
 	}
 
-	infisicalSpec := storeSpec.Provider.Infisical
+	_, err = sdkClient.Auth().UniversalAuthLogin(clientID, clientSecret)
+	metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaUniversalAuth, err)
 
-	apiClient, err := api.NewAPIClient(infisicalSpec.HostAPI, &http.Client{
-		Timeout: time.Second * 15,
-	})
 	if err != nil {
-		return nil, err
+		return fmt.Errorf("failed to authenticate via universal auth %w", err)
 	}
 
-	if infisicalSpec.Auth.UniversalAuthCredentials != nil {
-		universalAuthCredentials := infisicalSpec.Auth.UniversalAuthCredentials
-		clientID, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientID)
-		if err != nil {
-			return nil, err
-		}
+	return nil
+}
+
+func performAzureAuthLogin(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error {
+	azureAuthCredentials := infisicalSpec.Auth.AzureAuthCredentials
+	identityID, err := GetStoreSecretData(ctx, store, kube, namespace, azureAuthCredentials.IdentityID)
+	if err != nil {
+		return fmt.Errorf("failed to get secret data id %w", err)
+	}
+
+	resource := ""
+	if azureAuthCredentials.Resource.Name != "" {
+		resource, err = GetStoreSecretData(ctx, store, kube, namespace, azureAuthCredentials.Resource)
 
-		clientSecret, err := GetStoreSecretData(ctx, store, kube, namespace, universalAuthCredentials.ClientSecret)
 		if err != nil {
-			return nil, err
+			return fmt.Errorf("failed to get secret data resource %w", err)
 		}
+	}
 
-		if err := apiClient.SetTokenViaMachineIdentity(clientID, clientSecret); err != nil {
-			return nil, fmt.Errorf("failed to authenticate via universal auth %w", err)
-		}
+	_, err = sdkClient.Auth().AzureAuthLogin(identityID, resource)
+	metrics.ObserveAPICall(constants.ProviderName, machineIdentityLoginViaAzureAuth, err)
 
-		secretPath := infisicalSpec.SecretsScope.SecretsPath
-		if secretPath == "" {
-			secretPath = "/"
-		}
+	if err != nil {
+		return fmt.Errorf("failed to authenticate via azure auth %w", err)
+	}
+
+	return nil
+}
+
+func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string) (esv1.SecretsClient, error) {
+	storeSpec := store.GetSpec()
+
+	if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.Infisical == nil {
+		return nil, errors.New("invalid infisical store")
+	}
 
-		return &Provider{
-			apiClient: apiClient,
-			apiScope: &InfisicalClientScope{
-				EnvironmentSlug:        infisicalSpec.SecretsScope.EnvironmentSlug,
-				ProjectSlug:            infisicalSpec.SecretsScope.ProjectSlug,
-				Recursive:              infisicalSpec.SecretsScope.Recursive,
-				SecretPath:             secretPath,
-				ExpandSecretReferences: infisicalSpec.SecretsScope.ExpandSecretReferences,
-			},
-		}, nil
+	infisicalSpec := storeSpec.Provider.Infisical
+
+	ctx, cancelSdkClient := context.WithCancel(ctx)
+
+	sdkClient := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
+		SiteUrl: infisicalSpec.HostAPI,
+	})
+	secretPath := infisicalSpec.SecretsScope.SecretsPath
+	if secretPath == "" {
+		secretPath = "/"
 	}
 
-	return &Provider{}, errors.New("authentication method not found")
+	var loginFn func(ctx context.Context, store esv1.GenericStore, infisicalSpec *esv1.InfisicalProvider, sdkClient infisicalSdk.InfisicalClientInterface, kube kclient.Client, namespace string) error
+	switch {
+	case infisicalSpec.Auth.UniversalAuthCredentials != nil:
+		loginFn = performUniversalAuthLogin
+	case infisicalSpec.Auth.AzureAuthCredentials != nil:
+		loginFn = performAzureAuthLogin
+	default:
+		cancelSdkClient()
+		return nil, errors.New("authentication method not found")
+	}
+
+	if err := loginFn(ctx, store, infisicalSpec, sdkClient, kube, namespace); err != nil {
+		cancelSdkClient()
+		return nil, err
+	}
+
+	return &Provider{
+		cancelSdkClient: cancelSdkClient,
+		sdkClient:       sdkClient,
+
+		apiScope: &InfisicalClientScope{
+			EnvironmentSlug:        infisicalSpec.SecretsScope.EnvironmentSlug,
+			ProjectSlug:            infisicalSpec.SecretsScope.ProjectSlug,
+			Recursive:              infisicalSpec.SecretsScope.Recursive,
+			SecretPath:             secretPath,
+			ExpandSecretReferences: infisicalSpec.SecretsScope.ExpandSecretReferences,
+		},
+	}, nil
 }
 
 func (p *Provider) Close(ctx context.Context) error {
-	if err := p.apiClient.RevokeAccessToken(); err != nil {
-		return err
-	}
-	return nil
+	p.cancelSdkClient()
+	err := p.sdkClient.Auth().RevokeAccessToken()
+	metrics.ObserveAPICall(constants.ProviderName, revokeAccessToken, err)
+
+	return err
 }
 
 func GetStoreSecretData(ctx context.Context, store esv1.GenericStore, kube kclient.Client, namespace string, secret esmeta.SecretKeySelector) (string, error) {

+ 11 - 7
pkg/provider/infisical/provider_test.go

@@ -106,10 +106,10 @@ func TestGetSecret(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
-			apiClient, closeFunc := api.NewMockClient(tc.MockStatusCode, tc.MockResponse)
+			sdkClient, closeFunc := api.NewMockClient(tc.MockStatusCode, tc.MockResponse)
 			defer closeFunc()
 			p := &Provider{
-				apiClient: apiClient,
+				sdkClient: sdkClient,
 				apiScope:  &apiScope,
 			}
 
@@ -151,6 +151,7 @@ func TestGetSecretWithPath(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, fmt.Sprintf("/api/v3/secrets/raw/%s", expectedSecretKey), r.URL.Path)
 		assert.Equal(t, expectedSecretPath, r.URL.Query().Get("secretPath"))
+		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(200)
 		_, err := w.Write(body)
 		if err != nil {
@@ -159,11 +160,13 @@ func TestGetSecretWithPath(t *testing.T) {
 	}))
 	defer server.Close()
 
-	client, err := api.NewAPIClient(server.URL, server.Client())
+	sdkClient, cancelFunc, err := api.NewAPIClient(server.URL, server.Certificate())
+	defer cancelFunc()
 	require.NoError(t, err)
 	p := &Provider{
-		apiClient: client,
-		apiScope:  &apiScope,
+		sdkClient:       sdkClient,
+		cancelSdkClient: cancelFunc,
+		apiScope:        &apiScope,
 	}
 
 	// Retrieve the secret.
@@ -204,10 +207,11 @@ func TestGetSecretMap(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.Name, func(t *testing.T) {
-			apiClient, closeFunc := api.NewMockClient(tc.MockStatusCode, tc.MockResponse)
+			sdkClient, closeFunc := api.NewMockClient(tc.MockStatusCode, tc.MockResponse)
 			defer closeFunc()
+
 			p := &Provider{
-				apiClient: apiClient,
+				sdkClient: sdkClient,
 				apiScope:  &apiScope,
 			}
 			output, err := p.GetSecretMap(context.Background(), esv1.ExternalSecretDataRemoteRef{