mirror of https://github.com/grafana/grafana
SecretsManager: Add encrypted value store (#106607)
* SecretsManager: add encrypted value store Co-authored-by: Dana Axinte <53751979+dana-axinte@users.noreply.github.com> Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com> * SecretsManager: wiring of encrypted value store --------- Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com> Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com> Co-authored-by: PoorlyDefinedBehaviour <brunotj2015@hotmail.com>pull/106613/head^2
parent
0879479c15
commit
c22b4845bb
@ -0,0 +1,13 @@ |
||||
INSERT INTO {{ .Ident "secret_encrypted_value" }} ( |
||||
{{ .Ident "uid" }}, |
||||
{{ .Ident "namespace" }}, |
||||
{{ .Ident "encrypted_data" }}, |
||||
{{ .Ident "created" }}, |
||||
{{ .Ident "updated" }} |
||||
) VALUES ( |
||||
{{ .Arg .Row.UID }}, |
||||
{{ .Arg .Row.Namespace }}, |
||||
{{ .Arg .Row.EncryptedData }}, |
||||
{{ .Arg .Row.Created }}, |
||||
{{ .Arg .Row.Updated }} |
||||
); |
@ -0,0 +1,4 @@ |
||||
DELETE FROM {{ .Ident "secret_encrypted_value" }} |
||||
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND |
||||
{{ .Ident "uid" }} = {{ .Arg .UID }} |
||||
; |
@ -0,0 +1,11 @@ |
||||
SELECT |
||||
{{ .Ident "uid" }}, |
||||
{{ .Ident "namespace" }}, |
||||
{{ .Ident "encrypted_data" }}, |
||||
{{ .Ident "created" }}, |
||||
{{ .Ident "updated" }} |
||||
FROM |
||||
{{ .Ident "secret_encrypted_value" }} |
||||
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND |
||||
{{ .Ident "uid" }} = {{ .Arg .UID }} |
||||
; |
@ -0,0 +1,8 @@ |
||||
UPDATE |
||||
{{ .Ident "secret_encrypted_value" }} |
||||
SET |
||||
{{ .Ident "encrypted_data" }} = {{ .Arg .EncryptedData }}, |
||||
{{ .Ident "updated" }} = {{ .Arg .Updated }} |
||||
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND |
||||
{{ .Ident "uid" }} = {{ .Arg .UID }} |
||||
; |
@ -0,0 +1,15 @@ |
||||
package encryption |
||||
|
||||
import "github.com/grafana/grafana/pkg/storage/secret/migrator" |
||||
|
||||
type EncryptedValue struct { |
||||
UID string |
||||
Namespace string |
||||
EncryptedData []byte |
||||
Created int64 |
||||
Updated int64 |
||||
} |
||||
|
||||
func (*EncryptedValue) TableName() string { |
||||
return migrator.TableNameEncryptedValue |
||||
} |
@ -0,0 +1,159 @@ |
||||
package encryption |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/google/uuid" |
||||
|
||||
"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" |
||||
) |
||||
|
||||
var ( |
||||
ErrEncryptedValueNotFound = errors.New("encrypted value not found") |
||||
) |
||||
|
||||
func ProvideEncryptedValueStorage(db contracts.Database, features featuremgmt.FeatureToggles) (contracts.EncryptedValueStorage, error) { |
||||
if !features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) || |
||||
!features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform) { |
||||
return &encryptedValStorage{}, nil |
||||
} |
||||
|
||||
return &encryptedValStorage{ |
||||
db: db, |
||||
dialect: sqltemplate.DialectForDriver(db.DriverName()), |
||||
}, nil |
||||
} |
||||
|
||||
type encryptedValStorage struct { |
||||
db contracts.Database |
||||
dialect sqltemplate.Dialect |
||||
} |
||||
|
||||
func (s *encryptedValStorage) Create(ctx context.Context, namespace string, encryptedData []byte) (*contracts.EncryptedValue, error) { |
||||
createdTime := time.Now().Unix() |
||||
encryptedValue := &EncryptedValue{ |
||||
UID: uuid.New().String(), |
||||
Namespace: namespace, |
||||
EncryptedData: encryptedData, |
||||
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 { |
||||
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{ |
||||
UID: encryptedValue.UID, |
||||
Namespace: encryptedValue.Namespace, |
||||
EncryptedData: encryptedValue.EncryptedData, |
||||
Created: encryptedValue.Created, |
||||
Updated: encryptedValue.Updated, |
||||
}, nil |
||||
} |
||||
|
||||
func (s *encryptedValStorage) Update(ctx context.Context, namespace string, uid string, encryptedData []byte) error { |
||||
req := updateEncryptedValue{ |
||||
SQLTemplate: sqltemplate.New(s.dialect), |
||||
Namespace: namespace, |
||||
UID: uid, |
||||
EncryptedData: encryptedData, |
||||
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", rowsAffected, namespace) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func (s *encryptedValStorage) Get(ctx context.Context, namespace string, uid string) (*contracts.EncryptedValue, error) { |
||||
req := &readEncryptedValue{ |
||||
SQLTemplate: sqltemplate.New(s.dialect), |
||||
Namespace: namespace, |
||||
UID: uid, |
||||
} |
||||
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.UID, &encryptedValue.Namespace, &encryptedValue.EncryptedData, &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{ |
||||
UID: encryptedValue.UID, |
||||
Namespace: encryptedValue.Namespace, |
||||
EncryptedData: encryptedValue.EncryptedData, |
||||
Created: encryptedValue.Created, |
||||
Updated: encryptedValue.Updated, |
||||
}, nil |
||||
} |
||||
|
||||
func (s *encryptedValStorage) Delete(ctx context.Context, namespace string, uid string) error { |
||||
req := deleteEncryptedValue{ |
||||
SQLTemplate: sqltemplate.New(s.dialect), |
||||
Namespace: namespace, |
||||
UID: uid, |
||||
} |
||||
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 |
||||
} |
@ -0,0 +1,105 @@ |
||||
package encryption |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"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/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestEncryptedValueStoreImpl(t *testing.T) { |
||||
// Initialize data key storage with a fake db
|
||||
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New())) |
||||
database := database.ProvideDatabase(testDB) |
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) |
||||
ctx := context.Background() |
||||
|
||||
store, err := ProvideEncryptedValueStorage(database, features) |
||||
require.NoError(t, err) |
||||
|
||||
t.Run("creating an encrypted value returns it", func(t *testing.T) { |
||||
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) |
||||
require.NoError(t, err) |
||||
require.NotEmpty(t, createdEV.UID) |
||||
require.NotEmpty(t, createdEV.Created) |
||||
require.NotEmpty(t, createdEV.Updated) |
||||
require.NotEmpty(t, createdEV.EncryptedData) |
||||
require.Equal(t, "test-namespace", createdEV.Namespace) |
||||
}) |
||||
|
||||
t.Run("get an existent encrypted value returns it", func(t *testing.T) { |
||||
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) |
||||
require.NoError(t, err) |
||||
|
||||
obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, createdEV.UID, obtainedEV.UID) |
||||
require.Equal(t, createdEV.Created, obtainedEV.Created) |
||||
require.Equal(t, createdEV.Updated, obtainedEV.Updated) |
||||
require.Equal(t, createdEV.EncryptedData, obtainedEV.EncryptedData) |
||||
require.Equal(t, createdEV.Namespace, obtainedEV.Namespace) |
||||
}) |
||||
|
||||
t.Run("get an existent encrypted value with a different namespace returns error", func(t *testing.T) { |
||||
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) |
||||
require.NoError(t, err) |
||||
|
||||
obtainedEV, err := store.Get(ctx, "other-test-namespace", createdEV.UID) |
||||
|
||||
require.Error(t, err) |
||||
require.Equal(t, "encrypted value not found", err.Error()) |
||||
require.Nil(t, obtainedEV) |
||||
}) |
||||
|
||||
t.Run("get a non existent encrypted value returns error", func(t *testing.T) { |
||||
obtainedEV, err := store.Get(ctx, "test-namespace", "test-uid") |
||||
require.Error(t, err) |
||||
require.Equal(t, "encrypted value not found", err.Error()) |
||||
require.Nil(t, obtainedEV) |
||||
}) |
||||
|
||||
t.Run("updating an existing encrypted value returns no error", func(t *testing.T) { |
||||
createdEV, err := store.Create(ctx, "test-namespace", []byte("test-data")) |
||||
require.NoError(t, err) |
||||
|
||||
err = store.Update(ctx, "test-namespace", createdEV.UID, []byte("test-data-updated")) |
||||
require.NoError(t, err) |
||||
|
||||
updatedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) |
||||
require.NoError(t, err) |
||||
|
||||
require.Equal(t, []byte("test-data-updated"), updatedEV.EncryptedData) |
||||
require.Equal(t, createdEV.Created, updatedEV.Created) |
||||
require.Equal(t, createdEV.Namespace, updatedEV.Namespace) |
||||
}) |
||||
|
||||
t.Run("updating a non existing encrypted value returns error", func(t *testing.T) { |
||||
err := store.Update(ctx, "test-namespace", "test-uid", []byte("test-data")) |
||||
require.Error(t, err) |
||||
}) |
||||
|
||||
t.Run("delete an existing encrypted value returns error", func(t *testing.T) { |
||||
createdEV, err := store.Create(ctx, "test-namespace", []byte("ttttest-data")) |
||||
require.NoError(t, err) |
||||
|
||||
obtainedEV, err := store.Get(ctx, "test-namespace", createdEV.UID) |
||||
require.NoError(t, err) |
||||
|
||||
err = store.Delete(ctx, "test-namespace", obtainedEV.UID) |
||||
require.NoError(t, err) |
||||
|
||||
obtainedEV, err = store.Get(ctx, "test-namespace", createdEV.UID) |
||||
require.Error(t, err) |
||||
require.Nil(t, obtainedEV) |
||||
}) |
||||
|
||||
t.Run("delete a non existing encrypted value does not return error", func(t *testing.T) { |
||||
err := store.Delete(ctx, "test-namespace", "test-uid") |
||||
require.NoError(t, err) |
||||
}) |
||||
} |
@ -0,0 +1,81 @@ |
||||
package encryption |
||||
|
||||
import ( |
||||
"embed" |
||||
"fmt" |
||||
"text/template" |
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate" |
||||
) |
||||
|
||||
var ( |
||||
//go:embed data/*.sql
|
||||
sqlTemplatesFS embed.FS |
||||
|
||||
sqlTemplates = template.Must(template.New("sql").ParseFS(sqlTemplatesFS, `data/*.sql`)) |
||||
|
||||
// The SQL Commands
|
||||
sqlEncryptedValueCreate = mustTemplate("encrypted_value_create.sql") |
||||
sqlEncryptedValueRead = mustTemplate("encrypted_value_read.sql") |
||||
sqlEncryptedValueUpdate = mustTemplate("encrypted_value_update.sql") |
||||
sqlEncryptedValueDelete = mustTemplate("encrypted_value_delete.sql") |
||||
) |
||||
|
||||
// TODO: Move this to a common place so that all stores can use
|
||||
func mustTemplate(filename string) *template.Template { |
||||
if t := sqlTemplates.Lookup(filename); t != nil { |
||||
return t |
||||
} |
||||
panic(fmt.Sprintf("template file not found: %s", filename)) |
||||
} |
||||
|
||||
/*************************************/ |
||||
/**-- Encrypted Value Queries --**/ |
||||
/*************************************/ |
||||
type createEncryptedValue struct { |
||||
sqltemplate.SQLTemplate |
||||
Row *EncryptedValue |
||||
} |
||||
|
||||
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||
func (r createEncryptedValue) Validate() error { |
||||
return nil // TODO
|
||||
} |
||||
|
||||
// Read Encrypted Value
|
||||
type readEncryptedValue struct { |
||||
sqltemplate.SQLTemplate |
||||
Namespace string |
||||
UID string |
||||
} |
||||
|
||||
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||
func (r readEncryptedValue) Validate() error { |
||||
return nil // TODO
|
||||
} |
||||
|
||||
// Update Encrypted Value
|
||||
type updateEncryptedValue struct { |
||||
sqltemplate.SQLTemplate |
||||
Namespace string |
||||
UID string |
||||
EncryptedData []byte |
||||
Updated int64 |
||||
} |
||||
|
||||
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||
func (r updateEncryptedValue) Validate() error { |
||||
return nil // TODO
|
||||
} |
||||
|
||||
// Delete Encrypted Value
|
||||
type deleteEncryptedValue struct { |
||||
sqltemplate.SQLTemplate |
||||
Namespace string |
||||
UID string |
||||
} |
||||
|
||||
// Validate is only used if we use `dbutil` from `unifiedstorage`
|
||||
func (r deleteEncryptedValue) Validate() error { |
||||
return nil // TODO
|
||||
} |
@ -0,0 +1,63 @@ |
||||
package encryption |
||||
|
||||
import ( |
||||
"testing" |
||||
"text/template" |
||||
|
||||
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate/mocks" |
||||
) |
||||
|
||||
func TestEncryptedValueQueries(t *testing.T) { |
||||
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ |
||||
RootDir: "testdata", |
||||
Templates: map[*template.Template][]mocks.TemplateTestCase{ |
||||
sqlEncryptedValueCreate: { |
||||
{ |
||||
Name: "create", |
||||
Data: &createEncryptedValue{ |
||||
SQLTemplate: mocks.NewTestingSQLTemplate(), |
||||
Row: &EncryptedValue{ |
||||
Namespace: "ns", |
||||
UID: "abc123", |
||||
EncryptedData: []byte("secret"), |
||||
Created: 1234, |
||||
Updated: 5678, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
sqlEncryptedValueRead: { |
||||
{ |
||||
Name: "read", |
||||
Data: &readEncryptedValue{ |
||||
SQLTemplate: mocks.NewTestingSQLTemplate(), |
||||
Namespace: "ns", |
||||
UID: "abc123", |
||||
}, |
||||
}, |
||||
}, |
||||
sqlEncryptedValueUpdate: { |
||||
{ |
||||
Name: "update", |
||||
Data: &updateEncryptedValue{ |
||||
SQLTemplate: mocks.NewTestingSQLTemplate(), |
||||
Namespace: "ns", |
||||
UID: "abc123", |
||||
EncryptedData: []byte("secret"), |
||||
Updated: 5679, |
||||
}, |
||||
}, |
||||
}, |
||||
sqlEncryptedValueDelete: { |
||||
{ |
||||
Name: "delete", |
||||
Data: &deleteEncryptedValue{ |
||||
SQLTemplate: mocks.NewTestingSQLTemplate(), |
||||
Namespace: "ns", |
||||
UID: "abc123", |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
} |
@ -0,0 +1,13 @@ |
||||
INSERT INTO `secret_encrypted_value` ( |
||||
`uid`, |
||||
`namespace`, |
||||
`encrypted_data`, |
||||
`created`, |
||||
`updated` |
||||
) VALUES ( |
||||
'abc123', |
||||
'ns', |
||||
'[115 101 99 114 101 116]', |
||||
1234, |
||||
5678 |
||||
); |
@ -0,0 +1,4 @@ |
||||
DELETE FROM `secret_encrypted_value` |
||||
WHERE `namespace` = 'ns' AND |
||||
`uid` = 'abc123' |
||||
; |
@ -0,0 +1,11 @@ |
||||
SELECT |
||||
`uid`, |
||||
`namespace`, |
||||
`encrypted_data`, |
||||
`created`, |
||||
`updated` |
||||
FROM |
||||
`secret_encrypted_value` |
||||
WHERE `namespace` = 'ns' AND |
||||
`uid` = 'abc123' |
||||
; |
@ -0,0 +1,8 @@ |
||||
UPDATE |
||||
`secret_encrypted_value` |
||||
SET |
||||
`encrypted_data` = '[115 101 99 114 101 116]', |
||||
`updated` = 5679 |
||||
WHERE `namespace` = 'ns' AND |
||||
`uid` = 'abc123' |
||||
; |
@ -0,0 +1,13 @@ |
||||
INSERT INTO "secret_encrypted_value" ( |
||||
"uid", |
||||
"namespace", |
||||
"encrypted_data", |
||||
"created", |
||||
"updated" |
||||
) VALUES ( |
||||
'abc123', |
||||
'ns', |
||||
'[115 101 99 114 101 116]', |
||||
1234, |
||||
5678 |
||||
); |
@ -0,0 +1,4 @@ |
||||
DELETE FROM "secret_encrypted_value" |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
@ -0,0 +1,11 @@ |
||||
SELECT |
||||
"uid", |
||||
"namespace", |
||||
"encrypted_data", |
||||
"created", |
||||
"updated" |
||||
FROM |
||||
"secret_encrypted_value" |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
@ -0,0 +1,8 @@ |
||||
UPDATE |
||||
"secret_encrypted_value" |
||||
SET |
||||
"encrypted_data" = '[115 101 99 114 101 116]', |
||||
"updated" = 5679 |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
@ -0,0 +1,13 @@ |
||||
INSERT INTO "secret_encrypted_value" ( |
||||
"uid", |
||||
"namespace", |
||||
"encrypted_data", |
||||
"created", |
||||
"updated" |
||||
) VALUES ( |
||||
'abc123', |
||||
'ns', |
||||
'[115 101 99 114 101 116]', |
||||
1234, |
||||
5678 |
||||
); |
@ -0,0 +1,4 @@ |
||||
DELETE FROM "secret_encrypted_value" |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
@ -0,0 +1,11 @@ |
||||
SELECT |
||||
"uid", |
||||
"namespace", |
||||
"encrypted_data", |
||||
"created", |
||||
"updated" |
||||
FROM |
||||
"secret_encrypted_value" |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
@ -0,0 +1,8 @@ |
||||
UPDATE |
||||
"secret_encrypted_value" |
||||
SET |
||||
"encrypted_data" = '[115 101 99 114 101 116]', |
||||
"updated" = 5679 |
||||
WHERE "namespace" = 'ns' AND |
||||
"uid" = 'abc123' |
||||
; |
Loading…
Reference in new issue