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

refactor Yandex Lockbox provider

This commit is contained in:
Docs 2022-04-22 21:23:40 +03:00
parent 1286937feb
commit 61c4579ef5
14 changed files with 906 additions and 662 deletions

View file

@ -0,0 +1,22 @@
/*
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 clock
import (
"time"
)
type Clock interface {
CurrentTime() time.Time
}

View file

@ -0,0 +1,32 @@
/*
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 clock
import "time"
type FakeClock struct {
now time.Time
}
func NewFakeClock() *FakeClock {
return &FakeClock{time.Time{}}
}
func (c *FakeClock) CurrentTime() time.Time {
return c.now
}
func (c *FakeClock) AddDuration(duration time.Duration) {
c.now = c.now.Add(duration)
}

View file

@ -0,0 +1,27 @@
/*
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 clock
import "time"
type RealClock struct {
}
func NewRealClock() *RealClock {
return &RealClock{}
}
func (c *RealClock) CurrentTime() time.Time {
return time.Now()
}

View file

@ -0,0 +1,255 @@
/*
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 common
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
clock2 "github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
"github.com/go-logr/logr"
"sync"
"time"
"github.com/yandex-cloud/go-sdk/iamkey"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
kclient "sigs.k8s.io/controller-runtime/pkg/client"
)
const maxSecretsClientLifetime = 5 * time.Minute // supposed SecretsClient lifetime is quite short
// https://github.com/external-secrets/external-secrets/issues/644
var _ esv1beta1.Provider = &YandexCloudProvider{}
// Implementation of v1beta1.Provider
type YandexCloudProvider struct {
logger logr.Logger
clock clock2.Clock
adaptInputFunc AdaptInputFunc
newSecretGetterFunc NewSecretGetterFunc
newIamTokenFunc NewIamTokenFunc
secretGetteMap map[string]SecretGetter // apiEndpoint -> SecretGetter
secretGetterMapMutex sync.Mutex
iamTokenMap map[iamTokenKey]*IamToken
iamTokenMapMutex sync.Mutex
}
type iamTokenKey struct {
authorizedKeyID string
serviceAccountID string
privateKeyHash string
}
func InitYandexCloudProvider(
logger logr.Logger,
clock clock2.Clock,
adaptInputFunc AdaptInputFunc,
newSecretGetterFunc NewSecretGetterFunc,
newIamTokenFunc NewIamTokenFunc,
iamTokenCleanupDelay time.Duration,
) *YandexCloudProvider {
provider := &YandexCloudProvider{
logger: logger,
clock: clock,
adaptInputFunc: adaptInputFunc,
newSecretGetterFunc: newSecretGetterFunc,
newIamTokenFunc: newIamTokenFunc,
secretGetteMap: make(map[string]SecretGetter),
iamTokenMap: make(map[iamTokenKey]*IamToken),
}
if iamTokenCleanupDelay > 0 {
go func() {
for {
time.Sleep(iamTokenCleanupDelay)
provider.CleanUpIamTokenMap()
}
}()
}
return provider
}
type AdaptInputFunc func(store esv1beta1.GenericStore) (*SecretsClientInput, error)
type NewSecretGetterFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error)
type NewIamTokenFunc func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error)
type IamToken struct {
Token string
ExpiresAt time.Time
}
type SecretsClientInput struct {
APIEndpoint string
AuthorizedKey esmeta.SecretKeySelector
CACertificate *esmeta.SecretKeySelector
}
// NewClient constructs a Yandex.Cloud Provider.
func (p *YandexCloudProvider) NewClient(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
input, err := p.adaptInputFunc(store)
if err != nil {
return nil, err
}
objectKey := types.NamespacedName{
Name: input.AuthorizedKey.Name,
Namespace: namespace,
}
// only ClusterStore is allowed to set namespace (and then it's required)
if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
if input.AuthorizedKey.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing AuthorizedKey Namespace")
}
objectKey.Namespace = *input.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[input.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)
}
var caCertificateData []byte
if input.CACertificate != nil {
certObjectKey := types.NamespacedName{
Name: input.CACertificate.Name,
Namespace: namespace,
}
if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
if input.CACertificate.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
}
certObjectKey.Namespace = *input.CACertificate.Namespace
}
caCertificateSecret := &corev1.Secret{}
err := kube.Get(ctx, certObjectKey, caCertificateSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
}
caCertificateData = caCertificateSecret.Data[input.CACertificate.Key]
if (caCertificateData == nil) || (len(caCertificateData) == 0) {
return nil, fmt.Errorf("missing CA Certificate")
}
}
secretGetter, err := p.getOrCreateSecretGetter(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
if err != nil {
return nil, fmt.Errorf("failed to create Yandex.Cloud client: %w", err)
}
iamToken, err := p.getOrCreateIamToken(ctx, input.APIEndpoint, &authorizedKey, caCertificateData)
if err != nil {
return nil, fmt.Errorf("failed to create IAM token: %w", err)
}
return &yandexCloudSecretsClient{secretGetter, iamToken.Token}, nil
}
func (p *YandexCloudProvider) getOrCreateSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (SecretGetter, error) {
p.secretGetterMapMutex.Lock()
defer p.secretGetterMapMutex.Unlock()
if _, ok := p.secretGetteMap[apiEndpoint]; !ok {
p.logger.Info("creating SecretGetter", "apiEndpoint", apiEndpoint)
secretGetter, err := p.newSecretGetterFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
if err != nil {
return nil, err
}
p.secretGetteMap[apiEndpoint] = secretGetter
}
return p.secretGetteMap[apiEndpoint], nil
}
func (p *YandexCloudProvider) getOrCreateIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
iamTokenKey := buildIamTokenKey(authorizedKey)
if iamToken, ok := p.iamTokenMap[iamTokenKey]; !ok || !p.isIamTokenUsable(iamToken) {
p.logger.Info("creating IAM token", "authorizedKeyId", authorizedKey.Id)
iamToken, err := p.newIamTokenFunc(ctx, apiEndpoint, authorizedKey, caCertificate)
if err != nil {
return nil, err
}
p.logger.Info("created IAM token", "authorizedKeyId", authorizedKey.Id, "expiresAt", iamToken.ExpiresAt)
p.iamTokenMap[iamTokenKey] = iamToken
}
return p.iamTokenMap[iamTokenKey], nil
}
func (p *YandexCloudProvider) isIamTokenUsable(iamToken *IamToken) bool {
now := p.clock.CurrentTime()
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 *YandexCloudProvider) IsIamTokenCached(authorizedKey *iamkey.Key) bool {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
_, ok := p.iamTokenMap[buildIamTokenKey(authorizedKey)]
return ok
}
func (p *YandexCloudProvider) CleanUpIamTokenMap() {
p.iamTokenMapMutex.Lock()
defer p.iamTokenMapMutex.Unlock()
for key, value := range p.iamTokenMap {
if p.clock.CurrentTime().After(value.ExpiresAt) {
p.logger.Info("deleting IAM token", "authorizedKeyId", key.authorizedKeyID)
delete(p.iamTokenMap, key)
}
}
}
func (p *YandexCloudProvider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}

View file

@ -11,32 +11,36 @@ 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
package common
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"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"
"time"
)
// Implementation of YandexCloudCreator.
type YandexCloudCreator struct {
}
// Creates a connection to the given Yandex.Cloud API endpoint
func NewGrpcConnection(
ctx context.Context,
apiEndpoint string,
apiEndpointID string, // an ID from https://api.cloud.yandex.net/endpoints
authorizedKey *iamkey.Key,
caCertificate []byte,
) (*grpc.ClientConn, error) {
tlsConfig, err := tlsConfig(caCertificate)
if err != nil {
return nil, err
}
func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (client.LockboxClient, error) {
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey)
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
if err != nil {
return nil, err
}
@ -44,26 +48,15 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
_ = closeSDK(ctx, sdk)
}()
payloadAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
ApiEndpointId: "lockbox-payload", // the ID from https://api.cloud.yandex.net/endpoints
serviceAPIEndpoint, err := sdk.ApiEndpoint().ApiEndpoint().Get(ctx, &endpoint.GetApiEndpointRequest{
ApiEndpointId: apiEndpointID,
})
if err != nil {
return nil, err
}
tlsConfig := tls.Config{MinVersion: tls.VersionTLS12}
if caCertificate != nil {
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(caCertificate)
if !ok {
return nil, errors.New("unable to read certificate from PEM file")
}
tlsConfig.RootCAs = caCertPool
}
conn, err := grpc.Dial(payloadAPIEndpoint.Address,
grpc.WithTransportCredentials(credentials.NewTLS(&tlsConfig)),
return grpc.Dial(serviceAPIEndpoint.Address,
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: time.Second * 30,
Timeout: time.Second * 10,
@ -71,15 +64,16 @@ func (lb *YandexCloudCreator) CreateLockboxClient(ctx context.Context, apiEndpoi
}),
grpc.WithUserAgent("external-secrets"),
)
}
// Exchanges the given authorized key to an IAM token
func NewIamToken(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*IamToken, error) {
tlsConfig, err := tlsConfig(caCertificate)
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)
sdk, err := buildSDK(ctx, apiEndpoint, authorizedKey, tlsConfig)
if err != nil {
return nil, err
}
@ -92,14 +86,23 @@ func (lb *YandexCloudCreator) CreateIamToken(ctx context.Context, apiEndpoint st
return nil, err
}
return &client.IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
return &IamToken{Token: iamToken.IamToken, ExpiresAt: iamToken.ExpiresAt.AsTime()}, nil
}
func (lb *YandexCloudCreator) Now() time.Time {
return time.Now()
func tlsConfig(caCertificate []byte) (*tls.Config, error) {
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
if caCertificate != nil {
caCertPool := x509.NewCertPool()
ok := caCertPool.AppendCertsFromPEM(caCertificate)
if !ok {
return nil, errors.New("unable to read trusted CA certificates")
}
tlsConfig.RootCAs = caCertPool
}
return tlsConfig, nil
}
func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key) (*ycsdk.SDK, error) {
func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, tlsConfig *tls.Config) (*ycsdk.SDK, error) {
creds, err := ycsdk.ServiceAccountKey(authorizedKey)
if err != nil {
return nil, err
@ -108,6 +111,7 @@ func buildSDK(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key
sdk, err := ycsdk.Build(ctx, ycsdk.Config{
Credentials: creds,
Endpoint: apiEndpoint,
TLSConfig: tlsConfig,
})
if err != nil {
return nil, err
@ -120,34 +124,14 @@ func closeSDK(ctx context.Context, sdk *ycsdk.SDK) error {
return sdk.Shutdown(ctx)
}
// Implementation of LockboxClient.
type LockboxClient struct {
lockboxPayloadClient lockbox.PayloadServiceClient
type PerRPCCredentials struct {
IamToken string
}
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
func (t PerRPCCredentials) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) {
return map[string]string{"Authorization": "Bearer " + t.IamToken}, 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 {
func (PerRPCCredentials) RequireTransportSecurity() bool {
return true
}

View file

@ -0,0 +1,24 @@
/*
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 common
import (
"context"
)
// Adapts the secrets received from a remote Yandex.Cloud service for the format expected by v1beta1.SecretsClient
type SecretGetter interface {
GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error)
GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error)
}

View file

@ -0,0 +1,50 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package common
import (
"context"
"fmt"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
)
// https://github.com/external-secrets/external-secrets/issues/644
var _ esv1beta1.SecretsClient = &yandexCloudSecretsClient{}
// Implementation of v1beta1.SecretsClient
type yandexCloudSecretsClient struct {
secretGetter SecretGetter
iamToken string
}
func (c *yandexCloudSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
return nil, fmt.Errorf("GetAllSecrets not implemented")
}
func (c *yandexCloudSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
return c.secretGetter.GetSecret(ctx, c.iamToken, ref.Key, ref.Version, ref.Property)
}
func (c *yandexCloudSecretsClient) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
return c.secretGetter.GetSecretMap(ctx, c.iamToken, ref.Key, ref.Version)
}
func (c *yandexCloudSecretsClient) Close(ctx context.Context) error {
return nil
}
func (c *yandexCloudSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, nil
}

View file

@ -15,25 +15,10 @@ package client
import (
"context"
"time"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
)
// Creates Lockbox clients and Yandex.Cloud IAM tokens.
type YandexCloudCreator interface {
CreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (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.
// Requests the payload of the given secret from Lockbox
type LockboxClient interface {
GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*lockbox.Payload_Entry, error)
GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error)
}

View file

@ -1,152 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 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/go-cmp/cmp/cmpopts"
"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, caCertificate []byte) (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, cmpopts.IgnoreUnexported(iamkey.Key{})) {
return nil, fmt.Errorf("permission denied")
}
return lb.versionMap[versionKey{secretID, versionID}].entries, nil
}

View file

@ -0,0 +1,134 @@
/*
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"
"fmt"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
"time"
)
// Fake implementation of LockboxClient
type fakeLockboxClient struct {
fakeLockboxServer *FakeLockboxServer
}
func NewFakeLockboxClient(fakeLockboxServer *FakeLockboxServer) LockboxClient {
return &fakeLockboxClient{fakeLockboxServer}
}
func (c *fakeLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
return c.fakeLockboxServer.getEntries(iamToken, secretID, versionID)
}
// Fakes Yandex Lockbox service backend.
type FakeLockboxServer 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
clock clock.Clock
}
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 []*api.Payload_Entry
}
type tokenKey struct {
token string
}
type tokenValue struct {
authorizedKey *iamkey.Key
expiresAt time.Time
}
func NewFakeLockboxServer(clock clock.Clock, tokenExpirationDuration time.Duration) *FakeLockboxServer {
return &FakeLockboxServer{
secretMap: make(map[secretKey]secretValue),
versionMap: make(map[versionKey]versionValue),
tokenMap: make(map[tokenKey]tokenValue),
tokenExpirationDuration: tokenExpirationDuration,
clock: clock,
}
}
func (s *FakeLockboxServer) CreateSecret(authorizedKey *iamkey.Key, entries ...*api.Payload_Entry) (string, string) {
secretID := uuid.NewString()
versionID := uuid.NewString()
s.secretMap[secretKey{secretID}] = secretValue{authorizedKey}
s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
return secretID, versionID
}
func (s *FakeLockboxServer) AddVersion(secretID string, entries ...*api.Payload_Entry) string {
versionID := uuid.NewString()
s.versionMap[versionKey{secretID, ""}] = versionValue{entries} // empty versionID corresponds to the latest version
s.versionMap[versionKey{secretID, versionID}] = versionValue{entries}
return versionID
}
func (s *FakeLockboxServer) NewIamToken(authorizedKey *iamkey.Key) *common.IamToken {
token := uuid.NewString()
expiresAt := s.clock.CurrentTime().Add(s.tokenExpirationDuration)
s.tokenMap[tokenKey{token}] = tokenValue{authorizedKey, expiresAt}
return &common.IamToken{Token: token, ExpiresAt: expiresAt}
}
func (s *FakeLockboxServer) getEntries(iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
if _, ok := s.secretMap[secretKey{secretID}]; !ok {
return nil, fmt.Errorf("secret not found")
}
if _, ok := s.versionMap[versionKey{secretID, versionID}]; !ok {
return nil, fmt.Errorf("version not found")
}
if _, ok := s.tokenMap[tokenKey{iamToken}]; !ok {
return nil, fmt.Errorf("unauthenticated")
}
if s.tokenMap[tokenKey{iamToken}].expiresAt.Before(s.clock.CurrentTime()) {
return nil, fmt.Errorf("iam token expired")
}
if !cmp.Equal(s.tokenMap[tokenKey{iamToken}].authorizedKey, s.secretMap[secretKey{secretID}].expectedAuthorizedKey, cmpopts.IgnoreUnexported(iamkey.Key{})) {
return nil, fmt.Errorf("permission denied")
}
return s.versionMap[versionKey{secretID, versionID}].entries, nil
}

View file

@ -0,0 +1,56 @@
/*
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"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
api "github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
"github.com/yandex-cloud/go-sdk/iamkey"
"google.golang.org/grpc"
)
// Real/gRPC implementation of LockboxClient
type grpcLockboxClient struct {
lockboxPayloadClient api.PayloadServiceClient
}
func NewGrpcLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (LockboxClient, error) {
conn, err := common.NewGrpcConnection(
ctx,
apiEndpoint,
"lockbox-payload", // taken from https://api.cloud.yandex.net/endpoints
authorizedKey,
caCertificate,
)
if err != nil {
return nil, err
}
return &grpcLockboxClient{api.NewPayloadServiceClient(conn)}, nil
}
func (c *grpcLockboxClient) GetPayloadEntries(ctx context.Context, iamToken, secretID, versionID string) ([]*api.Payload_Entry, error) {
payload, err := c.lockboxPayloadClient.Get(
ctx,
&api.GetPayloadRequest{
SecretId: secretID,
VersionId: versionID,
},
grpc.PerRPCCredentials(common.PerRPCCredentials{IamToken: iamToken}),
)
if err != nil {
return nil, err
}
return payload.Entries, nil
}

View file

@ -15,326 +15,63 @@ package lockbox
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sync"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
"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"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
"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
}
// https://github.com/external-secrets/external-secrets/issues/644
var _ esv1beta1.SecretsClient = &lockboxSecretsClient{}
var _ esv1beta1.Provider = &lockboxProvider{}
// 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 esv1beta1.GenericStore, kube kclient.Client, namespace string) (esv1beta1.SecretsClient, error) {
func adaptInput(store esv1beta1.GenericStore) (*common.SecretsClientInput, 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 == "" {
if storeSpecYandexLockbox.Auth.AuthorizedKey.Name == "" {
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 == esv1beta1.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)
}
var caCertificateData []byte
var caCertificate *esmeta.SecretKeySelector
if storeSpecYandexLockbox.CAProvider != nil {
certObjectKey := types.NamespacedName{
Name: storeSpecYandexLockbox.CAProvider.Certificate.Name,
Namespace: namespace,
}
if store.GetObjectKind().GroupVersionKind().Kind == esv1beta1.ClusterSecretStoreKind {
if storeSpecYandexLockbox.CAProvider.Certificate.Namespace == nil {
return nil, fmt.Errorf("invalid ClusterSecretStore: missing CA certificate Namespace")
}
certObjectKey.Namespace = *storeSpecYandexLockbox.CAProvider.Certificate.Namespace
}
caCertificateSecret := &corev1.Secret{}
err := kube.Get(ctx, certObjectKey, caCertificateSecret)
if err != nil {
return nil, fmt.Errorf("could not fetch CA certificate secret: %w", err)
}
caCertificateData = caCertificateSecret.Data[storeSpecYandexLockbox.CAProvider.Certificate.Key]
if (caCertificateData == nil) || (len(caCertificateData) == 0) {
return nil, fmt.Errorf("missing CA Certificate")
}
caCertificate = &storeSpecYandexLockbox.CAProvider.Certificate
}
lockboxClient, err := p.getOrCreateLockboxClient(ctx, storeSpecYandexLockbox.APIEndpoint, &authorizedKey, caCertificateData)
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
return &common.SecretsClientInput{
storeSpecYandexLockbox.APIEndpoint,
storeSpecYandexLockbox.Auth.AuthorizedKey,
caCertificate,
}, nil
}
func (p *lockboxProvider) getOrCreateLockboxClient(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (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, caCertificate)
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)
}
}
}
func (p *lockboxProvider) ValidateStore(store esv1beta1.GenericStore) error {
return nil
}
// lockboxSecretsClient is a secrets client for Yandex Lockbox.
type lockboxSecretsClient struct {
lockboxClient client.LockboxClient
iamToken string
}
// Empty GetAllSecrets.
func (c *lockboxSecretsClient) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecretFind) (map[string][]byte, error) {
// TO be implemented
return nil, fmt.Errorf("GetAllSecrets not implemented")
}
// GetSecret returns a single secret from the provider.
func (c *lockboxSecretsClient) GetSecret(ctx context.Context, ref esv1beta1.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)
func newSecretGetter(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
lockboxClient, err := client.NewGrpcLockboxClient(ctx, apiEndpoint, authorizedKey, caCertificate)
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 esv1beta1.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 (c *lockboxSecretsClient) Validate() (esv1beta1.ValidationResult, error) {
return esv1beta1.ValidationResultReady, 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)
return newLockboxSecretGetter(lockboxClient)
}
func init() {
lockboxProvider := newLockboxProvider(&grpc.YandexCloudCreator{})
go func() {
for {
time.Sleep(iamTokenCleanupDelay)
lockboxProvider.cleanUpIamTokenMap()
}
}()
provider := common.InitYandexCloudProvider(
log,
clock.NewRealClock(),
adaptInput,
newSecretGetter,
common.NewIamToken,
time.Hour,
)
esv1beta1.Register(
lockboxProvider,
provider,
&esv1beta1.SecretStoreProvider{
YandexLockbox: &esv1beta1.YandexLockboxProvider{},
},

View file

@ -15,11 +15,12 @@ package lockbox
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
b64 "encoding/base64"
"encoding/json"
"math/big"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common/clock"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
ctrl "sigs.k8s.io/controller-runtime"
"testing"
"time"
@ -29,12 +30,11 @@ import (
"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"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client/fake"
)
const (
@ -83,7 +83,7 @@ func TestNewClient(t *testing.T) {
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())
err = createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, newFakeAuthorizedKey()))
tassert.Nil(t, err)
const caCertificateSecretName = "caCertificateSecretName"
@ -98,10 +98,10 @@ func TestNewClient(t *testing.T) {
tassert.EqualError(t, err, "could not fetch CA certificate secret: secrets \"caCertificateSecretName\" not found")
tassert.Nil(t, secretClient)
err = createK8sSecret(ctx, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, newFakeCACertificate())
err = createK8sSecret(t, ctx, k8sClient, namespace, caCertificateSecretName, caCertificateSecretKey, []byte("it-is-not-a-certificate"))
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 a PEM encoded PKCS1 or PKCS8 key")
tassert.EqualError(t, err, "failed to create Yandex.Cloud client: unable to read trusted CA certificates")
tassert.Nil(t, secretClient)
}
@ -110,10 +110,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
@ -121,13 +122,11 @@ func TestGetSecretForAllEntries(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@ -148,10 +147,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
@ -159,13 +159,11 @@ func TestGetSecretForTextEntry(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
@ -179,10 +177,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
@ -190,13 +189,11 @@ func TestGetSecretForBinaryEntry(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k2})
@ -210,22 +207,21 @@ func TestGetSecretByVersionID(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
oldKey, oldVal := "oldKey", "oldVal"
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(oldKey, oldVal),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@ -234,7 +230,7 @@ func TestGetSecretByVersionID(t *testing.T) {
tassert.Equal(t, map[string]string{oldKey: oldVal}, unmarshalStringMap(t, data))
newKey, newVal := "newKey", "newVal"
newVersionID := lockboxBackend.AddVersion(secretID,
newVersionID := fakeLockboxServer.AddVersion(secretID,
textEntry(newKey, newVal),
)
@ -253,21 +249,20 @@ func TestGetSecretUnauthorized(t *testing.T) {
authorizedKeyA := newFakeAuthorizedKey()
authorizedKeyB := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
secretID, _ := lockboxBackend.CreateSecret(authorizedKeyA,
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKeyA,
textEntry("k1", "v1"),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKeyB)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKeyB))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@ -279,24 +274,23 @@ func TestGetSecretNotFound(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: "no-secret-with-this-id"})
tassert.EqualError(t, err, errSecretPayloadNotFound)
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry("k1", "v1"),
)
_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: "no-version-with-this-id"})
@ -310,29 +304,28 @@ func TestGetSecretWithTwoNamespaces(t *testing.T) {
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
textEntry(k1, v1),
)
k2, v2 := "k2", "v2"
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
textEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey1)
err := createK8sSecret(t, ctx, k8sClient, namespace1, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey1))
tassert.Nil(t, err)
err = createK8sSecret(ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey2)
err = createK8sSecret(t, ctx, k8sClient, namespace2, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey2))
tassert.Nil(t, err)
store1 := newYandexLockboxSecretStore("", namespace1, authorizedKeySecretName, authorizedKeySecretKey)
store2 := newYandexLockboxSecretStore("", namespace2, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient1, err := provider.NewClient(ctx, store1, k8sClient, namespace1)
tassert.Nil(t, err)
secretsClient2, err := provider.NewClient(ctx, store2, k8sClient, namespace2)
@ -361,36 +354,33 @@ func TestGetSecretWithTwoApiEndpoints(t *testing.T) {
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
lockboxBackend1 := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer1 := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
secretID1, _ := lockboxBackend1.CreateSecret(authorizedKey1,
secretID1, _ := fakeLockboxServer1.CreateSecret(authorizedKey1,
textEntry(k1, v1),
)
lockboxBackend2 := fake.NewLockboxBackend(time.Hour)
fakeLockboxServer2 := client.NewFakeLockboxServer(fakeClock, time.Hour)
k2, v2 := "k2", "v2"
secretID2, _ := lockboxBackend2.CreateSecret(authorizedKey2,
secretID2, _ := fakeLockboxServer2.CreateSecret(authorizedKey2,
textEntry(k2, v2),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName1 = "authorizedKeySecretName1"
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJson(t, authorizedKey1))
tassert.Nil(t, err)
const authorizedKeySecretName2 = "authorizedKeySecretName2"
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
err = createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJson(t, 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,
})
provider1 := newLockboxProvider(fakeClock, fakeLockboxServer1)
provider2 := newLockboxProvider(fakeClock, fakeLockboxServer2)
secretsClient1, err := provider1.NewClient(ctx, store1, k8sClient, namespace)
tassert.Nil(t, err)
@ -419,23 +409,22 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
fakeClock := clock.NewFakeClock()
tokenExpirationTime := time.Hour
lockboxBackend := fake.NewLockboxBackend(tokenExpirationTime)
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationTime)
k1, v1 := "k1", "v1"
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(k1, v1),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
var data []byte
@ -445,7 +434,7 @@ func TestGetSecretWithIamTokenExpiration(t *testing.T) {
tassert.Equal(t, v1, string(data))
tassert.Nil(t, err)
lockboxBackend.AdvanceClock(2 * tokenExpirationTime)
fakeClock.AddDuration(2 * tokenExpirationTime)
data, err = oldSecretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Property: k1})
tassert.Nil(t, data)
@ -464,12 +453,13 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
authorizedKey1 := newFakeAuthorizedKey()
authorizedKey2 := newFakeAuthorizedKey()
fakeClock := clock.NewFakeClock()
tokenExpirationDuration := time.Hour
lockboxBackend := fake.NewLockboxBackend(tokenExpirationDuration)
secretID1, _ := lockboxBackend.CreateSecret(authorizedKey1,
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, tokenExpirationDuration)
secretID1, _ := fakeLockboxServer.CreateSecret(authorizedKey1,
textEntry("k1", "v1"),
)
secretID2, _ := lockboxBackend.CreateSecret(authorizedKey2,
secretID2, _ := fakeLockboxServer.CreateSecret(authorizedKey2,
textEntry("k2", "v2"),
)
@ -478,22 +468,20 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName1 = "authorizedKeySecretName1"
const authorizedKeySecretKey1 = "authorizedKeySecretKey1"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, authorizedKey1)
err = createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName1, authorizedKeySecretKey1, toJson(t, authorizedKey1))
tassert.Nil(t, err)
const authorizedKeySecretName2 = "authorizedKeySecretName2"
const authorizedKeySecretKey2 = "authorizedKeySecretKey2"
err = createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, authorizedKey2)
err = createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName2, authorizedKeySecretKey2, toJson(t, authorizedKey2))
tassert.Nil(t, err)
store1 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName1, authorizedKeySecretKey1)
store2 := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName2, authorizedKeySecretKey2)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
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)
@ -501,10 +489,10 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID1})
tassert.Nil(t, err)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration * 2)
fakeClock.AddDuration(tokenExpirationDuration * 2)
// Access secretID2 with authorizedKey2, IAM token for authorizedKey2 should be cached
secretsClient, err = provider.NewClient(ctx, store2, k8sClient, namespace)
@ -512,28 +500,28 @@ func TestGetSecretWithIamTokenCleanup(t *testing.T) {
_, err = secretsClient.GetSecret(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID2})
tassert.Nil(t, err)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration)
fakeClock.AddDuration(tokenExpirationDuration)
tassert.True(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
tassert.True(t, provider.IsIamTokenCached(authorizedKey1))
tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
provider.cleanUpIamTokenMap()
provider.CleanUpIamTokenMap()
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
lockboxBackend.AdvanceClock(tokenExpirationDuration)
fakeClock.AddDuration(tokenExpirationDuration)
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.True(t, provider.isIamTokenCached(authorizedKey2))
tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
tassert.True(t, provider.IsIamTokenCached(authorizedKey2))
provider.cleanUpIamTokenMap()
provider.CleanUpIamTokenMap()
tassert.False(t, provider.isIamTokenCached(authorizedKey1))
tassert.False(t, provider.isIamTokenCached(authorizedKey2))
tassert.False(t, provider.IsIamTokenCached(authorizedKey1))
tassert.False(t, provider.IsIamTokenCached(authorizedKey2))
}
func TestGetSecretMap(t *testing.T) {
@ -541,10 +529,11 @@ func TestGetSecretMap(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
k1, v1 := "k1", "v1"
k2, v2 := "k2", []byte("v2")
secretID, _ := lockboxBackend.CreateSecret(authorizedKey,
secretID, _ := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(k1, v1),
binaryEntry(k2, v2),
)
@ -552,13 +541,11 @@ func TestGetSecretMap(t *testing.T) {
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID})
@ -579,22 +566,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
namespace := uuid.NewString()
authorizedKey := newFakeAuthorizedKey()
lockboxBackend := fake.NewLockboxBackend(time.Hour)
fakeClock := clock.NewFakeClock()
fakeLockboxServer := client.NewFakeLockboxServer(fakeClock, time.Hour)
oldKey, oldVal := "oldKey", "oldVal"
secretID, oldVersionID := lockboxBackend.CreateSecret(authorizedKey,
secretID, oldVersionID := fakeLockboxServer.CreateSecret(authorizedKey,
textEntry(oldKey, oldVal),
)
k8sClient := clientfake.NewClientBuilder().Build()
const authorizedKeySecretName = "authorizedKeySecretName"
const authorizedKeySecretKey = "authorizedKeySecretKey"
err := createK8sSecret(ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, authorizedKey)
err := createK8sSecret(t, ctx, k8sClient, namespace, authorizedKeySecretName, authorizedKeySecretKey, toJson(t, authorizedKey))
tassert.Nil(t, err)
store := newYandexLockboxSecretStore("", namespace, authorizedKeySecretName, authorizedKeySecretKey)
provider := newLockboxProvider(&fake.YandexCloudCreator{
Backend: lockboxBackend,
})
provider := newLockboxProvider(fakeClock, fakeLockboxServer)
secretsClient, err := provider.NewClient(ctx, store, k8sClient, namespace)
tassert.Nil(t, err)
data, err := secretsClient.GetSecretMap(ctx, esv1beta1.ExternalSecretDataRemoteRef{Key: secretID, Version: oldVersionID})
@ -603,7 +589,7 @@ func TestGetSecretMapByVersionID(t *testing.T) {
tassert.Equal(t, map[string][]byte{oldKey: []byte(oldVal)}, data)
newKey, newVal := "newKey", "newVal"
newVersionID := lockboxBackend.AddVersion(secretID,
newVersionID := fakeLockboxServer.AddVersion(secretID,
textEntry(newKey, newVal),
)
@ -618,6 +604,21 @@ func TestGetSecretMapByVersionID(t *testing.T) {
// helper functions
func newLockboxProvider(clock clock.Clock, fakeLockboxServer *client.FakeLockboxServer) *common.YandexCloudProvider {
return common.InitYandexCloudProvider(
ctrl.Log.WithName("provider").WithName("yandex").WithName("lockbox"),
clock,
adaptInput,
func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (common.SecretGetter, error) {
return newLockboxSecretGetter(client.NewFakeLockboxClient(fakeLockboxServer))
},
func(ctx context.Context, apiEndpoint string, authorizedKey *iamkey.Key, caCertificate []byte) (*common.IamToken, error) {
return fakeLockboxServer.NewIamToken(authorizedKey), nil
},
0,
)
}
func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName, authorizedKeySecretKey string) esv1beta1.GenericStore {
return &esv1beta1.SecretStore{
ObjectMeta: metav1.ObjectMeta{
@ -639,23 +640,21 @@ func newYandexLockboxSecretStore(apiEndpoint, namespace, authorizedKeySecretName
}
}
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
}
func toJson(t *testing.T, v interface{}) []byte {
jsonBytes, err := json.Marshal(v)
tassert.Nil(t, err)
return jsonBytes
}
err = k8sClient.Create(ctx, &corev1.Secret{
func createK8sSecret(t *testing.T, ctx context.Context, k8sClient k8sclient.Client, namespace, secretName, secretKey string, secretValue []byte) error {
err := k8sClient.Create(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: secretName,
},
Data: map[string][]byte{secretKey: data},
Data: map[string][]byte{secretKey: secretValue},
})
if err != nil {
return err
}
tassert.Nil(t, err)
return nil
}
@ -670,26 +669,6 @@ func newFakeAuthorizedKey() *iamkey.Key {
}
}
func newFakeCACertificate() []byte {
cert := x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{
Organization: []string{"Company, INC."},
Country: []string{"US"},
Locality: []string{"San Francisco"},
StreetAddress: []string{"Golden Gate Bridge"},
PostalCode: []string{"94016"},
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(10, 0, 0),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
return cert.Raw
}
func textEntry(key, value string) *lockbox.Payload_Entry {
return &lockbox.Payload_Entry{
Key: key,

View file

@ -0,0 +1,111 @@
/*
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"
"encoding/json"
"fmt"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/common"
"github.com/external-secrets/external-secrets/pkg/provider/yandex/lockbox/client"
"github.com/yandex-cloud/go-genproto/yandex/cloud/lockbox/v1"
)
// Implementation of common.SecretGetter
type lockboxSecretGetter struct {
lockboxClient client.LockboxClient
}
func newLockboxSecretGetter(lockboxClient client.LockboxClient) (common.SecretGetter, error) {
return &lockboxSecretGetter{
lockboxClient: lockboxClient,
}, nil
}
func (g *lockboxSecretGetter) GetSecret(ctx context.Context, iamToken, resourceID, versionID, property string) ([]byte, error) {
entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
if err != nil {
return nil, fmt.Errorf("unable to request secret payload to get secret: %w", err)
}
if 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, property)
if err != nil {
return nil, err
}
return getValueAsBinary(entry)
}
func (g *lockboxSecretGetter) GetSecretMap(ctx context.Context, iamToken, resourceID, versionID string) (map[string][]byte, error) {
entries, err := g.lockboxClient.GetPayloadEntries(ctx, iamToken, resourceID, versionID)
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 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)
}