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:
parent
1286937feb
commit
61c4579ef5
14 changed files with 906 additions and 662 deletions
22
pkg/provider/yandex/common/clock/clock.go
Normal file
22
pkg/provider/yandex/common/clock/clock.go
Normal 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
|
||||
}
|
32
pkg/provider/yandex/common/clock/fakeclock.go
Normal file
32
pkg/provider/yandex/common/clock/fakeclock.go
Normal 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)
|
||||
}
|
27
pkg/provider/yandex/common/clock/realclock.go
Normal file
27
pkg/provider/yandex/common/clock/realclock.go
Normal 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()
|
||||
}
|
255
pkg/provider/yandex/common/provider.go
Normal file
255
pkg/provider/yandex/common/provider.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
24
pkg/provider/yandex/common/secretgetter.go
Normal file
24
pkg/provider/yandex/common/secretgetter.go
Normal 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)
|
||||
}
|
50
pkg/provider/yandex/common/secretsclient.go
Normal file
50
pkg/provider/yandex/common/secretsclient.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
134
pkg/provider/yandex/lockbox/client/fakeclient.go
Normal file
134
pkg/provider/yandex/lockbox/client/fakeclient.go
Normal 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
|
||||
}
|
56
pkg/provider/yandex/lockbox/client/grpcclient.go
Normal file
56
pkg/provider/yandex/lockbox/client/grpcclient.go
Normal 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
|
||||
}
|
|
@ -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{},
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
111
pkg/provider/yandex/lockbox/lockboxsecretgetter.go
Normal file
111
pkg/provider/yandex/lockbox/lockboxsecretgetter.go
Normal 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)
|
||||
}
|
Loading…
Reference in a new issue