cf242def3a
* Secrets: Refactor data_key_id out of the encoded secure value payload (#111852) * everything compiles * tests pass * remove file included by accident * add entry to gitignore * some scaffolding for the migration executor * remove file * implement and test the migration * use xkube.Namespace in our interfaces * add todo * update wire deps * add some logs * fix wire dependency ordering * create tests to validate error conditions during migrations * only run the migration as an MT api server * formatting issues * change detection of secrets running as MT server * add todo * use more specific initializer flags * make secrets playwright tests work * set new properties to true by default * remove developer mode flag * fix unit tests
421 lines
13 KiB
Go
421 lines
13 KiB
Go
package encryption
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
|
|
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
|
|
)
|
|
|
|
var (
|
|
ErrEncryptedValueNotFound = errors.New("encrypted value not found")
|
|
ErrEncryptedValueAlreadyExists = errors.New("encrypted value alredy exists")
|
|
ErrUnexpectedNumberOfRowsAffected = errors.New("unexpected number of rows modified by query")
|
|
)
|
|
|
|
func ProvideEncryptedValueStorage(
|
|
db contracts.Database,
|
|
tracer trace.Tracer,
|
|
) (contracts.EncryptedValueStorage, error) {
|
|
return &encryptedValStorage{
|
|
db: db,
|
|
dialect: sqltemplate.DialectForDriver(db.DriverName()),
|
|
tracer: tracer,
|
|
}, nil
|
|
}
|
|
|
|
type encryptedValStorage struct {
|
|
db contracts.Database
|
|
dialect sqltemplate.Dialect
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
func (s *encryptedValStorage) Create(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) (ev *contracts.EncryptedValue, err error) {
|
|
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Create", trace.WithAttributes(
|
|
attribute.String("namespace", namespace.String()),
|
|
))
|
|
defer span.End()
|
|
|
|
defer func() {
|
|
if ev != nil {
|
|
span.SetAttributes(
|
|
attribute.String("namespace", ev.Namespace),
|
|
attribute.String("name", ev.Name),
|
|
attribute.Int64("version", ev.Version),
|
|
)
|
|
}
|
|
}()
|
|
|
|
createdTime := time.Now().Unix()
|
|
|
|
encryptedValue := &EncryptedValue{
|
|
Namespace: namespace.String(),
|
|
Name: name,
|
|
Version: version,
|
|
EncryptedData: encryptedData.EncryptedData,
|
|
DataKeyID: encryptedData.DataKeyID,
|
|
Created: createdTime,
|
|
Updated: createdTime,
|
|
}
|
|
|
|
req := createEncryptedValue{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
Row: encryptedValue,
|
|
}
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueCreate, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("executing template %q: %w", sqlEncryptedValueCreate.Name(), err)
|
|
}
|
|
|
|
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
|
|
if err != nil {
|
|
if sql.IsRowAlreadyExistsError(err) {
|
|
return nil, ErrEncryptedValueAlreadyExists
|
|
}
|
|
return nil, fmt.Errorf("inserting row: %w", err)
|
|
}
|
|
|
|
if rowsAffected, err := res.RowsAffected(); err != nil {
|
|
return nil, fmt.Errorf("getting rows affected: %w", err)
|
|
} else if rowsAffected != 1 {
|
|
return nil, fmt.Errorf("expected 1 row affected, got %d", rowsAffected)
|
|
}
|
|
|
|
return &contracts.EncryptedValue{
|
|
Namespace: encryptedValue.Namespace,
|
|
Name: encryptedValue.Name,
|
|
Version: encryptedValue.Version,
|
|
EncryptedPayload: contracts.EncryptedPayload{
|
|
DataKeyID: encryptedValue.DataKeyID,
|
|
EncryptedData: encryptedValue.EncryptedData,
|
|
},
|
|
Created: encryptedValue.Created,
|
|
Updated: encryptedValue.Updated,
|
|
}, nil
|
|
}
|
|
|
|
func (s *encryptedValStorage) Update(ctx context.Context, namespace xkube.Namespace, name string, version int64, encryptedData contracts.EncryptedPayload) error {
|
|
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Update", trace.WithAttributes(
|
|
attribute.String("namespace", namespace.String()),
|
|
attribute.String("name", name),
|
|
attribute.Int64("version", version),
|
|
))
|
|
defer span.End()
|
|
|
|
req := updateEncryptedValue{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
Namespace: namespace.String(),
|
|
Name: name,
|
|
Version: version,
|
|
EncryptedData: encryptedData.EncryptedData,
|
|
DataKeyID: encryptedData.DataKeyID,
|
|
Updated: time.Now().Unix(),
|
|
}
|
|
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueUpdate, req)
|
|
if err != nil {
|
|
return fmt.Errorf("executing template %q: %w", sqlEncryptedValueUpdate.Name(), err)
|
|
}
|
|
|
|
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
|
|
if err != nil {
|
|
return fmt.Errorf("updating row: %w", err)
|
|
}
|
|
|
|
if rowsAffected, err := res.RowsAffected(); err != nil {
|
|
return fmt.Errorf("getting rows affected: %w", err)
|
|
} else if rowsAffected != 1 {
|
|
return fmt.Errorf("expected 1 row affected, got %d on %s: %w", rowsAffected, namespace, ErrUnexpectedNumberOfRowsAffected)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *encryptedValStorage) Get(ctx context.Context, namespace xkube.Namespace, name string, version int64) (*contracts.EncryptedValue, error) {
|
|
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Get", trace.WithAttributes(
|
|
attribute.String("namespace", namespace.String()),
|
|
attribute.String("name", name),
|
|
attribute.Int64("version", version),
|
|
))
|
|
defer span.End()
|
|
|
|
req := &readEncryptedValue{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
Namespace: namespace.String(),
|
|
Name: name,
|
|
Version: version,
|
|
}
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueRead, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("executing template %q: %w", sqlEncryptedValueRead.Name(), err)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting row: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
if !rows.Next() {
|
|
return nil, ErrEncryptedValueNotFound
|
|
}
|
|
|
|
var encryptedValue EncryptedValue
|
|
err = rows.Scan(&encryptedValue.Namespace, &encryptedValue.Name, &encryptedValue.Version, &encryptedValue.EncryptedData, &encryptedValue.DataKeyID, &encryptedValue.Created, &encryptedValue.Updated)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan encrypted value row: %w", err)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("read rows error: %w", err)
|
|
}
|
|
|
|
return &contracts.EncryptedValue{
|
|
Namespace: encryptedValue.Namespace,
|
|
Name: encryptedValue.Name,
|
|
Version: encryptedValue.Version,
|
|
EncryptedPayload: contracts.EncryptedPayload{
|
|
DataKeyID: encryptedValue.DataKeyID,
|
|
EncryptedData: encryptedValue.EncryptedData,
|
|
},
|
|
Created: encryptedValue.Created,
|
|
Updated: encryptedValue.Updated,
|
|
}, nil
|
|
}
|
|
|
|
func (s *encryptedValStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string, version int64) error {
|
|
ctx, span := s.tracer.Start(ctx, "EncryptedValueStorage.Delete", trace.WithAttributes(
|
|
attribute.String("namespace", namespace.String()),
|
|
attribute.String("name", name),
|
|
attribute.Int64("version", version),
|
|
))
|
|
defer span.End()
|
|
|
|
req := deleteEncryptedValue{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
Namespace: namespace.String(),
|
|
Name: name,
|
|
Version: version,
|
|
}
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueDelete, req)
|
|
if err != nil {
|
|
return fmt.Errorf("executing template %q: %w", sqlEncryptedValueDelete.Name(), err)
|
|
}
|
|
|
|
if _, err = s.db.ExecContext(ctx, query, req.GetArgs()...); err != nil {
|
|
return fmt.Errorf("deleting row: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type globalEncryptedValStorage struct {
|
|
db contracts.Database
|
|
dialect sqltemplate.Dialect
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
func ProvideGlobalEncryptedValueStorage(
|
|
db contracts.Database,
|
|
tracer trace.Tracer,
|
|
) (contracts.GlobalEncryptedValueStorage, error) {
|
|
return &globalEncryptedValStorage{
|
|
db: db,
|
|
dialect: sqltemplate.DialectForDriver(db.DriverName()),
|
|
tracer: tracer,
|
|
}, nil
|
|
}
|
|
|
|
func (s *globalEncryptedValStorage) ListAll(ctx context.Context, opts contracts.ListOpts, untilTime *int64) ([]*contracts.EncryptedValue, error) {
|
|
attrs := []attribute.KeyValue{
|
|
attribute.Int64("limit", opts.Limit),
|
|
attribute.Int64("offset", opts.Offset),
|
|
}
|
|
if untilTime != nil {
|
|
attrs = append(attrs, attribute.Int64("untilTime", *untilTime))
|
|
}
|
|
ctx, span := s.tracer.Start(ctx, "GlobalEncryptedValueStorage.CountAll", trace.WithAttributes(attrs...))
|
|
defer span.End()
|
|
|
|
req := listAllEncryptedValues{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
Limit: opts.Limit,
|
|
Offset: opts.Offset,
|
|
}
|
|
if untilTime != nil {
|
|
req.HasUntilTime = true
|
|
req.UntilTime = *untilTime
|
|
}
|
|
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueListAll, req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute template %q: %w", sqlEncryptedValueListAll.Name(), err)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing encrypted values %q: %w", sqlEncryptedValueListAll.Name(), err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
encryptedValues := make([]*contracts.EncryptedValue, 0)
|
|
for rows.Next() {
|
|
var row EncryptedValue
|
|
err = rows.Scan(
|
|
&row.Namespace,
|
|
&row.Name,
|
|
&row.Version,
|
|
&row.EncryptedData,
|
|
&row.DataKeyID,
|
|
&row.Created,
|
|
&row.Updated,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error reading data key row: %w", err)
|
|
}
|
|
|
|
encryptedValues = append(encryptedValues, &contracts.EncryptedValue{
|
|
Namespace: row.Namespace,
|
|
Name: row.Name,
|
|
Version: row.Version,
|
|
EncryptedPayload: contracts.EncryptedPayload{
|
|
DataKeyID: row.DataKeyID,
|
|
EncryptedData: row.EncryptedData,
|
|
},
|
|
Created: row.Created,
|
|
Updated: row.Updated,
|
|
})
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("read rows error: %w", err)
|
|
}
|
|
|
|
return encryptedValues, nil
|
|
}
|
|
|
|
func (s *globalEncryptedValStorage) CountAll(ctx context.Context, untilTime *int64) (int64, error) {
|
|
attrs := []attribute.KeyValue{}
|
|
if untilTime != nil {
|
|
attrs = append(attrs, attribute.Int64("untilTime", *untilTime))
|
|
}
|
|
ctx, span := s.tracer.Start(ctx, "GlobalEncryptedValueStorage.CountAll", trace.WithAttributes(attrs...))
|
|
defer span.End()
|
|
|
|
req := countAllEncryptedValues{
|
|
SQLTemplate: sqltemplate.New(s.dialect),
|
|
}
|
|
if untilTime != nil {
|
|
req.HasUntilTime = true
|
|
req.UntilTime = *untilTime
|
|
}
|
|
|
|
query, err := sqltemplate.Execute(sqlEncryptedValueCountAll, req)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("execute template %q: %w", sqlEncryptedValueCountAll.Name(), err)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("getting row: %w", err)
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
if !rows.Next() {
|
|
return 0, fmt.Errorf("no rows returned when counting encrypted values")
|
|
}
|
|
|
|
var count int64
|
|
err = rows.Scan(&count)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to scan encrypted value row: %w", err)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return 0, fmt.Errorf("read rows error: %w", err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
type encryptedValMigrationExecutor struct {
|
|
db contracts.Database
|
|
dialect sqltemplate.Dialect
|
|
tracer trace.Tracer
|
|
encryptedValueStore contracts.EncryptedValueStorage
|
|
globalStore contracts.GlobalEncryptedValueStorage
|
|
}
|
|
|
|
func ProvideEncryptedValueMigrationExecutor(
|
|
db contracts.Database,
|
|
tracer trace.Tracer,
|
|
encryptedValueStore contracts.EncryptedValueStorage,
|
|
globalStore contracts.GlobalEncryptedValueStorage,
|
|
) (contracts.EncryptedValueMigrationExecutor, error) {
|
|
return &encryptedValMigrationExecutor{
|
|
db: db,
|
|
dialect: sqltemplate.DialectForDriver(db.DriverName()),
|
|
tracer: tracer,
|
|
encryptedValueStore: encryptedValueStore,
|
|
globalStore: globalStore,
|
|
}, nil
|
|
}
|
|
|
|
func (s *encryptedValMigrationExecutor) Execute(ctx context.Context) (int, error) {
|
|
ctx, span := s.tracer.Start(ctx, "EncryptedValueMigrationExecutor.Execute")
|
|
defer span.End()
|
|
|
|
// 1. Retrieve all encrypted values
|
|
encryptedValues, err := s.globalStore.ListAll(ctx, contracts.ListOpts{}, nil)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("listing all encrypted values: %w", err)
|
|
}
|
|
|
|
// This doesn't need to be done in a single transaction because there's no risk to successful rows if other rows fail
|
|
rowsAffected := 0
|
|
for _, encryptedValue := range encryptedValues {
|
|
// 2. If the value already has the data key id broken out, skip it
|
|
if encryptedValue.DataKeyID != "" {
|
|
continue
|
|
}
|
|
|
|
// 3. Split the data key id and the encrypted data out from the encoded payload
|
|
payload := encryptedValue.EncryptedData
|
|
const keyIdDelimiter = '#'
|
|
payload = payload[1:]
|
|
endOfKey := bytes.Index(payload, []byte{keyIdDelimiter})
|
|
if endOfKey == -1 {
|
|
return rowsAffected, fmt.Errorf("could not find valid key id in encrypted payload with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
|
|
}
|
|
b64Key := payload[:endOfKey]
|
|
encryptedData := payload[endOfKey+1:]
|
|
if len(encryptedData) == 0 {
|
|
return rowsAffected, fmt.Errorf("encrypted data is empty with namespace %s and name %s and version %d", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version)
|
|
}
|
|
keyId := make([]byte, base64.RawStdEncoding.DecodedLen(len(b64Key)))
|
|
_, err := base64.RawStdEncoding.Decode(keyId, b64Key)
|
|
if err != nil {
|
|
return rowsAffected, fmt.Errorf("decoding key id with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
|
|
}
|
|
|
|
// 4. Update the encrypted value with the data key id and the encrypted data
|
|
err = s.encryptedValueStore.Update(ctx, xkube.Namespace(encryptedValue.Namespace), encryptedValue.Name, encryptedValue.Version, contracts.EncryptedPayload{
|
|
DataKeyID: string(keyId),
|
|
EncryptedData: encryptedData,
|
|
})
|
|
if err != nil {
|
|
return rowsAffected, fmt.Errorf("updating encrypted value with namespace %s and name %s and version %d: %w", encryptedValue.Namespace, encryptedValue.Name, encryptedValue.Version, err)
|
|
}
|
|
rowsAffected++
|
|
}
|
|
|
|
return rowsAffected, nil
|
|
}
|