1
0
Fork 0
mirror of https://github.com/external-secrets/external-secrets.git synced 2024-12-14 11:57:59 +00:00

Merge pull request #323 from AndreyZamyslov/lockbox

Add support for Yandex Lockbox
This commit is contained in:
paul-the-alien[bot] 2021-08-25 10:06:21 +00:00 committed by GitHub
commit ee830e47e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1654 additions and 10 deletions

View file

@ -17,6 +17,7 @@ Multiple people and organizations are joining efforts to create a single Externa
- [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/)
- [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/)
- [IBM Cloud Secrets Manager](https://external-secrets.io/provider-ibm-secrets-manager/)
- [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/)
## Stability and Support Level
@ -35,6 +36,7 @@ Multiple people and organizations are joining efforts to create a single Externa
| ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
| [Azure KV](https://external-secrets.io/provider-azure-key-vault/) | alpha | @ahmedmus-1A @asnowfix @ncourbet-1A @1A-mj |
| [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) | alpha | @knelasevero @sebagomez @ricardoptcosta |
| [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) | alpha | @AndreyZamyslov @knelasevero |
## Documentation

View file

@ -53,6 +53,10 @@ type SecretStoreProvider struct {
// IBM configures this store to sync secrets using IBM Cloud provider
// +optional
IBM *IBMProvider `json:"ibm,omitempty"`
// YandexLockbox configures this store to sync secrets using Yandex Lockbox provider
// +optional
YandexLockbox *YandexLockboxProvider `json:"yandexlockbox,omitempty"`
}
type SecretStoreConditionType string

View file

@ -0,0 +1,35 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1alpha1
import (
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
)
type YandexLockboxAuth struct {
// The authorized key used for authentication
// +optional
AuthorizedKey esmeta.SecretKeySelector `json:"authorizedKeySecretRef,omitempty"`
}
// YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.
type YandexLockboxProvider struct {
// Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
// +optional
APIEndpoint string `json:"apiEndpoint,omitempty"`
// Auth defines the information necessary to authenticate against Yandex Lockbox
Auth YandexLockboxAuth `json:"auth"`
}

View file

@ -644,6 +644,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) {
*out = new(IBMProvider)
(*in).DeepCopyInto(*out)
}
if in.YandexLockbox != nil {
in, out := &in.YandexLockbox, &out.YandexLockbox
*out = new(YandexLockboxProvider)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretStoreProvider.
@ -949,3 +954,35 @@ func (in *VaultProvider) DeepCopy() *VaultProvider {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxAuth) DeepCopyInto(out *YandexLockboxAuth) {
*out = *in
in.AuthorizedKey.DeepCopyInto(&out.AuthorizedKey)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxAuth.
func (in *YandexLockboxAuth) DeepCopy() *YandexLockboxAuth {
if in == nil {
return nil
}
out := new(YandexLockboxAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *YandexLockboxProvider) DeepCopyInto(out *YandexLockboxProvider) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new YandexLockboxProvider.
func (in *YandexLockboxProvider) DeepCopy() *YandexLockboxProvider {
if in == nil {
return nil
}
out := new(YandexLockboxProvider)
in.DeepCopyInto(out)
return out
}

View file

@ -580,6 +580,39 @@ spec:
- path
- server
type: object
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider
properties:
apiEndpoint:
description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
type: string
auth:
description: Auth defines the information necessary to authenticate
against Yandex Lockbox
properties:
authorizedKeySecretRef:
description: The authorized key used for authentication
properties:
key:
description: The key of the entry in the Secret resource's
`data` field to be used. Some instances of this
field may be defaulted, in others it may be required.
type: string
name:
description: The name of the Secret resource being
referred to.
type: string
namespace:
description: Namespace of the resource being referred
to. Ignored if referent is not cluster-scoped. cluster-scoped
defaults to the namespace of the referent.
type: string
type: object
type: object
required:
- auth
type: object
type: object
required:
- provider

View file

@ -580,6 +580,39 @@ spec:
- path
- server
type: object
yandexlockbox:
description: YandexLockbox configures this store to sync secrets
using Yandex Lockbox provider
properties:
apiEndpoint:
description: Yandex.Cloud API endpoint (e.g. 'api.cloud.yandex.net:443')
type: string
auth:
description: Auth defines the information necessary to authenticate
against Yandex Lockbox
properties:
authorizedKeySecretRef:
description: The authorized key used for authentication
properties:
key:
description: The key of the entry in the Secret resource's
`data` field to be used. Some instances of this
field may be defaulted, in others it may be required.
type: string
name:
description: The name of the Secret resource being
referred to.
type: string
namespace:
description: Namespace of the resource being referred
to. Ignored if referent is not cluster-scoped. cluster-scoped
defaults to the namespace of the referent.
type: string
type: object
type: object
required:
- auth
type: object
type: object
required:
- provider

View file

@ -0,0 +1,86 @@
## Yandex Lockbox
External Secrets Operator integrates with [Yandex Lockbox](https://cloud.yandex.com/docs/lockbox/)
for secret management.
### Prerequisites
* [External Secrets Operator installed](../guides-getting-started/#installing-with-helm)
* [Yandex.Cloud CLI installed](https://cloud.yandex.com/docs/cli/quickstart)
### Authentication
At the moment, [authorized key](https://cloud.yandex.com/docs/iam/concepts/authorization/key) authentication is only supported:
* Create a [service account](https://cloud.yandex.com/docs/iam/concepts/users/service-accounts) in Yandex.Cloud:
```bash
yc iam service-account create --name eso-service-account
```
* Create an authorized key for the service account and save it to `authorized-key.json` file:
```bash
yc iam key create \
--service-account-name eso-service-account \
--output authorized-key.json
```
* Create a k8s secret containing the authorized key saved above:
```bash
kubectl create secret generic yc-auth --from-file=authorized-key=authorized-key.json
```
* Create a [SecretStore](../api-secretstore/) pointing to `yc-auth` k8s secret:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: secret-store
spec:
provider:
yandexlockbox:
auth:
authorizedKeySecretRef:
name: yc-auth
key: authorized-key
```
### Creating external secret
To make External Secrets Operator sync a k8s secret with a Lockbox secret:
* Create a Lockbox secret, if not already created:
```bash
yc lockbox secret create \
--name lockbox-secret \
--payload '[{"key": "password","textValue": "p@$$w0rd"}]'
```
* Assign the [`lockbox.payloadViewer`](https://cloud.yandex.com/docs/lockbox/security/#roles-list) role
for accessing the `lockbox-secret` payload to the service account used for authentication:
```bash
yc lockbox secret add-access-binding \
--name lockbox-secret \
--service-account-name eso-service-account \
--role lockbox.payloadViewer
```
Run the following command to ensure that the correct access binding has been added:
```bash
yc lockbox secret list-access-bindings --name lockbox-secret
```
* Create an [ExternalSecret](../api-externalsecret/) pointing to `secret-store` and `lockbox-secret`:
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: external-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: secret-store
kind: SecretStore
target:
name: k8s-secret # the target k8s secret name
data:
- secretKey: password # the target k8s secret key
remoteRef:
key: ***** # ID of lockbox-secret
property: password # (optional) payload entry key of lockbox-secret
```
The operator will fetch the Yandex Lockbox secret and inject it as a `Kind=Secret`
```yaml
kubectl get secret k8s-secret -n <namespace> | -o jsonpath='{.data.password}' | base64 -d
```

View file

@ -1469,6 +1469,20 @@ IBMProvider
<p>IBM configures this store to sync secrets using IBM Cloud provider</p>
</td>
</tr>
<tr>
<td>
<code>yandexlockbox</code></br>
<em>
<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">
YandexLockboxProvider
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>YandexLockbox configures this store to sync secrets using Yandex Lockbox provider</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.SecretStoreRef">SecretStoreRef
@ -2277,6 +2291,80 @@ are used to validate the TLS connection.</p>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.YandexLockboxAuth">YandexLockboxAuth
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider</a>)
</p>
<p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>authorizedKeySecretRef</code></br>
<em>
github.com/external-secrets/external-secrets/apis/meta/v1.SecretKeySelector
</em>
</td>
<td>
<em>(Optional)</em>
<p>The authorized key used for authentication</p>
</td>
</tr>
</tbody>
</table>
<h3 id="external-secrets.io/v1alpha1.YandexLockboxProvider">YandexLockboxProvider
</h3>
<p>
(<em>Appears on:</em>
<a href="#external-secrets.io/v1alpha1.SecretStoreProvider">SecretStoreProvider</a>)
</p>
<p>
<p>YandexLockboxProvider Configures a store to sync secrets using the Yandex Lockbox provider.</p>
</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiEndpoint</code></br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Yandex.Cloud API endpoint (e.g. &lsquo;api.cloud.yandex.net:443&rsquo;)</p>
</td>
</tr>
<tr>
<td>
<code>auth</code></br>
<em>
<a href="#external-secrets.io/v1alpha1.YandexLockboxAuth">
YandexLockboxAuth
</a>
</em>
</td>
<td>
<p>Auth defines the information necessary to authenticate against Yandex Lockbox</p>
</td>
</tr>
</tbody>
</table>
<hr/>
<p><em>
Generated with <code>gen-crd-api-reference-docs</code>.

3
go.mod
View file

@ -64,6 +64,8 @@ require (
github.com/spf13/cobra v1.1.3 // indirect
github.com/stretchr/testify v1.7.0
github.com/tidwall/gjson v1.7.5
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
go.uber.org/zap v1.17.0
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
@ -72,6 +74,7 @@ require (
golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect
google.golang.org/api v0.30.0
google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a
google.golang.org/grpc v1.31.0
honnef.co/go/tools v0.1.4 // indirect
k8s.io/api v0.21.2
k8s.io/apimachinery v0.21.2

10
go.sum
View file

@ -98,6 +98,7 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
@ -135,6 +136,7 @@ github.com/decred/dcrd/chaincfg/chainhash v1.0.2/go.mod h1:BpbrGgrPTr3YJYRN3Bm+D
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0 h1:sgNeV1VRMDzs6rzyPpxyM0jp317hnwiq58Filgag2xw=
github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+3f1KZLNaXkuv+yeFTKoxM8=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
@ -166,6 +168,7 @@ github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
@ -479,6 +482,7 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
@ -645,6 +649,10 @@ github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588 h1:Lbz8X5Nre0Lg5QgCblmo0AhScWxeN3CVnX+mZ5Hxksk=
github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE=
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa h1:Un1jWl/YWbK1179aMbsEZ6uLlDjjBAjL8KXldho1Umo=
github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa/go.mod h1:UkgAKjyQo+Pylt2HTYz/G0PgnxmKOJ9IX/3XiRYQ9Ns=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
@ -763,6 +771,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
@ -996,6 +1005,7 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=

View file

@ -46,6 +46,8 @@ nav:
- IBM:
- Secrets Manager: provider-ibm-secrets-manager.md
- HashiCorp Vault: provider-hashicorp-vault.md
- Yandex:
- Lockbox: provider-yandex-lockbox.md
- References:
- API specification: spec.md
- Contributing:

View file

@ -147,7 +147,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
}
defer func() {
err = secretClient.Close()
err = secretClient.Close(ctx)
if err != nil {
log.Error(err, errCloseStoreClient)
}

View file

@ -90,6 +90,6 @@ func (pm *ParameterStore) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
return secretData, nil
}
func (pm *ParameterStore) Close() error {
func (pm *ParameterStore) Close(ctx context.Context) error {
return nil
}

View file

@ -123,6 +123,6 @@ func (sm *SecretsManager) GetSecretMap(ctx context.Context, ref esv1alpha1.Exter
return secretData, nil
}
func (sm *SecretsManager) Close() error {
func (sm *SecretsManager) Close(ctx context.Context) error {
return nil
}

View file

@ -227,7 +227,7 @@ func (a *Azure) secretKeyRef(ctx context.Context, namespace string, secretRef sm
return value, nil
}
func (a *Azure) Close() error {
func (a *Azure) Close(ctx context.Context) error {
return nil
}

View file

@ -74,7 +74,7 @@ func (v *Client) WithGetSecret(secData []byte, err error) *Client {
func (v *Client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return v.GetSecretMapFn(ctx, ref)
}
func (v *Client) Close() error {
func (v *Client) Close(ctx context.Context) error {
return nil
}

View file

@ -210,7 +210,7 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1alpha1.External
return secretData, nil
}
func (sm *ProviderGCP) Close() error {
func (sm *ProviderGCP) Close(ctx context.Context) error {
err := sm.SecretManagerClient.Close()
if err != nil {
return fmt.Errorf(errClientClose, err)

View file

@ -289,7 +289,7 @@ func (ibm *providerIBM) GetSecretMap(ctx context.Context, ref esv1alpha1.Externa
}
}
func (ibm *providerIBM) Close() error {
func (ibm *providerIBM) Close(ctx context.Context) error {
return nil
}

View file

@ -35,5 +35,5 @@ type SecretsClient interface {
// GetSecretMap returns multiple k/v pairs from the provider
GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error)
Close() error
Close(ctx context.Context) error
}

View file

@ -22,4 +22,5 @@ import (
_ "github.com/external-secrets/external-secrets/pkg/provider/gcp/secretmanager"
_ "github.com/external-secrets/external-secrets/pkg/provider/ibm"
_ "github.com/external-secrets/external-secrets/pkg/provider/vault"
_ "github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox"
)

View file

@ -41,7 +41,7 @@ func (p *PP) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretData
return map[string][]byte{}, nil
}
func (p *PP) Close() error {
func (p *PP) Close(ctx context.Context) error {
return nil
}

View file

@ -155,7 +155,7 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecret
return v.readSecret(ctx, ref.Key, ref.Version)
}
func (v *client) Close() error {
func (v *client) Close(ctx context.Context) error {
return nil
}

View file

@ -0,0 +1,39 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package client
import (
"context"
"time"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
)
// Creates Lockbox clients and Yandex.Cloud IAM tokens.
type YandexCloudCreator interface {
CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (LockboxClient, error)
CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*IamToken, error)
Now() time.Time
}
type IamToken struct {
Token string
ExpiresAt time.Time
}
// Responsible for accessing Lockbox secrets.
type LockboxClient interface {
GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
}

View file

@ -0,0 +1,151 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fake
import (
"context"
"fmt"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
)
// Fake implementation of YandexCloudCreator.
type YandexCloudCreator struct {
Backend *LockboxBackend
}
func (c *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
return &LockboxClient{c.Backend}, nil
}
func (c *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
return c.Backend.getToken(authorizedKey)
}
func (c *YandexCloudCreator) Now() time.Time {
return c.Backend.now
}
// Fake implementation of LockboxClient.
type LockboxClient struct {
fakeLockboxBackend *LockboxBackend
}
func (c *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
return c.fakeLockboxBackend.getEntries(iamToken, secretID, versionID)
}
// Fakes Yandex Lockbox service backend.
type LockboxBackend struct {
secretMap map[secretKey]secretValue // secret specific data
versionMap map[versionKey]versionValue // version specific data
tokenMap map[tokenKey]tokenValue // token specific data
tokenExpirationDuration time.Duration
now time.Time // fakes the current time
}
type secretKey struct {
secretID string
}
type secretValue struct {
expectedAuthorizedKey *iamkey.Key // authorized key expected to access the secret
}
type versionKey struct {
secretID string
versionID string
}
type versionValue struct {
entries []*lockbox.Payload_Entry
}
type tokenKey struct {
token string
}
type tokenValue struct {
authorizedKey *iamkey.Key
expiresAt time.Time
}
func NewLockboxBackend(tokenExpirationDuration time.Duration) *LockboxBackend {
return &LockboxBackend{
secretMap: make(map[secretKey]secretValue),
versionMap: make(map[versionKey]versionValue),
tokenMap: make(map[tokenKey]tokenValue),
tokenExpirationDuration: tokenExpirationDuration,
now: time.Time{},
}
}
func (lb *LockboxBackend) CreateSecret(authorizedKey *iamkey.Key, entries ...*lockbox.Payload_Entry) (string, string) {
secretID := uuid.NewString()
versionID := uuid.NewString()
lb.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
return secretID, versionID
}
func (lb *LockboxBackend) AddVersion(secretID string, entries ...*lockbox.Payload_Entry) string {
versionID := uuid.NewString()
lb.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
lb.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
return versionID
}
func (lb *LockboxBackend) AdvanceClock(duration time.Duration) {
lb.now = lb.now.Add(duration)
}
func (lb *LockboxBackend) getToken(authorizedKey *iamkey.Key) (*client.IamToken, error) {
token := uuid.NewString()
expiresAt := lb.now.Add(lb.tokenExpirationDuration)
lb.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
return &client.IamToken{Token: token, ExpiresAt: expiresAt}, nil
}
func (lb *LockboxBackend) getEntries(iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
if _, ok := lb.secretMap[secretKey{secretID}]; !ok {
return nil, fmt.Errorf("secret not found")
}
if _, ok := lb.versionMap[versionKey{secretID, versionID}]; !ok {
return nil, fmt.Errorf("version not found")
}
if _, ok := lb.tokenMap[tokenKey{iamToken}]; !ok {
return nil, fmt.Errorf("unauthenticated")
}
if lb.tokenMap[tokenKey{iamToken}].expiresAt.Before(lb.now) {
return nil, fmt.Errorf("iam token expired")
}
if !cmp.Equal(lb.tokenMap[tokenKey{iamToken}].authorizedKey, lb.secretMap[secretKey{secretID}].expectedAuthorizedKey) {
return nil, fmt.Errorf("permission denied")
}
return lb.versionMap[versionKey{secretID, versionID}].entries, nil
}

View file

@ -0,0 +1,144 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package grpc
import (
"context"
"crypto/tls"
"time"
"github.com/yandex-cloud/go-genproto/yandex/cloud/endpoint"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
ycsdk "github.com/yandex-cloud/go-sdk"
"github.com/yandex-cloud/go-sdk/iamkey"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/keepalive"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
)
// Implementation of YandexCloudCreator.
type YandexCloudCreator struct {
}
func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
if err != nil {
return nil, err
}
payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
})
if err != nil {
return nil, err
}
err = closeSDK(ctx, sdk)
if err != nil {
return nil, err
}
conn, err := grpc.Dial(payloadAPIEndpoint.Address,
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12})),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: time.Second * 30,
Timeout: time.Second * 10,
PermitWithoutStream: false,
}),
grpc.WithUserAgent("external-secrets"),
)
if err != nil {
return nil, err
}
return &LockboxClient{lockbox.NewPayloadServiceClient(conn)}, nil
}
func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
if err != nil {
return nil, err
}
iamToken, err := sdk.CreateIAMToken(ctx)
if err != nil {
return nil, err
}
err = closeSDK(ctx, sdk)
if err != nil {
return nil, err
}
return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
}
func (lb *YandexCloudCreator) Now() time.Time {
return time.Now()
}
func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
creds, err := ycsdk.ServiceAccountKey(authorizedKey)
if err != nil {
return nil, err
}
sdk, err := ycsdk.Build(ctx, ycsdk.Config{
Credentials: creds,
Endpoint: apiEndpoint,
})
if err != nil {
return nil, err
}
return sdk, nil
}
func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
return sdk.Shutdown(ctx)
}
// Implementation of LockboxClient.
type LockboxClient struct {
lockboxPayloadClient lockbox.PayloadServiceClient
}
func (lc *LockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error) {
payload, err := lc.lockboxPayloadClient.Get(
ctx,
&lockbox.GetPayloadRequest{
SecretId: secretID,
VersionId: versionID,
},
grpc.PerRPCCredentials(perRPCCredentials{iamToken: iamToken}),
)
if err != nil {
return nil, err
}
return payload.Entries, nil
}
type perRPCCredentials struct {
iamToken string
}
func (t perRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
return map[string]string{"Authorization": "Bearer " + t.iamToken}, nil
}
func (perRPCCredentials) RequireTransportSecurity() bool {
return true
}

View file

@ -0,0 +1,299 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lockbox
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
"github.com/external-secrets/external-secrets/pkg/provider"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/grpc"
)
const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
const iamTokenCleanupDelay = 1 * time.Hour // specifies how often cleanUpIamTokenMap() is performed
var log = ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox")
type iamTokenKey struct {
authorizedKeyID string
serviceAccountID string
privateKeyHash string
}
// lockboxProvider is a provider for Yandex Lockbox.
type lockboxProvider struct {
yandexCloudCreator client.YandexCloudCreator
lockboxClientMap map[string]client.LockboxClient // apiEndpoint -> LockboxClient
lockboxClientMapMutex sync.Mutex
iamTokenMap map[iamTokenKey]*client.IamToken
iamTokenMapMutex sync.Mutex
}
func newLockboxProvider(yandexCloudCreator client.YandexCloudCreator) *lockboxProvider {
return &lockboxProvider{
yandexCloudCreator: yandexCloudCreator,
lockboxClientMap: make(map[string]client.LockboxClient),
iamTokenMap: make(map[iamTokenKey]*client.IamToken),
}
}
// NewClient constructs a Yandex Lockbox Provider.
func (p *lockboxProvider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube kclient.Client, namespace string) (provider.SecretsClient, error) {
storeSpec := store.GetSpec()
if storeSpec == nil || storeSpec.Provider == nil || storeSpec.Provider.YandexLockbox == nil {
return nil, fmt.Errorf("received invalid Yandex Lockbox SecretStore resource")
}
storeSpecYandexLockbox := storeSpec.Provider.YandexLockbox
authorizedKeySecretName := storeSpecYandexLockbox.Auth.AuthorizedKey.Name
if authorizedKeySecretName == "" {
return nil, fmt.Errorf("invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
}
objectKey := types.NamespacedName{
Name: authorizedKeySecretName,
Namespace: namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind {
if storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
}
objectKey.Namespace = *storeSpecYandexLockbox.Auth.AuthorizedKey.Namespace
}
authorizedKeySecret := &corev1.Secret{}
err := kube.Get(ctx, objectKey, authorizedKeySecret)
if err != nil {
return nil, fmt.Errorf("could not fetch AuthorizedKey secret: %w", err)
}
authorizedKeySecretData := authorizedKeySecret.Data[storeSpecYandexLockbox.Auth.AuthorizedKey.Key]
if (authorizedKeySecretData == nil) || (len(authorizedKeySecretData) == 0) {
return nil, fmt.Errorf("missing AuthorizedKey")
}
var authorizedKey iamkey.Key
err = json.Unmarshal(authorizedKeySecretData, &authorizedKey)
if err != nil {
return nil, fmt.Errorf("unable to unmarshal authorized key: %w", err)
}
lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
if err != nil {
return nil, fmt.Errorf("failed to create Yandex Lockbox client: %w", err)
}
iamToken, err := p.getOrCreateIamToken(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey)
if err != nil {
return nil, fmt.Errorf("failed to create IAM token: %w", err)
}
return &lockboxSecretsClient{lockboxClient, iamToken.Token}, nil
}
func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (client.LockboxClient, error) {
p.lockboxClientMapMutex.Lock()
defer p.lockboxClientMapMutex.Unlock()
if _, ok := p.lockboxClientMap[apiEndpoint]; !ok {
log.Info("creating LockboxClient", "apiEndpoint", apiEndpoint)
lockboxClient, err := p.yandexCloudCreator.CreateLockboxClient(ctx, apiEndpoint, authorizedKey)
if err != nil {
return nil, err
}
p.lockboxClientMap[apiEndpoint] = lockboxClient
}
return p.lockboxClientMap[apiEndpoint], nil
}
func (p *lockboxProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*client.IamToken, error) {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
iamTokenKey := buildIamTokenKey(authorizedKey)
if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
log.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
iamToken, err := p.yandexCloudCreator.CreateIamToken(ctx, apiEndpoint, authorizedKey)
if err != nil {
return nil, err
}
log.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
p.iamTokenMap[iamTokenKey] = iamToken
}
return p.iamTokenMap[iamTokenKey], nil
}
func (p *lockboxProvider) isIamTokenUsable(iamToken *client.IamToken) bool {
now := p.yandexCloudCreator.Now()
return now.Add(maxSecretsClientLifetime).Before(iamToken.ExpiresAt)
}
func buildIamTokenKey(authorizedKey *iamkey.Key) iamTokenKey {
privateKeyHash := sha256.Sum256([]byte(authorizedKey.PrivateKey))
return iamTokenKey{
authorizedKey.GetId(),
authorizedKey.GetServiceAccountId(),
hex.EncodeToString(privateKeyHash[:]),
}
}
// Used for testing.
func (p *lockboxProvider) isIamTokenCached(authorizedKey *iamkey.Key) bool {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
return ok
}
func (p *lockboxProvider) cleanUpIamTokenMap() {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
for key, value := range p.iamTokenMap {
if p.yandexCloudCreator.Now().After(value.ExpiresAt) {
log.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
delete(p.iamTokenMap, key)
}
}
}
// lockboxSecretsClient is a secrets client for Yandex Lockbox.
type lockboxSecretsClient struct {
lockboxClient client.LockboxClient
iamToken string
}
// GetSecret returns a single secret from the provider.
func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) {
entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
if err != nil {
return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
}
if ref.Property == "" {
keyToValue := make(map[string]interface{}, len(entries))
for _, entry := range entries {
value, err := getValueAsIs(entry)
if err != nil {
return nil, err
}
keyToValue[entry.Key] = value
}
out, err := json.Marshal(keyToValue)
if err != nil {
return nil, fmt.Errorf("failed to marshal secret: %w", err)
}
return out, nil
}
entry, err := findEntryByKey(entries, ref.Property)
if err != nil {
return nil, err
}
return getValueAsBinary(entry)
}
// GetSecretMap returns multiple k/v pairs from the provider.
func (c *lockboxSecretsClient) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
entries, err := c.lockboxClient.GetPayloadEntries(ctx, c.iamToken, ref.Key, ref.Version)
if err != nil {
return nil, fmt.Errorf("unable to request secret payload to get secret map: %w", err)
}
secretMap := make(map[string][]byte, len(entries))
for _, entry := range entries {
value, err := getValueAsBinary(entry)
if err != nil {
return nil, err
}
secretMap[entry.Key] = value
}
return secretMap, nil
}
func (c *lockboxSecretsClient) Close(ctx context.Context) error {
return nil
}
func getValueAsIs(entry *lockbox.Payload_Entry) (interface{}, error) {
switch entry.Value.(type) {
case *lockbox.Payload_Entry_TextValue:
return entry.GetTextValue(), nil
case *lockbox.Payload_Entry_BinaryValue:
return entry.GetBinaryValue(), nil
default:
return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
}
}
func getValueAsBinary(entry *lockbox.Payload_Entry) ([]byte, error) {
switch entry.Value.(type) {
case *lockbox.Payload_Entry_TextValue:
return []byte(entry.GetTextValue()), nil
case *lockbox.Payload_Entry_BinaryValue:
return entry.GetBinaryValue(), nil
default:
return nil, fmt.Errorf("unsupported payload value type, key: %v", entry.Key)
}
}
func findEntryByKey(entries []*lockbox.Payload_Entry, key string) (*lockbox.Payload_Entry, error) {
for i := range entries {
if entries[i].Key == key {
return entries[i], nil
}
}
return nil, fmt.Errorf("payload entry with key '%s' not found", key)
}
func init() {
lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
go func() {
for {
time.Sleep(iamTokenCleanupDelay)
lockboxProvider.cleanUpIamTokenMap()
}
}()
schema.Register(
lockboxProvider,
&esv1alpha1.SecretStoreProvider{
YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
},
)
}

