SecretsManager: add data key store (#107396)

* SecretsManager: Add data key store

Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>

* SecretsManager: Add wiring of data key store

Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com>

---------

Co-authored-by: Michael Mandrus <michael.mandrus@grafana.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
This commit is contained in:
Dana Axinte 2025-06-30 17:17:07 +01:00 committed by GitHub
parent 2d634639a2
commit 0fccc01ebe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1097 additions and 25 deletions

View File

@ -0,0 +1,35 @@
package contracts
import (
"context"
"errors"
"time"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
)
var (
ErrDataKeyNotFound = errors.New("data key not found")
)
// SecretDataKey does not have a mirrored K8s resource
type SecretDataKey struct {
UID string
Active bool
Namespace string
Label string
Provider encryption.ProviderID
EncryptedData []byte
Created time.Time
Updated time.Time
}
// DataKeyStorage is the interface for wiring and dependency injection.
type DataKeyStorage interface {
CreateDataKey(ctx context.Context, dataKey *SecretDataKey) error
GetDataKey(ctx context.Context, namespace, uid string) (*SecretDataKey, error)
GetCurrentDataKey(ctx context.Context, namespace, label string) (*SecretDataKey, error)
GetAllDataKeys(ctx context.Context, namespace string) ([]*SecretDataKey, error)
DisableDataKeys(ctx context.Context, namespace string) error
DeleteDataKey(ctx context.Context, namespace, uid string) error
}

View File

@ -1,3 +1,26 @@
package encryption
import (
"fmt"
"strings"
"time"
)
const UsageInsightsPrefix = "secrets_manager"
type ProviderID string
func (id ProviderID) Kind() (string, error) {
idStr := string(id)
parts := strings.SplitN(idStr, ".", 2)
if len(parts) != 2 {
return "", fmt.Errorf("malformatted provider identifier %s: expected format <provider>.<keyName>", idStr)
}
return parts[0], nil
}
func KeyLabel(providerID ProviderID) string {
return fmt.Sprintf("%s@%s", time.Now().Format("2006-01-02"), providerID)
}

View File

@ -423,6 +423,7 @@ var wireBasicSet = wire.NewSet(
secretmetadata.ProvideSecureValueMetadataStorage,
secretmetadata.ProvideKeeperMetadataStorage,
secretmetadata.ProvideOutboxQueue,
secretencryption.ProvideDataKeyStorage,
secretencryption.ProvideEncryptedValueStorage,
secretmigrator.NewWithEngine,
secretdatabase.ProvideDatabase,

View File

@ -0,0 +1,19 @@
INSERT INTO {{ .Ident "secret_data_key" }} (
{{ .Ident "uid" }},
{{ .Ident "namespace" }},
{{ .Ident "label" }},
{{ .Ident "provider" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "active" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
) VALUES (
{{ .Arg .Row.UID }},
{{ .Arg .Row.Namespace }},
{{ .Arg .Row.Label }},
{{ .Arg .Row.Provider }},
{{ .Arg .Row.EncryptedData }},
{{ .Arg .Row.Active }},
{{ .Arg .Row.Created }},
{{ .Arg .Row.Updated }}
);

View File

@ -0,0 +1,4 @@
DELETE FROM {{ .Ident "secret_data_key" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "uid" }} = {{ .Arg .UID }}
;

View File

@ -0,0 +1,8 @@
UPDATE
{{ .Ident "secret_data_key" }}
SET
{{ .Ident "active" }} = false,
{{ .Ident "updated" }} = {{ .Arg .Updated }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "active" }} = true
;

View File

@ -0,0 +1,13 @@
SELECT
{{ .Ident "uid" }},
{{ .Ident "namespace" }},
{{ .Ident "label" }},
{{ .Ident "provider" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "active" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
FROM
{{ .Ident "secret_data_key" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }}
;

View File

@ -0,0 +1,14 @@
SELECT
{{ .Ident "uid" }},
{{ .Ident "namespace" }},
{{ .Ident "label" }},
{{ .Ident "provider" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "active" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
FROM
{{ .Ident "secret_data_key" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "uid" }} = {{ .Arg .UID }}
;

View File

@ -0,0 +1,15 @@
SELECT
{{ .Ident "uid" }},
{{ .Ident "namespace" }},
{{ .Ident "label" }},
{{ .Ident "provider" }},
{{ .Ident "encrypted_data" }},
{{ .Ident "active" }},
{{ .Ident "created" }},
{{ .Ident "updated" }}
FROM
{{ .Ident "secret_data_key" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "label" }} = {{ .Arg .Label }} AND
{{ .Ident "active" }} = true
;

View File

@ -0,0 +1,19 @@
package encryption
import (
"time"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
)
// SecretDataKey does not have a mirrored K8s resource
type SecretDataKey struct {
UID string
Active bool
Namespace string
Label string
Provider encryption.ProviderID
EncryptedData []byte
Created time.Time
Updated time.Time
}

View File

@ -0,0 +1,332 @@
package encryption
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// encryptionStoreImpl is the actual implementation of the data key storage.
type encryptionStoreImpl struct {
db contracts.Database
dialect sqltemplate.Dialect
tracer trace.Tracer
log log.Logger
}
func ProvideDataKeyStorage(
db contracts.Database,
tracer trace.Tracer,
features featuremgmt.FeatureToggles,
registerer prometheus.Registerer,
) (contracts.DataKeyStorage, error) {
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) ||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) {
return &encryptionStoreImpl{}, nil
}
store := &encryptionStoreImpl{
db: db,
dialect: sqltemplate.DialectForDriver(db.DriverName()),
tracer: tracer,
log: log.New("encryption.store"),
}
return store, nil
}
func (ss *encryptionStoreImpl) GetDataKey(ctx context.Context, namespace, uid string) (*contracts.SecretDataKey, error) {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.GetDataKey", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("uid", uid),
))
defer span.End()
req := readDataKey{
SQLTemplate: sqltemplate.New(ss.dialect),
Namespace: namespace,
UID: uid,
}
query, err := sqltemplate.Execute(sqlDataKeyRead, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlDataKeyRead.Name(), err)
}
res, err := ss.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("getting data key row: %w", err)
}
defer func() { _ = res.Close() }()
if !res.Next() {
return nil, contracts.ErrDataKeyNotFound
}
var dataKey SecretDataKey
err = res.Scan(
&dataKey.UID,
&dataKey.Namespace,
&dataKey.Label,
&dataKey.Provider,
&dataKey.EncryptedData,
&dataKey.Active,
&dataKey.Created,
&dataKey.Updated,
)
if err != nil {
return nil, fmt.Errorf("failed to scan data key row: %w", err)
}
if err := res.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
return &contracts.SecretDataKey{
UID: dataKey.UID,
Namespace: dataKey.Namespace,
Label: dataKey.Label,
Provider: dataKey.Provider,
EncryptedData: dataKey.EncryptedData,
Active: dataKey.Active,
Created: dataKey.Created,
Updated: dataKey.Updated,
}, nil
}
func (ss *encryptionStoreImpl) GetCurrentDataKey(ctx context.Context, namespace, label string) (*contracts.SecretDataKey, error) {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.GetCurrentDataKey", trace.WithAttributes(
attribute.String("namespace", namespace),
attribute.String("label", label),
))
defer span.End()
req := readCurrentDataKey{
SQLTemplate: sqltemplate.New(ss.dialect),
Namespace: namespace,
Label: label,
}
query, err := sqltemplate.Execute(sqlDataKeyReadCurrent, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlDataKeyReadCurrent.Name(), err)
}
res, err := ss.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("getting current data key row: %w", err)
}
defer func() { _ = res.Close() }()
if !res.Next() {
return nil, contracts.ErrDataKeyNotFound
}
var dataKey SecretDataKey
err = res.Scan(
&dataKey.UID,
&dataKey.Namespace,
&dataKey.Label,
&dataKey.Provider,
&dataKey.EncryptedData,
&dataKey.Active,
&dataKey.Created,
&dataKey.Updated,
)
if err != nil {
return nil, fmt.Errorf("failed to scan data key row: %w", err)
}
if err := res.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
return &contracts.SecretDataKey{
UID: dataKey.UID,
Namespace: dataKey.Namespace,
Label: dataKey.Label,
Provider: dataKey.Provider,
EncryptedData: dataKey.EncryptedData,
Active: dataKey.Active,
Created: dataKey.Created,
Updated: dataKey.Updated,
}, nil
}
func (ss *encryptionStoreImpl) GetAllDataKeys(ctx context.Context, namespace string) ([]*contracts.SecretDataKey, error) {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.GetAllDataKeys", trace.WithAttributes(
attribute.String("namespace", namespace),
))
defer span.End()
req := listDataKeys{
SQLTemplate: sqltemplate.New(ss.dialect),
Namespace: namespace,
}
query, err := sqltemplate.Execute(sqlDataKeyList, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlDataKeyList.Name(), err)
}
rows, err := ss.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("listing data keys %q: %w", sqlDataKeyList.Name(), err)
}
defer func() { _ = rows.Close() }()
dataKeys := make([]*contracts.SecretDataKey, 0)
for rows.Next() {
var row SecretDataKey
err = rows.Scan(
&row.UID,
&row.Namespace,
&row.Label,
&row.Provider,
&row.EncryptedData,
&row.Active,
&row.Created,
&row.Updated,
)
if err != nil {
return nil, fmt.Errorf("error reading data key row: %w", err)
}
dataKeys = append(dataKeys, &contracts.SecretDataKey{
UID: row.UID,
Namespace: row.Namespace,
Label: row.Label,
Provider: row.Provider,
EncryptedData: row.EncryptedData,
Active: row.Active,
Created: row.Created,
Updated: row.Updated,
})
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
return dataKeys, nil
}
func (ss *encryptionStoreImpl) CreateDataKey(ctx context.Context, dataKey *contracts.SecretDataKey) error {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.CreateDataKey", trace.WithAttributes(
attribute.String("uid", dataKey.UID),
attribute.String("namespace", dataKey.Namespace),
attribute.Bool("active", dataKey.Active),
))
defer span.End()
if !dataKey.Active {
return fmt.Errorf("cannot insert deactivated data keys")
}
dataKey.Created = time.Now()
dataKey.Updated = dataKey.Created
req := createDataKey{
SQLTemplate: sqltemplate.New(ss.dialect),
Row: dataKey,
}
query, err := sqltemplate.Execute(sqlDataKeyCreate, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlDataKeyCreate.Name(), err)
}
result, err := ss.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("inserting data key row: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, but affected %d", rowsAffected)
}
return nil
}
func (ss *encryptionStoreImpl) DisableDataKeys(ctx context.Context, namespace string) error {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.DisableDataKeys", trace.WithAttributes(
attribute.String("namespace", namespace),
))
defer span.End()
req := disableDataKeys{
SQLTemplate: sqltemplate.New(ss.dialect),
Namespace: namespace,
Updated: time.Now(),
}
query, err := sqltemplate.Execute(sqlDataKeyDisable, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlDataKeyDisable.Name(), err)
}
result, err := ss.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("updating data key row: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, but affected %d", rowsAffected)
}
return nil
}
func (ss *encryptionStoreImpl) DeleteDataKey(ctx context.Context, namespace, uid string) error {
ctx, span := ss.tracer.Start(ctx, "DataKeyStorage.DeleteDataKey", trace.WithAttributes(
attribute.String("uid", uid),
attribute.String("namespace", namespace),
))
defer span.End()
if len(uid) == 0 {
return fmt.Errorf("data key id is missing")
}
req := deleteDataKey{
SQLTemplate: sqltemplate.New(ss.dialect),
Namespace: namespace,
UID: uid,
}
query, err := sqltemplate.Execute(sqlDataKeyDelete, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlDataKeyDelete.Name(), err)
}
result, err := ss.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("deleting data key is %s in namespace %s: %w", uid, namespace, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("bug: deleted more than one row from the data key table, should delete only one at a time: deleted=%v", rowsAffected)
}
return nil
}

