Przeglądaj źródła

feat: add environments support to 1Password SDK (#6318)

* feat: add environments support to 1Password SDK

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* chore: fixed remaning skipped test

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* chore: pin to an SDK sha because there is no release

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* chore: updated the onepassword

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* regenereted

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Signed-off-by: Gergely Bräutigam <gergely.brautigam@sap.com>
Gergely Bräutigam 2 dni temu
rodzic
commit
a4726b41fb

+ 9 - 1
apis/externalsecrets/v1/secretstore_onepassword_sdk_types.go

@@ -55,9 +55,17 @@ type CacheConfig struct {
 }
 
 // OnePasswordSDKProvider configures a store to sync secrets using the 1Password sdk.
+// Exactly one of Vault or Environment must be set.
 type OnePasswordSDKProvider struct {
 	// Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
-	Vault string `json:"vault"`
+	// Mutually exclusive with Environment.
+	// +optional
+	Vault string `json:"vault,omitempty"`
+	// Environment defines the 1Password Environment ID to read variables from.
+	// Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+	// Mutually exclusive with Vault.
+	// +optional
+	Environment string `json:"environment,omitempty"`
 	// IntegrationInfo specifies the name and version of the integration built using the 1Password Go SDK.
 	// If you don't know which name and version to use, use `DefaultIntegrationName` and `DefaultIntegrationVersion`, respectively.
 	// +optional

+ 9 - 3
config/crds/bases/external-secrets.io_clustersecretstores.yaml

@@ -4574,6 +4574,12 @@ spec:
                               Format: duration string (e.g., "5m", "1h", "30s")
                             type: string
                         type: object
+                      environment:
+                        description: |-
+                          Environment defines the 1Password Environment ID to read variables from.
+                          Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+                          Mutually exclusive with Vault.
+                        type: string
                       integrationInfo:
                         description: |-
                           IntegrationInfo specifies the name and version of the integration built using the 1Password Go SDK.
@@ -4589,12 +4595,12 @@ spec:
                             type: string
                         type: object
                       vault:
-                        description: Vault defines the vault's name or uuid to access.
-                          Do NOT add op:// prefix. This will be done automatically.
+                        description: |-
+                          Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                          Mutually exclusive with Environment.
                         type: string
                     required:
                     - auth
-                    - vault
                     type: object
                   openBao:
                     description: OpenBao configures this store to sync secrets using

+ 9 - 3
config/crds/bases/external-secrets.io_secretstores.yaml

@@ -4574,6 +4574,12 @@ spec:
                               Format: duration string (e.g., "5m", "1h", "30s")
                             type: string
                         type: object
+                      environment:
+                        description: |-
+                          Environment defines the 1Password Environment ID to read variables from.
+                          Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+                          Mutually exclusive with Vault.
+                        type: string
                       integrationInfo:
                         description: |-
                           IntegrationInfo specifies the name and version of the integration built using the 1Password Go SDK.
@@ -4589,12 +4595,12 @@ spec:
                             type: string
                         type: object
                       vault:
-                        description: Vault defines the vault's name or uuid to access.
-                          Do NOT add op:// prefix. This will be done automatically.
+                        description: |-
+                          Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                          Mutually exclusive with Environment.
                         type: string
                     required:
                     - auth
-                    - vault
                     type: object
                   openBao:
                     description: OpenBao configures this store to sync secrets using

+ 18 - 4
deploy/crds/bundle.yaml

@@ -6592,6 +6592,12 @@ spec:
                                 Format: duration string (e.g., "5m", "1h", "30s")
                               type: string
                           type: object
+                        environment:
+                          description: |-
+                            Environment defines the 1Password Environment ID to read variables from.
+                            Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+                            Mutually exclusive with Vault.
+                          type: string
                         integrationInfo:
                           description: |-
                             IntegrationInfo specifies the name and version of the integration built using the 1Password Go SDK.
@@ -6607,11 +6613,12 @@ spec:
                               type: string
                           type: object
                         vault:
-                          description: Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                          description: |-
+                            Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                            Mutually exclusive with Environment.
                           type: string
                       required:
                         - auth
-                        - vault
                       type: object
                     openBao:
                       description: OpenBao configures this store to sync secrets using the OpenBao provider.
@@ -19794,6 +19801,12 @@ spec:
                                 Format: duration string (e.g., "5m", "1h", "30s")
                               type: string
                           type: object
+                        environment:
+                          description: |-
+                            Environment defines the 1Password Environment ID to read variables from.
+                            Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+                            Mutually exclusive with Vault.
+                          type: string
                         integrationInfo:
                           description: |-
                             IntegrationInfo specifies the name and version of the integration built using the 1Password Go SDK.
@@ -19809,11 +19822,12 @@ spec:
                               type: string
                           type: object
                         vault:
-                          description: Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                          description: |-
+                            Vault defines the vault's name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+                            Mutually exclusive with Environment.
                           type: string
                       required:
                         - auth
-                        - vault
                       type: object
                     openBao:
                       description: OpenBao configures this store to sync secrets using the OpenBao provider.

+ 19 - 2
docs/api/spec.md

@@ -8801,7 +8801,8 @@ External Secrets meta/v1.SecretKeySelector
 <a href="#external-secrets.io/v1.SecretStoreProvider">SecretStoreProvider</a>)
 </p>
 <p>
-<p>OnePasswordSDKProvider configures a store to sync secrets using the 1Password sdk.</p>
+<p>OnePasswordSDKProvider configures a store to sync secrets using the 1Password sdk.
+Exactly one of Vault or Environment must be set.</p>
 </p>
 <table>
 <thead>
@@ -8819,7 +8820,23 @@ string
 </em>
 </td>
 <td>
-<p>Vault defines the vault&rsquo;s name or uuid to access. Do NOT add op:// prefix. This will be done automatically.</p>
+<em>(Optional)</em>
+<p>Vault defines the vault&rsquo;s name or uuid to access. Do NOT add op:// prefix. This will be done automatically.
+Mutually exclusive with Environment.</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>environment</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Environment defines the 1Password Environment ID to read variables from.
+Environments are read-only: PushSecret, DeleteSecret, and SecretExists return an error when set.
+Mutually exclusive with Vault.</p>
 </td>
 </tr>
 <tr>

+ 18 - 0
docs/provider/1password-sdk.md

@@ -86,6 +86,24 @@ To sync the entire secret into a single 1Password item, the following configurat
 {% include '1passwordsdk-push-secret-all-keys.yaml' %}
 ```
 
+### Environments
+
+1Password has added [Environments](https://developer.1password.com/docs/environments) functionality as a BETA feature.
+This is only supported by 1Password SDK and not the connect server.
+
+Environments are an alternative to Vaults. To use the environment define the `environment` id in your Store configuration
+instead of the `vault` value.
+
+The rest of the settings should remain the same.
+
+The SDK, as of this writing, does not support filtering client side, which means that each call always returns everything.
+To tackle this problem, the cache will cache the individual values so if ever the same object is requested again within the
+TTL of the cache it will only fetch that single value.
+
+It also caches ALL the values with a special key, so if repeated All calls are made, that shouldn't be a problem either.
+
+This is a BETA feature. Please use with caution.
+
 ### Supported Functionality
 
 Please check the documentation on 1password for [Supported Functionality](https://developer.1password.com/docs/sdks/functionality).

+ 5 - 5
go.mod

@@ -195,7 +195,7 @@ require (
 	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/compute/metadata v0.9.0 // indirect
 	dario.cat/mergo v1.0.2 // indirect
-	github.com/1password/onepassword-sdk-go v0.3.1 // indirect
+	github.com/1password/onepassword-sdk-go v0.4.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.4.0 // indirect
@@ -283,7 +283,7 @@ require (
 	github.com/gophercloud/gophercloud/v2 v2.8.0 // indirect
 	github.com/grafana/grafana-openapi-client-go v0.0.0-20250925215610-d92957c70d5c // indirect
 	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // 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 v1.0.2 // indirect
@@ -291,7 +291,7 @@ require (
 	github.com/hashicorp/vault/api/auth/aws v0.11.0 // indirect
 	github.com/hashicorp/vault/api/auth/gcp v0.11.0 // indirect
 	github.com/hashicorp/vault/api/auth/userpass v0.11.0 // indirect
-	github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 // indirect
+	github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f // indirect
 	github.com/infisical/go-sdk v0.8.0 // indirect
 	github.com/keeper-security/secrets-manager-go/core v1.6.4 // indirect
 	github.com/kylelemons/godebug v1.1.0 // indirect
@@ -316,7 +316,7 @@ require (
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/stoewer/go-strcase v1.3.1 // indirect
 	github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
-	github.com/tetratelabs/wazero v1.9.0 // indirect
+	github.com/tetratelabs/wazero v1.12.0 // indirect
 	github.com/tidwall/sjson v1.2.5 // indirect
 	github.com/volcengine/volc-sdk-golang v1.0.225 // indirect
 	github.com/volcengine/volcengine-go-sdk v1.1.46 // indirect
@@ -332,7 +332,7 @@ require (
 	go.opentelemetry.io/otel/metric v1.43.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.43.0 // indirect
 	go.opentelemetry.io/otel/trace v1.43.0 // indirect
-	go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.10.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.4 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect

+ 10 - 10
go.sum

@@ -54,8 +54,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
 github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk=
 github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs=
-github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc=
-github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk=
+github.com/1password/onepassword-sdk-go v0.4.1 h1:My/Q2QXemep0I0qHgGrOs7EEzpPh2QZ1/II+S3YqOG0=
+github.com/1password/onepassword-sdk-go v0.4.1/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w=
@@ -596,8 +596,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajR
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
 github.com/hashicorp/consul/api v1.10.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
 github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -671,8 +671,8 @@ github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI
 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 h1:QCtizt3VTaANvnsd8TtD/eonx7JLIVdEKW1//ZNPZ9A=
-github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f h1:NW3E2QSchEk63/fjeEvWOa2cE02FSv9ox//VE/N4c8g=
+github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f/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.8.0 h1:Ipw62V1ptg77OY/5DYH/39D2Ej61Hnrbgn05PCZd32c=
@@ -1018,8 +1018,8 @@ github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44Xt
 github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30=
 github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
 github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
-github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
-github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
 github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
 github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@@ -1102,8 +1102,8 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHS
 go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
-go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
+go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=

+ 118 - 17
providers/v1/onepasswordsdk/client.go

@@ -43,6 +43,7 @@ const (
 	vaultCachePrefix  = "vault:"
 	itemCachePrefix   = "item:"
 	fileCachePrefix   = "file:"
+	envAllCachePrefix = "env-all:"
 	defaultFieldLabel = "password"
 
 	errMsgUpdateItem       = "failed to update item: %w"
@@ -50,8 +51,6 @@ const (
 	errMsgParsePushMeta    = "failed to parse push secret metadata: %w"
 	errMsgExpectedOneField = "found more than 1 fields with title '%s' in '%s', got %d"
 	errMsgExpectedOneFile  = "found more than 1 files with title '%s' in '%s', got %d"
-	errMsgFieldNotFound    = "field with label '%s' not found in item '%s'"
-	errMsgFileNotFound     = "file with title '%s' not found in item '%s'"
 )
 
 // ErrKeyNotFound is returned when a key is not found in the 1Password Vaults.
@@ -79,6 +78,10 @@ func (p *SecretsClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDa
 	if ref.Version != "" {
 		return nil, errors.New(errVersionNotImplemented)
 	}
+	if p.source == sourceEnvironment {
+		return p.getEnvironmentSecret(ctx, ref.Key)
+	}
+
 	key := p.constructRefKey(ref.Key)
 
 	if cached, ok := p.cacheGet(key); ok {
@@ -104,6 +107,80 @@ func (p *SecretsClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDa
 	return result, nil
 }
 
+// getEnvironmentSecret resolves a single variable from a 1Password Environment.
+func (p *SecretsClient) getEnvironmentSecret(ctx context.Context, name string) ([]byte, error) {
+	key := p.constructRefKey(name)
+	if cached, ok := p.cacheGet(key); ok {
+		return cached, nil
+	}
+
+	// If we didn't find the single value, let's get all the values and cache the single value
+	// with our special constructed key.
+	vars, err := p.fetchEnvironmentVariables(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	// As of this writing, the SDK does not support getting a single key. It either gets everything or it doesn't.
+	for _, v := range vars {
+		if v.Name == name {
+			result := []byte(v.Value)
+			p.cacheAdd(key, result)
+			return result, nil
+		}
+	}
+
+	return nil, ErrKeyNotFound
+}
+
+// fetchEnvironmentVariables returns all variables from the configured 1Password Environment.
+// The aggregated response is cached under a synthetic key so subsequent GetSecret/GetSecretMap
+// calls within the TTL avoid re-hitting the API.
+func (p *SecretsClient) fetchEnvironmentVariables(ctx context.Context) ([]onepassword.EnvironmentVariable, error) {
+	allKey := envAllCachePrefix + p.targetID
+	if cached, ok := p.cacheGet(allKey); ok {
+		var vars []onepassword.EnvironmentVariable
+		if err := json.Unmarshal(cached, &vars); err == nil {
+			return vars, nil
+		}
+	}
+
+	resp, err := p.client.Environments().GetVariables(ctx, p.targetID)
+	metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKEnvironmentsGetVars, err)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get environment variables: %w", err)
+	}
+
+	if serialized, err := json.Marshal(resp.Variables); err == nil {
+		p.cacheAdd(allKey, serialized)
+	}
+
+	return resp.Variables, nil
+}
+
+// getEnvironmentSecretMap returns variables from a 1Password Environment as a map.
+// If ref.Property is set, only that variable is returned.
+func (p *SecretsClient) getEnvironmentSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
+	vars, err := p.fetchEnvironmentVariables(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	out := make(map[string][]byte)
+	for _, v := range vars {
+		if ref.Property != "" && v.Name != ref.Property {
+			continue
+		}
+		out[v.Name] = []byte(v.Value)
+	}
+
+	if ref.Property != "" && len(out) == 0 {
+		return nil, ErrKeyNotFound
+	}
+
+	return out, nil
+}
+
 // Close closes the client connection.
 func (p *SecretsClient) Close(_ context.Context) error {
 	return nil
@@ -111,6 +188,9 @@ func (p *SecretsClient) Close(_ context.Context) error {
 
 // DeleteSecret implements Secret Deletion on the provider when PushSecret.spec.DeletionPolicy=Delete.
 func (p *SecretsClient) DeleteSecret(ctx context.Context, ref esv1.PushSecretRemoteRef) (err error) {
+	if p.source == sourceEnvironment {
+		return fmt.Errorf(errOnePasswordSdkEnvironmentReadOnly, "DeleteSecret")
+	}
 	providerItem, err := p.findItem(ctx, ref.GetRemoteKey())
 	if errors.Is(err, ErrKeyNotFound) {
 		// Since the item no longer exists upstream, it's safe to remove it from the cache.
@@ -189,6 +269,18 @@ func deleteField(fields []onepassword.ItemField, title string) ([]onepassword.It
 
 // GetAllSecrets syncs multiple 1Password Items into a single Kubernetes Secret, for dataFrom.find.
 func (p *SecretsClient) GetAllSecrets(ctx context.Context, ref esv1.ExternalSecretFind) (map[string][]byte, error) {
+	if p.source == sourceEnvironment {
+		vars, err := p.fetchEnvironmentVariables(ctx)
+		if err != nil {
+			return nil, err
+		}
+		out := make(map[string][]byte, len(vars))
+		for _, v := range vars {
+			out[v.Name] = []byte(v.Value)
+		}
+		return out, nil
+	}
+
 	items, err := p.listItems(ctx)
 	if err != nil {
 		return nil, err
@@ -262,6 +354,9 @@ func (p *SecretsClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecre
 	if ref.Version != "" {
 		return nil, errors.New(errVersionNotImplemented)
 	}
+	if p.source == sourceEnvironment {
+		return p.getEnvironmentSecretMap(ctx, ref)
+	}
 
 	cacheKey := p.constructRefKey(ref.Key) + "|" + ref.Property
 	if cached, ok := p.cacheGet(cacheKey); ok {
@@ -299,7 +394,7 @@ func (p *SecretsClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecre
 func (p *SecretsClient) listItems(ctx context.Context) ([]onepassword.ItemOverview, error) {
 	var items []onepassword.ItemOverview
 
-	cacheKey := vaultCachePrefix + p.vaultID
+	cacheKey := vaultCachePrefix + p.targetID
 	if cached, ok := p.cacheGet(cacheKey); ok {
 		if err := json.Unmarshal(cached, &items); err == nil {
 			return items, nil
@@ -307,7 +402,7 @@ func (p *SecretsClient) listItems(ctx context.Context) ([]onepassword.ItemOvervi
 	}
 
 	// Vault item list not found in cache - fetch from the API
-	items, err := p.client.Items().List(ctx, p.vaultID)
+	items, err := p.client.Items().List(ctx, p.targetID)
 	metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsList, err)
 	if err != nil {
 		return nil, fmt.Errorf("failed to list items: %w", err)
@@ -380,11 +475,11 @@ func (p *SecretsClient) getAllFields(item onepassword.Item, ref esv1.ExternalSec
 // TODO - Currently, cached files are not invalidated on updates. This should be done as part of the cache refactor.
 // See GitHub issue: https://github.com/external-secrets/external-secrets/issues/6444
 func (p *SecretsClient) fetchFile(ctx context.Context, itemID, fieldID string, attributes onepassword.FileAttributes) ([]byte, error) {
-	cacheKey := fileCachePrefix + p.vaultID + ":" + itemID + ":" + fieldID + ":" + attributes.Name
+	cacheKey := fileCachePrefix + p.targetID + ":" + itemID + ":" + fieldID + ":" + attributes.Name
 	if cached, ok := p.cacheGet(cacheKey); ok {
 		return cached, nil
 	}
-	contents, err := p.client.Items().Files().Read(ctx, p.vaultID, fieldID, attributes)
+	contents, err := p.client.Items().Files().Read(ctx, p.targetID, fieldID, attributes)
 	metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKFilesRead, err)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read file: %w", err)
@@ -510,7 +605,7 @@ func (p *SecretsClient) createItem(ctx context.Context, val []byte, ref esv1.Pus
 
 	createdItem, err := p.client.Items().Create(ctx, onepassword.ItemCreateParams{
 		Category: onepassword.ItemCategoryServer,
-		VaultID:  p.vaultID,
+		VaultID:  p.targetID,
 		Title:    ref.GetRemoteKey(),
 		Fields: []onepassword.ItemField{
 			generateNewItemField(label, string(val), fieldType),
@@ -608,6 +703,9 @@ func generateNewItemField(title, newVal string, fieldType onepassword.ItemFieldT
 
 // PushSecret creates or updates a secret in 1Password.
 func (p *SecretsClient) PushSecret(ctx context.Context, secret *corev1.Secret, ref esv1.PushSecretData) error {
+	if p.source == sourceEnvironment {
+		return fmt.Errorf(errOnePasswordSdkEnvironmentReadOnly, "PushSecret")
+	}
 	if ref.GetSecretKey() == "" {
 		return p.pushAllKeys(ctx, secret, ref)
 	}
@@ -669,7 +767,7 @@ func (p *SecretsClient) createAllKeysItem(ctx context.Context, secret *corev1.Se
 	}
 	createdItem, err := p.client.Items().Create(ctx, onepassword.ItemCreateParams{
 		Category: onepassword.ItemCategoryServer,
-		VaultID:  p.vaultID,
+		VaultID:  p.targetID,
 		Title:    title,
 		Fields:   fields,
 		Tags:     tags,
@@ -755,7 +853,7 @@ func (p *SecretsClient) GetVault(ctx context.Context, titleOrUUID string) (strin
 
 // fetchItemByID retrieves an item by its ID, using the cache if possible.
 func (p *SecretsClient) fetchItemByID(ctx context.Context, id string) (onepassword.Item, error) {
-	cacheKey := itemCachePrefix + p.vaultID + ":" + id
+	cacheKey := itemCachePrefix + p.targetID + ":" + id
 	if cached, ok := p.cacheGet(cacheKey); ok {
 		var item onepassword.Item
 		if err := json.Unmarshal(cached, &item); err == nil {
@@ -763,7 +861,7 @@ func (p *SecretsClient) fetchItemByID(ctx context.Context, id string) (onepasswo
 		}
 	}
 
-	item, err := p.client.Items().Get(ctx, p.vaultID, id)
+	item, err := p.client.Items().Get(ctx, p.targetID, id)
 	metrics.ObserveAPICall(constants.ProviderOnePasswordSDK, constants.CallOnePasswordSDKItemsGet, err)
 	if err != nil {
 		return onepassword.Item{}, err
@@ -777,7 +875,7 @@ func (p *SecretsClient) fetchItemByID(ctx context.Context, id string) (onepasswo
 
 // findItem retrieves an item by its title or ID, using the cache if possible.
 func (p *SecretsClient) findItem(ctx context.Context, name string) (onepassword.Item, error) {
-	cacheKey := itemCachePrefix + p.vaultID + ":" + name
+	cacheKey := itemCachePrefix + p.targetID + ":" + name
 	if cached, ok := p.cacheGet(cacheKey); ok {
 		var item onepassword.Item
 		if err := json.Unmarshal(cached, &item); err == nil {
@@ -841,7 +939,7 @@ func (p *SecretsClient) resolveFieldFromCachedItem(refKey string) ([]byte, bool)
 		return nil, false
 	}
 
-	cached, ok := p.cacheGet(itemCachePrefix + p.vaultID + ":" + itemName)
+	cached, ok := p.cacheGet(itemCachePrefix + p.targetID + ":" + itemName)
 	if !ok {
 		return nil, false
 	}
@@ -865,6 +963,9 @@ func (p *SecretsClient) resolveFieldFromCachedItem(refKey string) ([]byte, bool)
 
 // SecretExists returns true if the item exists, and if a property is specified, if a field with that title exists.
 func (p *SecretsClient) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
+	if p.source == sourceEnvironment {
+		return false, fmt.Errorf(errOnePasswordSdkEnvironmentReadOnly, "SecretExists")
+	}
 	item, err := p.findItem(ctx, ref.GetRemoteKey())
 	if errors.Is(err, ErrKeyNotFound) {
 		return false, nil
@@ -893,8 +994,8 @@ func (p *SecretsClient) Validate() (esv1.ValidationResult, error) {
 }
 
 func (p *SecretsClient) constructRefKey(key string) string {
-	// remove any possible leading slashes because the vaultPrefix already contains it.
-	return p.vaultPrefix + strings.TrimPrefix(key, "/")
+	// remove any possible leading slashes because targetPrefix already contains it.
+	return p.targetPrefix + strings.TrimPrefix(key, "/")
 }
 
 // cacheGet retrieves a value from the cache. Returns false if cache is disabled or key not found.
@@ -955,12 +1056,12 @@ func (p *SecretsClient) invalidateItem(item onepassword.Item) {
 		p.invalidateCacheByPrefix(p.constructRefKey(item.ID))
 	}
 
-	p.cache.Remove(itemCachePrefix + p.vaultID + ":" + item.Title)
+	p.cache.Remove(itemCachePrefix + p.targetID + ":" + item.Title)
 	if item.ID != "" {
-		p.cache.Remove(itemCachePrefix + p.vaultID + ":" + item.ID)
+		p.cache.Remove(itemCachePrefix + p.targetID + ":" + item.ID)
 	}
 
-	p.cache.Remove(vaultCachePrefix + p.vaultID)
+	p.cache.Remove(vaultCachePrefix + p.targetID)
 }
 
 func isNotFoundError(err error) bool {

+ 352 - 45
providers/v1/onepasswordsdk/client_test.go

@@ -33,6 +33,7 @@ import (
 
 	v1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 )
 
 func TestProviderGetSecret(t *testing.T) {
@@ -103,8 +104,8 @@ func TestProviderGetSecret(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			p := &SecretsClient{
-				client:      tt.client(),
-				vaultPrefix: "op://vault/",
+				client:       tt.client(),
+				targetPrefix: "op://vault/",
 			}
 			got, err := p.GetSecret(t.Context(), tt.ref)
 			tt.assertError(t, err)
@@ -273,8 +274,8 @@ func TestProviderGetSecretMap(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			p := &SecretsClient{
-				client:      tt.client(),
-				vaultPrefix: "op://vault/",
+				client:       tt.client(),
+				targetPrefix: "op://vault/",
 			}
 			got, err := p.GetSecretMap(t.Context(), tt.ref)
 			tt.assertError(t, err)
@@ -285,11 +286,11 @@ func TestProviderGetSecretMap(t *testing.T) {
 
 func TestProviderValidate(t *testing.T) {
 	tests := []struct {
-		name        string
-		want        v1.ValidationResult
-		assertError func(t *testing.T, err error)
-		client      func() *onepassword.Client
-		vaultPrefix string
+		name         string
+		want         v1.ValidationResult
+		assertError  func(t *testing.T, err error)
+		client       func() *onepassword.Client
+		targetPrefix string
 	}{
 		{
 			name: "validate successfully",
@@ -312,14 +313,14 @@ func TestProviderValidate(t *testing.T) {
 			assertError: func(t *testing.T, err error) {
 				require.NoError(t, err)
 			},
-			vaultPrefix: "op://vault/",
+			targetPrefix: "op://vault/",
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			p := &SecretsClient{
-				client:      tt.client(),
-				vaultPrefix: tt.vaultPrefix,
+				client:       tt.client(),
+				targetPrefix: tt.targetPrefix,
 			}
 			got, err := p.Validate()
 			tt.assertError(t, err)
@@ -642,6 +643,20 @@ func (f *fakeLister) Files() onepassword.ItemsFilesAPI {
 	return f.fileLister
 }
 
+func (f *fakeLister) CreateAll(ctx context.Context, vaultID string, params []onepassword.ItemCreateParams) (onepassword.ItemsUpdateAllResponse, error) {
+	return onepassword.ItemsUpdateAllResponse{}, nil
+}
+
+func (f *fakeLister) GetAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsGetAllResponse, error) {
+	return onepassword.ItemsGetAllResponse{}, nil
+}
+
+func (f *fakeLister) DeleteAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsDeleteAllResponse, error) {
+	return onepassword.ItemsDeleteAllResponse{}, nil
+}
+
+var _ onepassword.ItemsAPI = (*fakeLister)(nil)
+
 type fakeFileLister struct {
 	readContent []byte
 }
@@ -725,6 +740,18 @@ func (f *statefulFakeLister) Files() onepassword.ItemsFilesAPI {
 	return f.fileLister
 }
 
+func (f *statefulFakeLister) CreateAll(ctx context.Context, vaultID string, params []onepassword.ItemCreateParams) (onepassword.ItemsUpdateAllResponse, error) {
+	return onepassword.ItemsUpdateAllResponse{}, nil
+}
+
+func (f *statefulFakeLister) GetAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsGetAllResponse, error) {
+	return onepassword.ItemsGetAllResponse{}, nil
+}
+
+func (f *statefulFakeLister) DeleteAll(ctx context.Context, vaultID string, itemIDs []string) (onepassword.ItemsDeleteAllResponse, error) {
+	return onepassword.ItemsDeleteAllResponse{}, nil
+}
+
 var _ onepassword.ItemsAPI = (*statefulFakeLister)(nil)
 
 type fakeClient struct {
@@ -734,12 +761,48 @@ type fakeClient struct {
 	resolveAllError error
 	listAllResult   []onepassword.VaultOverview
 	listAllError    error
+
+	envVariables         []onepassword.EnvironmentVariable
+	envError             error
+	envGetVariablesCalls int
 }
 
-func (f *fakeClient) List(ctx context.Context) ([]onepassword.VaultOverview, error) {
+func (f *fakeClient) List(ctx context.Context, params ...onepassword.VaultListParams) ([]onepassword.VaultOverview, error) {
 	return f.listAllResult, f.listAllError
 }
 
+func (f *fakeClient) Create(ctx context.Context, params onepassword.VaultCreateParams) (onepassword.Vault, error) {
+	return onepassword.Vault{}, nil
+}
+
+func (f *fakeClient) Get(ctx context.Context, vaultID string, params onepassword.VaultGetParams) (onepassword.Vault, error) {
+	return onepassword.Vault{}, nil
+}
+
+func (f *fakeClient) GetOverview(ctx context.Context, vaultID string) (onepassword.VaultOverview, error) {
+	return onepassword.VaultOverview{}, nil
+}
+
+func (f *fakeClient) Update(ctx context.Context, vaultID string, params onepassword.VaultUpdateParams) (onepassword.Vault, error) {
+	return onepassword.Vault{}, nil
+}
+
+func (f *fakeClient) Delete(ctx context.Context, vaultID string) error {
+	return nil
+}
+
+func (f *fakeClient) GrantGroupPermissions(ctx context.Context, vaultID string, groupPermissionsList []onepassword.GroupAccess) error {
+	return nil
+}
+
+func (f *fakeClient) UpdateGroupPermissions(ctx context.Context, groupPermissionsList []onepassword.GroupVaultAccess) error {
+	return nil
+}
+
+func (f *fakeClient) RevokeGroupPermissions(ctx context.Context, vaultID, groupID string) error {
+	return nil
+}
+
 func (f *fakeClient) Resolve(ctx context.Context, secretReference string) (string, error) {
 	return f.resolveResult, f.resolveError
 }
@@ -748,6 +811,20 @@ func (f *fakeClient) ResolveAll(ctx context.Context, secretReferences []string)
 	return f.resolveAll, f.resolveAllError
 }
 
+func (f *fakeClient) GetVariables(ctx context.Context, environmentID string) (onepassword.GetVariablesResponse, error) {
+	f.envGetVariablesCalls++
+	if f.envError != nil {
+		return onepassword.GetVariablesResponse{}, f.envError
+	}
+	return onepassword.GetVariablesResponse{Variables: f.envVariables}, nil
+}
+
+var (
+	_ onepassword.VaultsAPI       = (*fakeClient)(nil)
+	_ onepassword.SecretsAPI      = (*fakeClient)(nil)
+	_ onepassword.EnvironmentsAPI = (*fakeClient)(nil)
+)
+
 func TestDeleteMultipleFieldsFromSameItem(t *testing.T) {
 	fc := &fakeClient{
 		listAllResult: []onepassword.VaultOverview{
@@ -842,7 +919,7 @@ func TestCachingGetSecret(t *testing.T) {
 				SecretsAPI: fcWithCounter,
 				VaultsAPI:  fcWithCounter.fakeClient,
 			},
-			vaultPrefix: "op://vault/",
+			targetPrefix: "op://vault/",
 		}
 
 		// Initialize cache
@@ -875,8 +952,8 @@ func TestCachingGetSecret(t *testing.T) {
 				SecretsAPI: fcWithCounter,
 				VaultsAPI:  fcWithCounter.fakeClient,
 			},
-			vaultPrefix: "op://vault/",
-			cache:       nil, // Cache disabled
+			targetPrefix: "op://vault/",
+			cache:        nil, // Cache disabled
 		}
 
 		ref := v1.ExternalSecretDataRemoteRef{Key: "item/field"}
@@ -924,9 +1001,9 @@ func TestCachingGetSecretMap(t *testing.T) {
 				VaultsAPI:  fc,
 				ItemsAPI:   flWithCounter,
 			},
-			vaultPrefix: "op://vault/",
-			vaultID:     "vault-id",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			targetID:     "vault-id",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		ref := v1.ExternalSecretDataRemoteRef{Key: "item"}
@@ -974,9 +1051,9 @@ func TestCacheInvalidationPushSecret(t *testing.T) {
 				VaultsAPI:  fcWithCounter.fakeClient,
 				ItemsAPI:   fl,
 			},
-			vaultPrefix: "op://vault/",
-			vaultID:     "vault-id",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			targetID:     "vault-id",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		ref := v1.ExternalSecretDataRemoteRef{Key: "item/password"}
@@ -1044,9 +1121,9 @@ func TestCacheInvalidationStaleItemAfterPush(t *testing.T) {
 				VaultsAPI:  fc,
 				ItemsAPI:   fl,
 			},
-			vaultPrefix: "op://vault/",
-			vaultID:     "vault-id",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			targetID:     "vault-id",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		mapRef := v1.ExternalSecretDataRemoteRef{Key: "key", Property: "password"}
@@ -1106,9 +1183,9 @@ func TestCacheInvalidationDeleteSecret(t *testing.T) {
 				VaultsAPI:  fcWithCounter.fakeClient,
 				ItemsAPI:   fl,
 			},
-			vaultPrefix: "op://vault/",
-			vaultID:     "vault-id",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			targetID:     "vault-id",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		ref := v1.ExternalSecretDataRemoteRef{Key: "item/field1"}
@@ -1136,8 +1213,8 @@ func TestCacheInvalidationDeleteSecret(t *testing.T) {
 func TestInvalidateCacheByPrefix(t *testing.T) {
 	t.Run("invalidates all entries with prefix", func(t *testing.T) {
 		p := &SecretsClient{
-			vaultPrefix: "op://vault/",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		// Add multiple cache entries
@@ -1162,8 +1239,8 @@ func TestInvalidateCacheByPrefix(t *testing.T) {
 
 	t.Run("handles nil cache gracefully", func(t *testing.T) {
 		p := &SecretsClient{
-			vaultPrefix: "op://vault/",
-			cache:       nil,
+			targetPrefix: "op://vault/",
+			cache:        nil,
 		}
 
 		// Should not panic
@@ -1172,8 +1249,8 @@ func TestInvalidateCacheByPrefix(t *testing.T) {
 
 	t.Run("does not invalidate entries with similar prefixes", func(t *testing.T) {
 		p := &SecretsClient{
-			vaultPrefix: "op://vault/",
-			cache:       expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			targetPrefix: "op://vault/",
+			cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 
 		p.cache.Add("op://vault/item/field1", []byte("val1"))
@@ -1410,7 +1487,7 @@ func TestSecretExists(t *testing.T) {
 					VaultsAPI:  fc,
 					ItemsAPI:   tt.lister,
 				},
-				vaultID: "vault-id",
+				targetID: "vault-id",
 			}
 			exists, err := p.SecretExists(t.Context(), tt.ref)
 			tt.assertError(t, err)
@@ -1491,7 +1568,7 @@ func TestPushSecretFieldType(t *testing.T) {
 					VaultsAPI:  fc,
 					ItemsAPI:   fl,
 				},
-				vaultID: "vault-id",
+				targetID: "vault-id",
 			}
 
 			ref := v1alpha1.PushSecretData{
@@ -1884,8 +1961,8 @@ func TestGetAllSecrets(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			p := &SecretsClient{
-				client:      tt.client(),
-				vaultPrefix: "op://vault/",
+				client:       tt.client(),
+				targetPrefix: "op://vault/",
 			}
 			got, err := p.GetAllSecrets(t.Context(), tt.ref)
 			tt.assertError(t, err)
@@ -1964,9 +2041,9 @@ func TestCachingGetAllSecrets(t *testing.T) {
 			},
 		}
 		return &SecretsClient{
-			client:  &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl},
-			vaultID: "vault-id",
-			cache:   expirable.NewLRU[string, []byte](100, nil, time.Minute),
+			client:   &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl},
+			targetID: "vault-id",
+			cache:    expirable.NewLRU[string, []byte](100, nil, time.Minute),
 		}
 	}
 
@@ -2110,7 +2187,7 @@ func TestPushAllKeys(t *testing.T) {
 
 	t.Run("creates new item with all secret keys as concealed fields", func(t *testing.T) {
 		fl := newLister()
-		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
+		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
 		require.NoError(t, p.PushSecret(t.Context(), secret("alpha", "val-alpha", "beta", "val-beta"), ref("", "my-item")))
 		require.True(t, fl.createCalled)
 		assert.False(t, fl.putCalled)
@@ -2122,7 +2199,7 @@ func TestPushAllKeys(t *testing.T) {
 
 	t.Run("updates existing item with all secret keys", func(t *testing.T) {
 		fl := newLister(onepassword.Item{ID: "item-id", Title: testExistingItem, VaultID: "vault-id"})
-		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
+		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
 		require.NoError(t, p.PushSecret(t.Context(), secret("key1", "value1", "key2", "value2"), ref("", testExistingItem)))
 		assert.False(t, fl.createCalled)
 		require.True(t, fl.putCalled)
@@ -2133,7 +2210,7 @@ func TestPushAllKeys(t *testing.T) {
 
 	t.Run("applies tags from metadata on create", func(t *testing.T) {
 		fl := newLister()
-		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
+		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
 		meta := `{"apiVersion":"kubernetes.external-secrets.io/v1alpha1","kind":"PushSecretMetadata","spec":{"tags":["env:prod","team:backend"]}}`
 		require.NoError(t, p.PushSecret(t.Context(), secret("k", "v"), ref("", "tagged-item", meta)))
 		require.True(t, fl.createCalled)
@@ -2142,7 +2219,7 @@ func TestPushAllKeys(t *testing.T) {
 
 	t.Run("removes fields deleted from the secret", func(t *testing.T) {
 		fl := newLister(existingItem) // existingItem has field testOldKey
-		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, vaultID: "vault-id"}
+		p := &SecretsClient{client: &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, ItemsAPI: fl}, targetID: "vault-id"}
 		// secret no longer contains testOldKey, only "new-key"
 		require.NoError(t, p.PushSecret(t.Context(), secret("new-key", "new-val"), ref("", testExistingItem)))
 		require.True(t, fl.putCalled)
@@ -2152,3 +2229,233 @@ func TestPushAllKeys(t *testing.T) {
 		assert.False(t, stillThere, "deleted key must be removed from the 1Password item")
 	})
 }
+
+func newEnvClient(fc *fakeClient) *SecretsClient {
+	return &SecretsClient{
+		client:       &onepassword.Client{EnvironmentsAPI: fc, SecretsAPI: fc, VaultsAPI: fc},
+		source:       sourceEnvironment,
+		targetID:     "env-id",
+		targetPrefix: "op://env/env-id/",
+	}
+}
+
+func TestEnvironmentGetSecret(t *testing.T) {
+	t.Run("returns variable value", func(t *testing.T) {
+		fc := &fakeClient{
+			envVariables: []onepassword.EnvironmentVariable{
+				{Name: "DB_PASSWORD", Value: "s3cret"},
+				{Name: "API_KEY", Value: "xyz"},
+			},
+		}
+		p := newEnvClient(fc)
+
+		got, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "DB_PASSWORD"})
+		require.NoError(t, err)
+		require.Equal(t, []byte("s3cret"), got)
+	})
+
+	t.Run("returns ErrKeyNotFound when variable absent", func(t *testing.T) {
+		fc := &fakeClient{
+			envVariables: []onepassword.EnvironmentVariable{
+				{Name: "OTHER", Value: "v"},
+			},
+		}
+		p := newEnvClient(fc)
+
+		_, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "MISSING"})
+		require.ErrorIs(t, err, ErrKeyNotFound)
+	})
+
+	t.Run("propagates SDK errors", func(t *testing.T) {
+		fc := &fakeClient{envError: errors.New("upstream boom")}
+		p := newEnvClient(fc)
+
+		_, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "ANY"})
+		require.ErrorContains(t, err, "upstream boom")
+	})
+
+	t.Run("rejects ref.Version", func(t *testing.T) {
+		p := newEnvClient(&fakeClient{})
+		_, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K", Version: "1"})
+		require.ErrorContains(t, err, "is not implemented")
+	})
+
+	t.Run("uses cache on subsequent reads", func(t *testing.T) {
+		fc := &fakeClient{
+			envVariables: []onepassword.EnvironmentVariable{
+				{Name: "K1", Value: "v1"},
+				{Name: "K2", Value: "v2"},
+			},
+		}
+		p := newEnvClient(fc)
+		p.cache = expirable.NewLRU[string, []byte](100, nil, time.Minute)
+
+		_, err := p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K1"})
+		require.NoError(t, err)
+		_, err = p.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "K2"})
+		require.NoError(t, err)
+		require.Equal(t, 1, fc.envGetVariablesCalls, "second lookup must hit the cached fetch, not the API")
+	})
+}
+
+func TestEnvironmentGetSecretMap(t *testing.T) {
+	fc := &fakeClient{
+		envVariables: []onepassword.EnvironmentVariable{
+			{Name: "K1", Value: "v1"},
+			{Name: "K2", Value: "v2"},
+		},
+	}
+	p := newEnvClient(fc)
+
+	t.Run("returns all variables when no property", func(t *testing.T) {
+		got, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{})
+		require.NoError(t, err)
+		require.Equal(t, map[string][]byte{"K1": []byte("v1"), "K2": []byte("v2")}, got)
+	})
+
+	t.Run("filters by property when set", func(t *testing.T) {
+		got, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Property: "K2"})
+		require.NoError(t, err)
+		require.Equal(t, map[string][]byte{"K2": []byte("v2")}, got)
+	})
+
+	t.Run("returns ErrKeyNotFound when property missing", func(t *testing.T) {
+		_, err := p.GetSecretMap(t.Context(), v1.ExternalSecretDataRemoteRef{Property: "missing"})
+		require.ErrorIs(t, err, ErrKeyNotFound)
+	})
+}
+
+func TestEnvironmentGetAllSecrets(t *testing.T) {
+	fc := &fakeClient{
+		envVariables: []onepassword.EnvironmentVariable{
+			{Name: "K1", Value: "v1"},
+			{Name: "K2", Value: "v2"},
+		},
+	}
+	p := newEnvClient(fc)
+
+	got, err := p.GetAllSecrets(t.Context(), v1.ExternalSecretFind{})
+	require.NoError(t, err)
+	require.Equal(t, map[string][]byte{"K1": []byte("v1"), "K2": []byte("v2")}, got)
+}
+
+func TestEnvironmentRejectsWrites(t *testing.T) {
+	p := newEnvClient(&fakeClient{})
+
+	t.Run("PushSecret", func(t *testing.T) {
+		err := p.PushSecret(t.Context(), &corev1.Secret{Data: map[string][]byte{"k": []byte("v")}}, &fakePushSecretData{secretKey: "k", remoteKey: "rk"})
+		require.ErrorContains(t, err, "read-only")
+		require.ErrorContains(t, err, "PushSecret")
+	})
+
+	t.Run("DeleteSecret", func(t *testing.T) {
+		err := p.DeleteSecret(t.Context(), &fakePushSecretData{remoteKey: "rk"})
+		require.ErrorContains(t, err, "read-only")
+		require.ErrorContains(t, err, "DeleteSecret")
+	})
+
+	t.Run("SecretExists", func(t *testing.T) {
+		_, err := p.SecretExists(t.Context(), &fakePushSecretData{remoteKey: "rk"})
+		require.ErrorContains(t, err, "read-only")
+		require.ErrorContains(t, err, "SecretExists")
+	})
+}
+
+type fakePushSecretData struct {
+	secretKey string
+	remoteKey string
+	property  string
+	metadata  *apiextensionsv1.JSON
+}
+
+func (f *fakePushSecretData) GetSecretKey() string               { return f.secretKey }
+func (f *fakePushSecretData) GetRemoteKey() string               { return f.remoteKey }
+func (f *fakePushSecretData) GetProperty() string                { return f.property }
+func (f *fakePushSecretData) GetMetadata() *apiextensionsv1.JSON { return f.metadata }
+
+func TestEnvironmentVsVaultCacheIsolation(t *testing.T) {
+	fc := &fakeClient{
+		resolveResult: "from-vault",
+		envVariables: []onepassword.EnvironmentVariable{
+			{Name: "shared", Value: "from-env"},
+		},
+	}
+
+	vault := &SecretsClient{
+		client:       &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, EnvironmentsAPI: fc},
+		source:       sourceVault,
+		targetID:     "vault-id",
+		targetPrefix: "op://myvault/",
+		cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
+	}
+	env := &SecretsClient{
+		client:       &onepassword.Client{SecretsAPI: fc, VaultsAPI: fc, EnvironmentsAPI: fc},
+		source:       sourceEnvironment,
+		targetID:     "env-id",
+		targetPrefix: "op://env/env-id/",
+		cache:        expirable.NewLRU[string, []byte](100, nil, time.Minute),
+	}
+
+	gotVault, err := vault.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "shared"})
+	require.NoError(t, err)
+	require.Equal(t, []byte("from-vault"), gotVault)
+
+	gotEnv, err := env.GetSecret(t.Context(), v1.ExternalSecretDataRemoteRef{Key: "shared"})
+	require.NoError(t, err)
+	require.Equal(t, []byte("from-env"), gotEnv, "vault and environment must not share cache entries for the same key")
+}
+
+func TestProviderValidateStore(t *testing.T) {
+	baseAuth := &v1.OnePasswordSDKAuth{
+		ServiceAccountSecretRef: esmeta.SecretKeySelector{
+			Name: "sa-secret",
+			Key:  "token",
+		},
+	}
+	provider := &Provider{}
+
+	mkStore := func(p *v1.OnePasswordSDKProvider) *v1.SecretStore {
+		return &v1.SecretStore{
+			TypeMeta: metav1.TypeMeta{Kind: "SecretStore"},
+			Spec: v1.SecretStoreSpec{
+				Provider: &v1.SecretStoreProvider{OnePasswordSDK: p},
+			},
+		}
+	}
+
+	tests := []struct {
+		name    string
+		spec    *v1.OnePasswordSDKProvider
+		wantErr string
+	}{
+		{
+			name: "vault only is valid",
+			spec: &v1.OnePasswordSDKProvider{Auth: baseAuth, Vault: "myvault"},
+		},
+		{
+			name: "environment only is valid",
+			spec: &v1.OnePasswordSDKProvider{Auth: baseAuth, Environment: "env-id"},
+		},
+		{
+			name:    "neither set is invalid",
+			spec:    &v1.OnePasswordSDKProvider{Auth: baseAuth},
+			wantErr: "exactly one of",
+		},
+		{
+			name:    "both set is invalid",
+			spec:    &v1.OnePasswordSDKProvider{Auth: baseAuth, Vault: "v", Environment: "e"},
+			wantErr: "mutually exclusive",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := provider.ValidateStore(mkStore(tt.spec))
+			if tt.wantErr == "" {
+				require.NoError(t, err)
+				return
+			}
+			require.ErrorContains(t, err, tt.wantErr)
+		})
+	}
+}

+ 4 - 4
providers/v1/onepasswordsdk/go.mod

@@ -3,7 +3,7 @@ module github.com/external-secrets/external-secrets/providers/v1/onepasswordsdk
 go 1.26.5
 
 require (
-	github.com/1password/onepassword-sdk-go v0.3.1
+	github.com/1password/onepassword-sdk-go v0.4.1
 	github.com/external-secrets/external-secrets/apis v0.0.0
 	github.com/external-secrets/external-secrets/runtime v0.0.0
 	github.com/hashicorp/golang-lru/v2 v2.0.7
@@ -49,7 +49,7 @@ require (
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/hashicorp/golang-lru v1.0.2 // indirect
 	github.com/huandu/xstrings v1.5.0 // indirect
-	github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 // indirect
+	github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
 	github.com/lestrrat-go/httpcc v1.0.1 // indirect
@@ -72,9 +72,9 @@ require (
 	github.com/spf13/cast v1.10.0 // indirect
 	github.com/spf13/pflag v1.0.10 // indirect
 	github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
-	github.com/tetratelabs/wazero v1.9.0 // indirect
+	github.com/tetratelabs/wazero v1.12.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+	go.opentelemetry.io/proto/otlp v1.10.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.4 // indirect
 	go.yaml.in/yaml/v3 v3.0.4 // indirect
 	golang.org/x/crypto v0.53.0 // indirect

+ 8 - 8
providers/v1/onepasswordsdk/go.sum

@@ -1,7 +1,7 @@
 dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
 dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
-github.com/1password/onepassword-sdk-go v0.3.1 h1:dz0LrYuIh/HrZ7rxr8NMymikNLBIXhyj4NBmo5Tdamc=
-github.com/1password/onepassword-sdk-go v0.3.1/go.mod h1:kssODrGGqHtniqPR91ZPoCMEo79mKulKat7RaD1bunk=
+github.com/1password/onepassword-sdk-go v0.4.1 h1:My/Q2QXemep0I0qHgGrOs7EEzpPh2QZ1/II+S3YqOG0=
+github.com/1password/onepassword-sdk-go v0.4.1/go.mod h1:j/CbzhucTywjlYrd6SE6k0LcQaFZ2l8OLBsAsOYtvD0=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
 github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
@@ -99,8 +99,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
 github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
-github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5 h1:QCtizt3VTaANvnsd8TtD/eonx7JLIVdEKW1//ZNPZ9A=
-github.com/ianlancetaylor/demangle v0.0.0-20250628045327-2d64ad6b7ec5/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
+github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f h1:NW3E2QSchEk63/fjeEvWOa2cE02FSv9ox//VE/N4c8g=
+github.com/ianlancetaylor/demangle v0.0.0-20260505044615-1ff4bf46051f/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
@@ -176,14 +176,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
 github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
 github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
 github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
-github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
-github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
+github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
-go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
-go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
+go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
+go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=

+ 40 - 16
providers/v1/onepasswordsdk/provider.go

@@ -42,22 +42,32 @@ const (
 	errOnePasswordSdkStoreNilSpecProviderOnePasswordSdk = "nil spec.provider.onepasswordsdk"
 	errOnePasswordSdkStoreMissingRefName                = "missing: spec.provider.onepasswordsdk.auth.secretRef.serviceAccountTokenSecretRef.name"
 	errOnePasswordSdkStoreMissingRefKey                 = "missing: spec.provider.onepasswordsdk.auth.secretRef.serviceAccountTokenSecretRef.key"
-	errOnePasswordSdkStoreMissingVaultKey               = "missing: spec.provider.onepasswordsdk.vault"
+	errOnePasswordSdkStoreMissingTarget                 = "missing: exactly one of spec.provider.onepasswordsdk.vault or spec.provider.onepasswordsdk.environment must be set"
+	errOnePasswordSdkStoreBothTargets                   = "spec.provider.onepasswordsdk.vault and spec.provider.onepasswordsdk.environment are mutually exclusive"
+	errOnePasswordSdkEnvironmentReadOnly                = "1Password Environment is read-only: %s is not supported"
 	errVersionNotImplemented                            = "'remoteRef.version' is not implemented in the 1Password SDK provider"
 	errNotImplemented                                   = "not implemented"
 )
 
+type secretSource int
+
+const (
+	sourceVault secretSource = iota
+	sourceEnvironment
+)
+
 // Provider contains the main cache for onepasswordsdk provider.
 type Provider struct {
 	clientCache *cache.Cache[esv1.SecretsClient]
 }
 
-// SecretsClient wraps a 1Password SDK client for a specific vault.
+// SecretsClient wraps a 1Password SDK client for a specific vault or environment.
 type SecretsClient struct {
-	client      *onepassword.Client
-	vaultPrefix string
-	vaultID     string
-	cache       *expirable.LRU[string, []byte]
+	client       *onepassword.Client
+	source       secretSource
+	targetPrefix string
+	targetID     string
+	cache        *expirable.LRU[string, []byte]
 }
 
 // NewClient will create a new client.
@@ -101,15 +111,24 @@ func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube
 	}
 
 	sc := &SecretsClient{
-		client:      c,
-		vaultPrefix: "op://" + config.Vault + "/",
-	}
-
-	vaultID, err := sc.GetVault(ctx, config.Vault)
-	if err != nil {
-		return nil, fmt.Errorf("failed to get store ID: %w", err)
+		client: c,
+	}
+
+	switch {
+	case config.Environment != "":
+		// As of this writing, the environment doesn't have a way of validation.
+		sc.source = sourceEnvironment
+		sc.targetID = config.Environment
+		sc.targetPrefix = "op://env/" + config.Environment + "/"
+	default:
+		sc.source = sourceVault
+		sc.targetPrefix = "op://" + config.Vault + "/"
+		vaultID, err := sc.GetVault(ctx, config.Vault)
+		if err != nil {
+			return nil, fmt.Errorf("failed to get store ID: %w", err)
+		}
+		sc.targetID = vaultID
 	}
-	sc.vaultID = vaultID
 
 	if config.Cache != nil {
 		ttl := 5 * time.Minute
@@ -151,8 +170,13 @@ func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, e
 		return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errOnePasswordSdkStoreMissingRefKey))
 	}
 
-	if config.Vault == "" {
-		return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errOnePasswordSdkStoreMissingVaultKey))
+	hasVault := config.Vault != ""
+	hasEnv := config.Environment != ""
+	if hasVault && hasEnv {
+		return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errOnePasswordSdkStoreBothTargets))
+	}
+	if !hasVault && !hasEnv {
+		return nil, fmt.Errorf(errOnePasswordSdkStore, errors.New(errOnePasswordSdkStoreMissingTarget))
 	}
 
 	// check namespace compared to kind

+ 10 - 9
runtime/constants/constants.go

@@ -125,15 +125,16 @@ const (
 	CallAKEYLESSSMUpdateSecretVal       = "UpdateSecretVal"
 	CallAKEYLESSSMDeleteItem            = "DeleteItem"
 
-	ProviderOnePasswordSDK        = "1Password/SDK"
-	CallOnePasswordSDKResolve     = "Resolve"
-	CallOnePasswordSDKItemsList   = "ItemsList"
-	CallOnePasswordSDKItemsGet    = "ItemsGet"
-	CallOnePasswordSDKItemsCreate = "ItemsCreate"
-	CallOnePasswordSDKItemsPut    = "ItemsPut"
-	CallOnePasswordSDKItemsDelete = "ItemsDelete"
-	CallOnePasswordSDKFilesRead   = "FilesRead"
-	CallOnePasswordSDKVaultsList  = "VaultsList"
+	ProviderOnePasswordSDK                = "1Password/SDK"
+	CallOnePasswordSDKResolve             = "Resolve"
+	CallOnePasswordSDKItemsList           = "ItemsList"
+	CallOnePasswordSDKItemsGet            = "ItemsGet"
+	CallOnePasswordSDKItemsCreate         = "ItemsCreate"
+	CallOnePasswordSDKItemsPut            = "ItemsPut"
+	CallOnePasswordSDKItemsDelete         = "ItemsDelete"
+	CallOnePasswordSDKFilesRead           = "FilesRead"
+	CallOnePasswordSDKVaultsList          = "VaultsList"
+	CallOnePasswordSDKEnvironmentsGetVars = "EnvironmentsGetVariables"
 
 	ProviderNebiusMysterybox           = "Nebius/Mysterybox"
 	CallNebiusMysteryboxGetSecret      = "GetSecret"

+ 1 - 0
tests/__snapshot__/clustersecretstore-v1.yaml

@@ -672,6 +672,7 @@ spec:
       cache:
         maxSize: 100
         ttl: "5m"
+      environment: string
       integrationInfo:
         name: "1Password SDK"
         version: "v1.0.0"

+ 1 - 0
tests/__snapshot__/secretstore-v1.yaml

@@ -672,6 +672,7 @@ spec:
       cache:
         maxSize: 100
         ttl: "5m"
+      environment: string
       integrationInfo:
         name: "1Password SDK"
         version: "v1.0.0"