View file

@ -0,0 +1,677 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package lockbox
import (
"context"
b64 "encoding/base64"
"encoding/json"
"testing"
"time"
"github.com/google/uuid"
tassert "github.com/stretchr/testify/assert"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/schema"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
)
func TestNewClient(t *testing.T) {
ctx := context.Background()
const namespace = "namespace"
store := &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
YandexLockbox: &esv1alpha1.YandexLockboxProvider{},
},
},
}
provider, err := schema.GetProvider(store)
tassert.Nil(t, err)
k8sClient := clientfake.NewClientBuilder().Build()
secretClient, err := provider.NewClient(context.Background(), store, k8sClient, namespace)
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
tassert.Nil(t, secretClient)
store.Spec.Provider.YandexLockbox.Auth = esv1alpha1.YandexLockboxAuth{}
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
tassert.Nil(t, secretClient)
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey = esmeta.SecretKeySelector{}
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
tassert.EqualError(t, err, "invalid Yandex Lockbox SecretStore resource: missing AuthorizedKey Name")
tassert.Nil(t, secretClient)
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Name = authorizedKeySecretName
store.Spec.Provider.YandexLockbox.Auth.AuthorizedKey.Key = authorizedKeySecretKey
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
tassert.EqualError(t, err, "could not fetch AuthorizedKey secret: secrets \"authorizedKeySecretName\" not found")
tassert.Nil(t, secretClient)
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, newFakeAuthorizedKey())
tassert.Nil(t, err)
secretClient, err = provider.NewClient(context.Background(), store, k8sClient, namespace)
tassert.EqualError(t, err, "failed to create Yandex Lockbox client: private key parsing failed: Invalid Key: Key must be PEM encoded PKCS1 or PKCS8 private key")
tassert.Nil(t, secretClient)
}
func TestGetSecretForAllEntries(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
tassert.Nil(t, err)
tassert.Equal(
t,
map[string]string{
k1: v1,
k2: base64(v2),
},
unmarshalStringMap(t, data),
)
}
func TestGetSecretForTextEntry(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
tassert.Nil(t, err)
tassert.Equal(t, v1, string(data))
}
func TestGetSecretForBinaryEntry(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
tassert.Nil(t, err)
tassert.Equal(t, v2, data)
}
func TestGetSecretByVersionID(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
oldKey, oldVal := "oldKey", "oldVal"
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
textEntry(oldKey, oldVal),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
newKey, newVal := "newKey", "newVal"
newVersionID := lockboxBackend.AddVersion(secretID,
textEntry(newKey, newVal),
)
data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
data, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string]string{newKey: newVal}, unmarshalStringMap(t, data))
}
func TestGetSecretUnauthorized(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKeyA := newFakeAuthorizedKey()
authorizedKeyB := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
textEntry("k1", "v1"),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
}
func TestGetSecretNotFound(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry("k1", "v1"),
)
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
tassert.EqualError(t, err, "unable to request secret payload to get secret: version not found")
}
func TestGetSecretWithTwoNamespaces(t *testing.T) {
ctx := context.Background()
namespace1 := uuid.NewString()
namespace2 := uuid.NewString()
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
textEntry(k1, v1),
)
k2, v2 := "k2", "v2"
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
textEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
tassert.Nil(t, err)
err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
tassert.Nil(t, err)
store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
tassert.Nil(t, err)
secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
tassert.Nil(t, err)
data, err := secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
tassert.Equal(t, v1, string(data))
tassert.Nil(t, err)
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
tassert.Nil(t, data)
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
tassert.Nil(t, data)
tassert.EqualError(t, err, "unable to request secret payload to get secret: permission denied")
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
tassert.Equal(t, v2, string(data))
tassert.Nil(t, err)
}
func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
ctx := context.Background()
apiEndpoint1 := uuid.NewString()
apiEndpoint2 := uuid.NewString()
namespace := uuid.NewString()
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
textEntry(k1, v1),
)
lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
k2, v2 := "k2", "v2"
secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
textEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName1 = "authorizedKeySecretName1"
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
tassert.Nil(t, err)
const authorizedKeySecretName2 = "authorizedKeySecretName2"
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
tassert.Nil(t, err)
store1 := newYandexLockboxSecretStore(apiEndpoint1, namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
store2 := newYandexLockboxSecretStore(apiEndpoint2, namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
provider1 := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend1,
})
provider2 := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend2,
})
secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
tassert.Nil(t, err)
secretsClient2, err := provider2.NewClient(ctx, store2, k8sClient, namespace)
tassert.Nil(t, err)
var data []byte
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
tassert.Equal(t, v1, string(data))
tassert.Nil(t, err)
data, err = secretsClient1.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
tassert.Nil(t, data)
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1, Property: k1})
tassert.Nil(t, data)
tassert.EqualError(t, err, "unable to request secret payload to get secret: secret not found")
data, err = secretsClient2.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2, Property: k2})
tassert.Equal(t, v2, string(data))
tassert.Nil(t, err)
}
func TestGetSecretWithIamTokenExpiration(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
tokenExpirationTime := time.Hour
lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
k1, v1 := "k1", "v1"
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry(k1, v1),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
var data []byte
oldSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
tassert.Equal(t, v1, string(data))
tassert.Nil(t, err)
lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
data, err = oldSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
tassert.Nil(t, data)
tassert.EqualError(t, err, "unable to request secret payload to get secret: iam token expired")
newSecretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err = newSecretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
tassert.Equal(t, v1, string(data))
tassert.Nil(t, err)
}
func TestGetSecretWithIamTokenCleanup(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
tokenExpirationDuration := time.Hour
lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
textEntry("k1", "v1"),
)
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
textEntry("k2", "v2"),
)
var err error
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName1 = "authorizedKeySecretName1"
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
tassert.Nil(t, err)
const authorizedKeySecretName2 = "authorizedKeySecretName2"
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
tassert.Nil(t, err)
store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
// Access secretID1 with authorizedKey1, IAM token for authorizedKey1 should be cached
secretsClient, err := provider.NewClient(ctx, store1, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID1})
tassert.Nil(t, err)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID2})
tassert.Nil(t, err)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
provider.cleanUpIamTokenMap()
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration)
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
provider.cleanUpIamTokenMap()
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
}
func TestGetSecretMap(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID})
tassert.Nil(t, err)
tassert.Equal(
t,
map[string][]byte{
k1: []byte(v1),
k2: v2,
},
data,
)
}
func TestGetSecretMapByVersionID(t *testing.T) {
ctx := context.Background()
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
oldKey, oldVal := "oldKey", "oldVal"
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
textEntry(oldKey, oldVal),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
newKey, newVal := "newKey", "newVal"
newVersionID := lockboxBackend.AddVersion(secretID,
textEntry(newKey, newVal),
)
data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
data, err = secretsClient.GetSecretMap(ctx, esv1alpha1.ExternalSecretDataRemoteRef{Key: secretID, Version: newVersionID})
tassert.Nil(t, err)
tassert.Equal(t, map[string][]byte{newKey: []byte(newVal)}, data)
}
// helper functions
func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1alpha1.GenericStore {
return &esv1alpha1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
},
Spec: esv1alpha1.SecretStoreSpec{
Provider: &esv1alpha1.SecretStoreProvider{
YandexLockbox: &esv1alpha1.YandexLockboxProvider{
APIEndpoint: apiEndpoint,
Auth: esv1alpha1.YandexLockboxAuth{
AuthorizedKey: esmeta.SecretKeySelector{
Name: authorizedKeySecretName,
Key: authorizedKeySecretKey,
},
},
},
},
},
}
}
func createK8sSecret(ctx context.Context, k8sClient client.Client, namespace, secretName, secretKey string, secretContent interface{}) error {
data, err := json.Marshal(secretContent)
if err != nil {
return err
}
err = k8sClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Data: map[string][]byte{secretKey: data},
})
if err != nil {
return err
}
return nil
}
func newFakeAuthorizedKey() *iamkey.Key {
uniqueLabel := uuid.NewString()
return &iamkey.Key{
Id: uniqueLabel,
Subject: &iamkey.Key_ServiceAccountId{
ServiceAccountId: uniqueLabel,
},
PrivateKey: uniqueLabel,
}
}
func textEntry(key, value string) *lockbox.Payload_Entry {
return &lockbox.Payload_Entry{
Key: key,
Value: &lockbox.Payload_Entry_TextValue{
TextValue: value,
},
}
}
func binaryEntry(key string, value []byte) *lockbox.Payload_Entry {
return &lockbox.Payload_Entry{
Key: key,
Value: &lockbox.Payload_Entry_BinaryValue{
BinaryValue: value,
},
}
}
func unmarshalStringMap(t *testing.T, data []byte) map[string]string {
stringMap := make(map[string]string)
err := json.Unmarshal(data, &stringMap)
tassert.Nil(t, err)
return stringMap
}
func base64(data []byte) string {
return b64.StdEncoding.EncodeToString(data)
}