View File

@ -0,0 +1,128 @@
package encryption
import (
"context"
"encoding/base64"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/grafana/grafana/pkg/tests/testsuite"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
const (
passThroughProvider encryption.ProviderID = "PASS_THROUGH_PROVIDER"
base64Provider encryption.ProviderID = "BASE64_PROVIDER"
)
func TestEncryptionStoreImpl_DataKeyLifecycle(t *testing.T) {
// Initialize data key storage with a fake db
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
store, err := ProvideDataKeyStorage(database.ProvideDatabase(testDB), tracer, features, nil)
require.NoError(t, err)
ctx := context.Background()
// Define a data key whose lifecycle we will test
dataKey := &contracts.SecretDataKey{
UID: "test-uid",
Namespace: "test-namespace",
Label: "test-label",
Active: true,
EncryptedData: []byte("test-data"),
Provider: passThroughProvider, // so that the Decrypt() method just gets us the input test data
}
// Define a second data key in a different namespace that should remain undisturbed
unchangingDataKey := &contracts.SecretDataKey{
UID: "static-uid",
Namespace: "static-namespace",
Label: "static-label",
Active: true,
EncryptedData: []byte("static-data"),
}
// Test CreateDataKey
err = store.CreateDataKey(ctx, dataKey)
require.NoError(t, err)
err = store.CreateDataKey(ctx, unchangingDataKey)
require.NoError(t, err)
// Test GetDataKey
retrievedKey, err := store.GetDataKey(ctx, "test-namespace", "test-uid")
require.NoError(t, err)
require.Equal(t, dataKey.UID, retrievedKey.UID)
require.Equal(t, dataKey.Namespace, retrievedKey.Namespace)
// Test GetCurrentDataKey
currentKey, err := store.GetCurrentDataKey(ctx, "test-namespace", "test-label")
require.NoError(t, err)
require.Equal(t, dataKey.UID, currentKey.UID)
require.Equal(t, dataKey.Namespace, currentKey.Namespace)
// Test GetAllDataKeys
allKeys, err := store.GetAllDataKeys(ctx, "test-namespace")
require.NoError(t, err)
require.Len(t, allKeys, 1)
require.Equal(t, dataKey.UID, allKeys[0].UID)
// Test DisableDataKeys
err = store.DisableDataKeys(ctx, "test-namespace")
require.NoError(t, err)
// Verify that the data key is disabled
disabledKey, err := store.GetDataKey(ctx, "test-namespace", "test-uid")
require.NoError(t, err)
require.False(t, disabledKey.Active)
// Test DeleteDataKey
err = store.DeleteDataKey(ctx, "test-namespace", "test-uid")
require.NoError(t, err)
// Verify that the data key is deleted
_, err = store.GetDataKey(ctx, "test-namespace", "test-uid")
require.Error(t, err)
require.Equal(t, contracts.ErrDataKeyNotFound, err)
// Verify that the unchanging data key still exists and is active
staticKey, err := store.GetDataKey(ctx, "static-namespace", "static-uid")
require.NoError(t, err)
require.Equal(t, unchangingDataKey.UID, staticKey.UID)
require.Equal(t, unchangingDataKey.Namespace, staticKey.Namespace)
require.True(t, staticKey.Active)
}
type PassThroughEncryptionProvider struct{}
func (d *PassThroughEncryptionProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) {
return blob, nil
}
func (d *PassThroughEncryptionProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) {
return blob, nil
}
type Base64EncryptionProvider struct{}
func (d *Base64EncryptionProvider) Encrypt(ctx context.Context, blob []byte) ([]byte, error) {
r := base64.RawStdEncoding.EncodeToString(blob)
return []byte(r), nil
}
func (d *Base64EncryptionProvider) Decrypt(ctx context.Context, blob []byte) ([]byte, error) {
r, err := base64.RawStdEncoding.DecodeString(string(blob))
return r, err
}

View File

@ -4,7 +4,9 @@ import (
"embed"
"fmt"
"text/template"
"time"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
@ -19,6 +21,13 @@ var (
sqlEncryptedValueRead = mustTemplate("encrypted_value_read.sql")
sqlEncryptedValueUpdate = mustTemplate("encrypted_value_update.sql")
sqlEncryptedValueDelete = mustTemplate("encrypted_value_delete.sql")
sqlDataKeyCreate = mustTemplate("data_key_create.sql")
sqlDataKeyRead = mustTemplate("data_key_read.sql")
sqlDataKeyReadCurrent = mustTemplate("data_key_read_current.sql")
sqlDataKeyList = mustTemplate("data_key_list.sql")
sqlDataKeyDisable = mustTemplate("data_key_disable.sql")
sqlDataKeyDelete = mustTemplate("data_key_delete.sql")
)
// TODO: Move this to a common place so that all stores can use
@ -79,3 +88,52 @@ type deleteEncryptedValue struct {
func (r deleteEncryptedValue) Validate() error {
return nil // TODO
}
/*************************************/
/**-- Data Key Queries --**/
/*************************************/
type createDataKey struct {
sqltemplate.SQLTemplate
Row *contracts.SecretDataKey
}
func (r createDataKey) Validate() error { return nil }
type readDataKey struct {
sqltemplate.SQLTemplate
Namespace string
UID string
}
func (r readDataKey) Validate() error { return nil }
type readCurrentDataKey struct {
sqltemplate.SQLTemplate
Namespace string
Label string
}
func (r readCurrentDataKey) Validate() error { return nil }
type listDataKeys struct {
sqltemplate.SQLTemplate
Namespace string
}
func (r listDataKeys) Validate() error { return nil }
type disableDataKeys struct {
sqltemplate.SQLTemplate
Namespace string
Updated time.Time
}
func (r disableDataKeys) Validate() error { return nil }
type deleteDataKey struct {
sqltemplate.SQLTemplate
Namespace string
UID string
}
func (r deleteDataKey) Validate() error { return nil }

View File

@ -3,7 +3,9 @@ package encryption
import (
"testing"
"text/template"
"time"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks"
)
@ -61,3 +63,98 @@ func TestEncryptedValueQueries(t *testing.T) {
},
})
}
func TestDataKeyQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlDataKeyCreate: {
{
Name: "create",
Data: &createDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &contracts.SecretDataKey{
UID: "abc123",
Active: true,
Namespace: "ns",
Label: "label",
Provider: "provider",
EncryptedData: []byte("secret"),
},
},
},
{
Name: "create-not-active",
Data: &createDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &contracts.SecretDataKey{
UID: "abc123",
Active: false,
Namespace: "ns",
Label: "label",
Provider: "provider",
EncryptedData: []byte("secret"),
},
},
},
},
sqlDataKeyRead: {
{
Name: "read",
Data: &readDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
UID: "abc123",
},
},
},
sqlDataKeyReadCurrent: {
{
Name: "read_current",
Data: &readCurrentDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
Label: "label",
},
},
},
sqlDataKeyList: {
{
Name: "list",
Data: &listDataKeys{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
},
},
},
sqlDataKeyDisable: {
{
Name: "disable",
Data: &disableDataKeys{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
Updated: time.Unix(1735689600, 0).UTC(),
},
},
},
sqlDataKeyDelete: {
{
Name: "delete",
Data: &deleteDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
UID: "abc123",
},
},
{
Name: "delete-no-uid",
Data: &deleteDataKey{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Namespace: "ns",
UID: "",
},
},
},
},
})
}

View File

@ -0,0 +1,19 @@
INSERT INTO `secret_data_key` (
`uid`,
`namespace`,
`label`,
`provider`,
`encrypted_data`,
`active`,
`created`,
`updated`
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
FALSE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,19 @@
INSERT INTO `secret_data_key` (
`uid`,
`namespace`,
`label`,
`provider`,
`encrypted_data`,
`active`,
`created`,
`updated`
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
TRUE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,4 @@
DELETE FROM `secret_data_key`
WHERE `namespace` = 'ns' AND
`uid` = ''
;

View File

@ -0,0 +1,4 @@
DELETE FROM `secret_data_key`
WHERE `namespace` = 'ns' AND
`uid` = 'abc123'
;

View File

@ -0,0 +1,8 @@
UPDATE
`secret_data_key`
SET
`active` = false,
`updated` = '2025-01-01 00:00:00 +0000 UTC'
WHERE `namespace` = 'ns' AND
`active` = true
;

View File

@ -0,0 +1,13 @@
SELECT
`uid`,
`namespace`,
`label`,
`provider`,
`encrypted_data`,
`active`,
`created`,
`updated`
FROM
`secret_data_key`
WHERE `namespace` = 'ns'
;

View File

@ -0,0 +1,14 @@
SELECT
`uid`,
`namespace`,
`label`,
`provider`,
`encrypted_data`,
`active`,
`created`,
`updated`
FROM
`secret_data_key`
WHERE `namespace` = 'ns' AND
`uid` = 'abc123'
;

View File

@ -0,0 +1,15 @@
SELECT
`uid`,
`namespace`,
`label`,
`provider`,
`encrypted_data`,
`active`,
`created`,
`updated`
FROM
`secret_data_key`
WHERE `namespace` = 'ns' AND
`label` = 'label' AND
`active` = true
;

View File

@ -0,0 +1,19 @@
INSERT INTO "secret_data_key" (
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
FALSE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,19 @@
INSERT INTO "secret_data_key" (
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
TRUE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = ''
;

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = 'abc123'
;

View File

@ -0,0 +1,8 @@
UPDATE
"secret_data_key"
SET
"active" = false,
"updated" = '2025-01-01 00:00:00 +0000 UTC'
WHERE "namespace" = 'ns' AND
"active" = true
;

View File

@ -0,0 +1,13 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns'
;

View File

@ -0,0 +1,14 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = 'abc123'
;

View File

@ -0,0 +1,15 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns' AND
"label" = 'label' AND
"active" = true
;

View File

@ -0,0 +1,19 @@
INSERT INTO "secret_data_key" (
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
FALSE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,19 @@
INSERT INTO "secret_data_key" (
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
) VALUES (
'abc123',
'ns',
'label',
'provider',
'[115 101 99 114 101 116]',
TRUE,
'0001-01-01 00:00:00 +0000 UTC',
'0001-01-01 00:00:00 +0000 UTC'
);

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = ''
;

View File

@ -0,0 +1,4 @@
DELETE FROM "secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = 'abc123'
;

View File

@ -0,0 +1,8 @@
UPDATE
"secret_data_key"
SET
"active" = false,
"updated" = '2025-01-01 00:00:00 +0000 UTC'
WHERE "namespace" = 'ns' AND
"active" = true
;

View File

@ -0,0 +1,13 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns'
;

View File

@ -0,0 +1,14 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns' AND
"uid" = 'abc123'
;

View File

@ -0,0 +1,15 @@
SELECT
"uid",
"namespace",
"label",
"provider",
"encrypted_data",
"active",
"created",
"updated"
FROM
"secret_data_key"
WHERE "namespace" = 'ns' AND
"label" = 'label' AND
"active" = true
;

View File

@ -15,6 +15,7 @@ const (
TableNameKeeper = "secret_keeper"
TableNameSecureValue = "secret_secure_value"
TableNameSecureValueOutbox = "secret_secure_value_outbox"
TableNameDataKey = "secret_data_key"
TableNameEncryptedValue = "secret_encrypted_value"
)
@ -45,31 +46,6 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) {
tables := []migrator.Table{}
tables = append(tables, migrator.Table{
Name: TableNameKeeper,
Columns: []*migrator.Column{
// Kubernetes Metadata
{Name: "guid", Type: migrator.DB_NVarchar, Length: 36, IsPrimaryKey: true}, // Fixed size of a UUID.
{Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "annotations", Type: migrator.DB_Text, Nullable: true},
{Name: "labels", Type: migrator.DB_Text, Nullable: true},
{Name: "created", Type: migrator.DB_BigInt, Nullable: false},
{Name: "created_by", Type: migrator.DB_Text, Nullable: false},
{Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, // Used as RV (ResourceVersion)
{Name: "updated_by", Type: migrator.DB_Text, Nullable: false},
// Spec
{Name: "description", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Chosen arbitrarily, but should be enough.
{Name: "type", Type: migrator.DB_Text, Nullable: false},
// Each keeper has a different payload so we store the whole thing as a blob.
{Name: "payload", Type: migrator.DB_Text, Nullable: true},
},
Indices: []*migrator.Index{
{Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex},
},
})
tables = append(tables, migrator.Table{
Name: TableNameSecureValue,
Columns: []*migrator.Column{
@ -100,6 +76,48 @@ func (*SecretDB) AddMigration(mg *migrator.Migrator) {
},
})
tables = append(tables, migrator.Table{
Name: TableNameKeeper,
Columns: []*migrator.Column{
// Kubernetes Metadata
{Name: "guid", Type: migrator.DB_NVarchar, Length: 36, IsPrimaryKey: true}, // Fixed size of a UUID.
{Name: "name", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "annotations", Type: migrator.DB_Text, Nullable: true},
{Name: "labels", Type: migrator.DB_Text, Nullable: true},
{Name: "created", Type: migrator.DB_BigInt, Nullable: false},
{Name: "created_by", Type: migrator.DB_Text, Nullable: false},
{Name: "updated", Type: migrator.DB_BigInt, Nullable: false}, // Used as RV (ResourceVersion)
{Name: "updated_by", Type: migrator.DB_Text, Nullable: false},
// Spec
{Name: "description", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Chosen arbitrarily, but should be enough.
{Name: "type", Type: migrator.DB_Text, Nullable: false},
// Each keeper has a different payload so we store the whole thing as a blob.
{Name: "payload", Type: migrator.DB_Text, Nullable: true},
},
Indices: []*migrator.Index{
{Cols: []string{"namespace", "name"}, Type: migrator.UniqueIndex},
},
})
// TODO -- document how the seemingly arbitrary column lengths were chosen
// The answer for now is that they come from the legacy secrets service, but it would be good to know that they will still work in the new service
tables = append(tables, migrator.Table{
Name: TableNameDataKey,
Columns: []*migrator.Column{
{Name: "uid", Type: migrator.DB_NVarchar, Length: 100, IsPrimaryKey: true},
{Name: "namespace", Type: migrator.DB_NVarchar, Length: 253, Nullable: false}, // Limit enforced by K8s.
{Name: "label", Type: migrator.DB_NVarchar, Length: 100, IsPrimaryKey: false},
{Name: "active", Type: migrator.DB_Bool, Nullable: false},
{Name: "provider", Type: migrator.DB_NVarchar, Length: 50, Nullable: false},
{Name: "encrypted_data", Type: migrator.DB_Blob, Nullable: false},
{Name: "created", Type: migrator.DB_DateTime, Nullable: false},
{Name: "updated", Type: migrator.DB_DateTime, Nullable: false},
},
Indices: []*migrator.Index{}, // TODO: add indexes based on the queries we make.
})
tables = append(tables, migrator.Table{
Name: TableNameEncryptedValue,
Columns: []*migrator.Column{