2026-04-06 15:48:37 +05:30
// Copyright IBM Corp. 2020, 2026
2022-11-16 17:17:28 -06:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2021-03-03 11:23:00 -05:00
package kms
2020-08-19 16:47:18 -04:00
import (
"context"
"testing"
2022-11-16 17:17:28 -06:00
"time"
2020-08-19 16:47:18 -04:00
2026-10-12 10:33:41 -07:00
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/db"
"repo.anhonesthost.net/CyberCoveLLC/threshold/internal/errors"
2022-05-13 13:53:22 -04:00
wrappingKms "github.com/hashicorp/go-kms-wrapping/extras/kms/v2"
"github.com/stretchr/testify/assert"
2020-08-19 16:47:18 -04:00
"github.com/stretchr/testify/require"
)
2022-05-13 13:53:22 -04:00
func Test_New ( t * testing . T ) {
2020-08-19 16:47:18 -04:00
t . Parallel ()
2022-05-13 13:53:22 -04:00
testCtx := context . Background ()
2020-08-19 16:47:18 -04:00
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
2022-05-13 13:53:22 -04:00
tests := [] struct {
name string
r * db . Db
w * db . Db
want * Kms
wantErr bool
wantErrMatch * errors . Template
wantErrContains string
}{
{
name : "nil-reader" ,
w : rw ,
wantErr : true ,
wantErrContains : "missing reader" ,
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "nil-writer" ,
r : rw ,
wantErr : true ,
wantErrContains : "missing writer" ,
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "success" ,
r : rw ,
w : rw ,
want : & Kms {
reader : rw ,
2022-11-16 17:17:28 -06:00
writer : rw ,
2022-05-13 13:53:22 -04:00
underlying : func () * wrappingKms . Kms {
purposes := make ([] wrappingKms . KeyPurpose , 0 , len ( ValidDekPurposes ()))
for _ , p := range ValidDekPurposes () {
2023-09-06 18:25:49 -04:00
switch p {
case KeyPurposeOplog :
continue
default :
purposes = append ( purposes , wrappingKms . KeyPurpose ( p . String ()))
}
2022-05-13 13:53:22 -04:00
}
2023-03-11 11:00:06 -05:00
purposes = append ( purposes ,
wrappingKms . KeyPurpose ( KeyPurposeWorkerAuth . String ()),
wrappingKms . KeyPurpose ( KeyPurposeWorkerAuthStorage . String ()),
wrappingKms . KeyPurpose ( KeyPurposeRecovery . String ()),
wrappingKms . KeyPurpose ( KeyPurposeBsr . String ()),
)
2021-03-03 11:23:00 -05:00
2022-09-06 17:10:27 +01:00
wrapped , err := wrappingKms . New ( db . NewChangeSafeDbwReader ( rw ), db . NewChangeSafeDbwWriter ( rw ), purposes )
2022-05-13 13:53:22 -04:00
require . NoError ( t , err )
return wrapped
}(),
2023-09-06 18:25:49 -04:00
underlyingForOplog : func () * wrappingKms . Kms {
wrapped , err := wrappingKms . New (
db . NewChangeSafeDbwReader ( rw ),
db . NewChangeSafeDbwWriter ( rw ),
[] wrappingKms . KeyPurpose { wrappingKms . KeyPurpose ( KeyPurposeOplog . String ())},
wrappingKms . WithTableNamePrefix ( "kms_oplog" ),
)
require . NoError ( t , err )
return wrapped
}(),
2022-05-13 13:53:22 -04:00
},
},
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
2021-03-03 11:23:00 -05:00
2022-05-13 13:53:22 -04:00
got , err := New ( testCtx , tc . r , tc . w )
if tc . wantErr {
require . Error ( err )
if tc . wantErrMatch != nil {
assert . Truef ( errors . Match ( tc . wantErrMatch , err ), "expected %q and got err: %+v" , tc . wantErrMatch . Code , err )
}
if tc . wantErrContains != "" {
assert . Contains ( err . Error (), tc . wantErrContains )
}
return
}
require . NoError ( err )
assert . Equal ( tc . want , got )
})
}
}
2021-03-03 11:23:00 -05:00
2022-05-13 13:53:22 -04:00
func Test_NewUsingReaderWriter ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
2021-03-03 11:23:00 -05:00
2022-05-13 13:53:22 -04:00
tests := [] struct {
name string
r db . Reader
w db . Writer
want * Kms
wantErr bool
wantErrMatch * errors . Template
wantErrContains string
}{
{
name : "nil-reader" ,
w : rw ,
wantErr : true ,
wantErrContains : "missing reader" ,
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "nil-writer" ,
r : rw ,
wantErr : true ,
wantErrContains : "missing writer" ,
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "invalid-reader" ,
r : & invalidReader {},
w : rw ,
wantErr : true ,
2022-09-06 17:10:27 +01:00
wantErrContains : "unable to convert reader to db.Db" ,
2022-05-13 13:53:22 -04:00
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "invalid-writer" ,
r : rw ,
w : & invalidWriter {},
wantErr : true ,
2022-09-06 17:10:27 +01:00
wantErrContains : "unable to convert writer to db.Db" ,
2022-05-13 13:53:22 -04:00
wantErrMatch : errors . T ( errors . InvalidParameter ),
},
{
name : "success" ,
r : rw ,
w : rw ,
want : & Kms {
reader : rw ,
underlying : func () * wrappingKms . Kms {
purposes := stdNewKmsPurposes ()
2022-09-06 17:10:27 +01:00
wrapped , err := wrappingKms . New ( db . NewChangeSafeDbwReader ( rw ), db . NewChangeSafeDbwWriter ( rw ), purposes )
2022-05-13 13:53:22 -04:00
require . NoError ( t , err )
return wrapped
}(),
2023-09-06 18:25:49 -04:00
underlyingForOplog : func () * wrappingKms . Kms {
wrapped , err := wrappingKms . New (
db . NewChangeSafeDbwReader ( rw ),
db . NewChangeSafeDbwWriter ( rw ),
[] wrappingKms . KeyPurpose { wrappingKms . KeyPurpose ( KeyPurposeOplog . String ())},
wrappingKms . WithTableNamePrefix ( "kms_oplog" ),
)
require . NoError ( t , err )
return wrapped
}(),
2022-05-13 13:53:22 -04:00
},
},
}
for _ , tc := range tests {
t . Run ( tc . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
2021-03-03 11:23:00 -05:00
2022-05-13 13:53:22 -04:00
got , err := NewUsingReaderWriter ( testCtx , tc . r , tc . w )
if tc . wantErr {
require . Error ( err )
if tc . wantErrMatch != nil {
assert . Truef ( errors . Match ( tc . wantErrMatch , err ), "expected %q and got err: %+v" , tc . wantErrMatch . Code , err )
}
if tc . wantErrContains != "" {
assert . Contains ( err . Error (), tc . wantErrContains )
}
return
}
require . NoError ( err )
assert . Equal ( tc . want , got )
})
}
}
2021-03-03 11:23:00 -05:00
2022-11-16 17:17:28 -06:00
func Test_ListKeys ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
t . Run ( "success" , func ( t * testing . T ) {
t . Parallel ()
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
require . Len ( t , keys , len ( ValidDekPurposes ()) + 1 )
})
t . Run ( "unknown-scope" , func ( t * testing . T ) {
t . Parallel ()
_ , err := kmsCache . ListKeys ( testCtx , "myscope" )
assert . True ( t , errors . IsNotFoundError ( err ))
})
t . Run ( "empty-scope" , func ( t * testing . T ) {
t . Parallel ()
_ , err := kmsCache . ListKeys ( testCtx , "" )
assert . Error ( t , err )
})
}
func Test_RotateKeys ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
rw := db . New ( conn )
t . Run ( "success" , func ( t * testing . T ) {
// arrange
// we're not trying to test the ListKeys function, although we need to use it to validate the rotation
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
require . Len ( t , keys , len ( ValidDekPurposes ()) + 1 )
// act
err = kmsCache . RotateKeys ( testCtx , "global" )
require . NoError ( t , err )
// assert
keys , err = kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
keyCount := 0
for _ , key := range keys {
keyCount += len ( key . Versions )
}
2022-12-16 12:16:17 -05:00
require . Equal ( t , 14 , keyCount )
2022-11-16 17:17:28 -06:00
})
t . Run ( "reader provided, missing writer" , func ( t * testing . T ) {
// arrange
WithReader := func ( reader db . Reader ) Option {
return func ( o * options ) {
o . withReader = reader
}
}
// act
err := kmsCache . RotateKeys ( testCtx , "global" , WithReader ( rw ))
// assert
assert . ErrorContains ( t , err , "missing writer" )
})
t . Run ( "writer provided, missing reader" , func ( t * testing . T ) {
// arrange
WithWriter := func ( writer db . Writer ) Option {
return func ( o * options ) {
o . withWriter = writer
}
}
// act
err := kmsCache . RotateKeys ( testCtx , "global" , WithWriter ( rw ))
// assert
assert . ErrorContains ( t , err , "missing reader" )
})
t . Run ( "invalid reader" , func ( t * testing . T ) {
// act
err := kmsCache . RotateKeys ( testCtx , "global" , WithReaderWriter ( & invalidReader {}, rw ))
// assert
assert . ErrorContains ( t , err , "unable to convert reader" )
})
t . Run ( "invalid writer" , func ( t * testing . T ) {
// act
err := kmsCache . RotateKeys ( testCtx , "global" , WithReaderWriter ( rw , & invalidWriter {}))
// assert
assert . ErrorContains ( t , err , "unable to convert writer" )
})
t . Run ( "both reader and writer succeed" , func ( t * testing . T ) {
2022-12-16 12:16:17 -05:00
err := kmsCache . RotateKeys ( testCtx , "global" , WithReaderWriter ( rw , rw ))
2022-11-16 17:17:28 -06:00
require . NoError ( t , err )
})
// other options are passed directly and shouldn't need to be tested
}
func Test_ListDataKeyVersionReferencers ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
t . Run ( "returns-something" , func ( t * testing . T ) {
referencers , err := kmsCache . ListDataKeyVersionReferencers ( testCtx )
require . NoError ( t , err )
// Don't want to check the length or explicit tables
// as this will make the test fail whenever we add or
// remove a reference. Lets just sanity check that it's
// got at least one element in it.
assert . NotEmpty ( t , referencers )
})
t . Run ( "errors when just a writer is specified" , func ( t * testing . T ) {
withWriter := func ( writer db . Writer ) Option {
return func ( o * options ) {
o . withWriter = writer
}
}
_ , err := kmsCache . ListDataKeyVersionReferencers ( testCtx , withWriter ( rw ))
require . Error ( t , err )
})
t . Run ( "errors when just a reader is specified" , func ( t * testing . T ) {
withReader := func ( reader db . Reader ) Option {
return func ( o * options ) {
o . withReader = reader
}
}
_ , err := kmsCache . ListDataKeyVersionReferencers ( testCtx , withReader ( rw ))
require . Error ( t , err )
})
t . Run ( "errors when an invalid writer is specified" , func ( t * testing . T ) {
_ , err := kmsCache . ListDataKeyVersionReferencers ( testCtx , WithReaderWriter ( rw , & invalidWriter {}))
require . Error ( t , err )
})
t . Run ( "errors when an invalid reader is specified" , func ( t * testing . T ) {
_ , err := kmsCache . ListDataKeyVersionReferencers ( testCtx , WithReaderWriter ( & invalidReader {}, rw ))
require . Error ( t , err )
})
t . Run ( "succeeds-with-reader-writer" , func ( t * testing . T ) {
referencers , err := kmsCache . ListDataKeyVersionReferencers ( testCtx , WithReaderWriter ( rw , rw ))
require . NoError ( t , err )
// Don't want to check the length or explicit tables
// as this will make the test fail whenever we add or
// remove a reference. Lets just sanity check that it's
// got at least one element in it.
assert . NotEmpty ( t , referencers )
})
}
func Test_ListDataKeyVersionDestructionJobs ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
err = kmsCache . RotateKeys ( testCtx , "global" )
require . NoError ( t , err )
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
t . Run ( "errors-on-empty-scope-ids" , func ( t * testing . T ) {
_ , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "" )
assert . Error ( t , err )
})
t . Run ( "lists-no-jobs-when-the-scope-id-is-invalid" , func ( t * testing . T ) {
jobs , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "notascope" )
require . NoError ( t , err )
assert . Empty ( t , jobs )
})
t . Run ( "lists-no-jobs-when-there-are-none" , func ( t * testing . T ) {
jobs , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "global" )
require . NoError ( t , err )
assert . Empty ( t , jobs )
})
t . Run ( "lists-jobs-when-there-are-some" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
sqldb , err := conn . SqlDB ( testCtx )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 200)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
jobs , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "global" )
require . NoError ( t , err )
require . Len ( t , jobs , 1 )
job := jobs [ 0 ]
assert . Equal ( t , 0 , int ( job . CompletedCount ))
assert . Equal ( t , 300 , int ( job . TotalCount ))
assert . Equal ( t , "running" , job . Status )
assert . Equal ( t , kvToDestroy . Id , job . KeyId )
assert . Equal ( t , "global" , job . ScopeId )
})
t . Run ( "lists-no-jobs-when-given-unknown-scope" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
sqldb , err := conn . SqlDB ( testCtx )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 200)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
jobs , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "myscope" )
require . NoError ( t , err )
assert . Empty ( t , jobs )
})
}
func TestMonitorTableRewrappingRuns ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
err = kmsCache . RotateKeys ( testCtx , "global" )
require . NoError ( t , err )
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
sqldb , err := conn . SqlDB ( testCtx )
require . NoError ( t , err )
t . Run ( "does-nothing-when-no-run-available" , func ( t * testing . T ) {
callbackCalled := false
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
callbackCalled = true
return nil
}
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
require . NoError ( t , err )
assert . False ( t , callbackCalled , "auth_token callback should not have been called" )
})
t . Run ( "returns-an-error-when-given-empty-table-name" , func ( t * testing . T ) {
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "" )
require . Error ( t , err )
})
t . Run ( "returns-an-error-when-no-rewrapping-function-registered" , func ( t * testing . T ) {
tableNameToRewrapFn = make ( map [ string ] RewrapFn )
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
require . Error ( t , err )
})
t . Run ( "does-nothing-when-another-run-is-running" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_oidc_method', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_oidc_method'" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
callbackCalled := false
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
callbackCalled = true
return nil
}
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
require . NoError ( t , err )
assert . False ( t , callbackCalled , "auth_token callback should not have been called" )
})
t . Run ( "chooses-one-when-one-run-available" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
callbackCalled := make ( chan struct {})
returnFromCallback := make ( chan struct {})
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
close ( callbackCalled )
assert . Equal ( t , "global" , scopeId )
// Block here until we want it to return
<- returnFromCallback
return nil
}
monitorErrCh := make ( chan error )
// Run in goroutine so we can check status while the job is running
go func () {
monitorErrCh <- kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
}()
// Wait for callback to have been called
select {
case <- callbackCalled :
case err := <- monitorErrCh :
require . NoError ( t , err )
case <- time . After ( 5 * time . Second ):
t . Fatalf ( "Callback had not been called after 5 seconds" )
return
}
// Now we know that the job is waiting for the callback to return, lets
// do some db inspection
row := sqldb . QueryRowContext ( testCtx , "select is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
isRunning := false
err = row . Scan ( & isRunning )
require . NoError ( t , err )
assert . True ( t , isRunning , "is_running should be set to true" )
// Trigger callback to return
close ( returnFromCallback )
// Wait for function to return
select {
case err := <- monitorErrCh :
require . NoError ( t , err )
case <- time . After ( 5 * time . Second ):
t . Fatalf ( "Function has not returned 5 seconds after callback finished" )
return
}
// Lets look at the db state again after the function has returned
row = sqldb . QueryRowContext ( testCtx , "select completed_count, is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
completedCount := 0
isRunning = false
err = row . Scan ( & completedCount , & isRunning )
require . NoError ( t , err )
assert . EqualValues ( t , completedCount , 100 , "completed_count should have been updated" )
assert . False ( t , isRunning , "is_running should be set to false" )
})
t . Run ( "chooses-oldest-when-two-runs-available" , func ( t * testing . T ) {
var kvsToDestroy [] wrappingKms . KeyVersion
for _ , key := range keys {
switch key . Purpose {
case wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()), wrappingKms . KeyPurpose ( KeyPurposeTokens . String ()):
kvsToDestroy = append ( kvsToDestroy , key . Versions [ 0 ])
}
}
require . Len ( t , kvsToDestroy , 2 )
for _ , kvToDestroy := range kvsToDestroy {
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
}
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
rewrappedKeyVersion := ""
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
rewrappedKeyVersion = dataKeyVersionId
return nil
}
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
require . NoError ( t , err )
assert . Equal ( t , rewrappedKeyVersion , kvsToDestroy [ 0 ]. Id , "auth_token callback should have been called with the oldest job" )
})
t . Run ( "resumes-running-one-when-two-runs-available-even-if-not-oldest" , func ( t * testing . T ) {
var kvsToDestroy [] wrappingKms . KeyVersion
for _ , key := range keys {
switch key . Purpose {
case wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()), wrappingKms . KeyPurpose ( KeyPurposeTokens . String ()):
kvsToDestroy = append ( kvsToDestroy , key . Versions [ 0 ])
}
}
require . Len ( t , kvsToDestroy , 2 )
for i , kvToDestroy := range kvsToDestroy {
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
if i == 1 {
_ , err = sqldb . ExecContext ( testCtx , "update kms_data_key_version_destruction_job_run set is_running=true where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
require . NoError ( t , err )
}
}
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
rewrappedKeyVersion := ""
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
rewrappedKeyVersion = dataKeyVersionId
return nil
}
err = kmsCache . MonitorTableRewrappingRuns ( testCtx , "auth_token" )
require . NoError ( t , err )
assert . Equal ( t , rewrappedKeyVersion , kvsToDestroy [ 1 ]. Id , "auth_token callback should have been called with the already running job" )
})
t . Run ( "updates-the-running-state-even-when-context-canceled" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
callbackCalled := make ( chan struct {})
2022-12-13 05:38:14 -05:00
tableNameToRewrapFn [ "auth_token" ] = func ( ctx context . Context , dataKeyVersionId string , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
close ( callbackCalled )
// Block here until we want it to return
<- ctx . Done ()
return ctx . Err ()
}
monitorErrCh := make ( chan error )
newCtx , cancel := context . WithCancel ( testCtx )
defer cancel ()
// Run in goroutine so we can check status while the job is running
go func () {
monitorErrCh <- kmsCache . MonitorTableRewrappingRuns ( newCtx , "auth_token" )
}()
// Wait for callback to have been called
select {
case <- callbackCalled :
case err := <- monitorErrCh :
require . NoError ( t , err )
case <- time . After ( 5 * time . Second ):
t . Fatalf ( "Callback had not been called after 5 seconds" )
return
}
// Now we know that the job is waiting for the callback to return, lets
// do some db inspection
row := sqldb . QueryRowContext ( testCtx , "select is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
isRunning := false
err = row . Scan ( & isRunning )
require . NoError ( t , err )
assert . True ( t , isRunning , "is_running should be set to true" )
// Trigger callback to return
cancel ()
// Wait for function to return
select {
case err := <- monitorErrCh :
require . True ( t , errors . Is ( err , context . Canceled ))
case <- time . After ( 5 * time . Second ):
t . Fatalf ( "Function has not returned 5 seconds after callback finished" )
return
}
// Lets look at the db state again after the function has returned
row = sqldb . QueryRowContext ( testCtx , "select completed_count, is_running from kms_data_key_version_destruction_job_run where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
completedCount := 0
isRunning = false
err = row . Scan ( & completedCount , & isRunning )
require . NoError ( t , err )
assert . EqualValues ( t , completedCount , 100 , "completed_count should have been updated" )
assert . False ( t , isRunning , "is_running should be set to false" )
})
}
func TestMonitorDataKeyVersionDestruction ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
err = kmsCache . RotateKeys ( testCtx , "global" )
require . NoError ( t , err )
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
sqldb , err := conn . SqlDB ( testCtx )
require . NoError ( t , err )
t . Run ( "does-nothing-when-no-jobs-available" , func ( t * testing . T ) {
err = kmsCache . MonitorDataKeyVersionDestruction ( testCtx )
require . NoError ( t , err )
})
t . Run ( "does-nothing-when-the-job-isnt-completed" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
err = kmsCache . MonitorDataKeyVersionDestruction ( testCtx )
require . NoError ( t , err )
row := sqldb . QueryRowContext ( testCtx , "select exists(select 1 from kms_data_key_version_destruction_job where key_id=$1)" , kvToDestroy . Id )
exists := false
err = row . Scan ( & exists )
require . NoError ( t , err )
assert . True ( t , exists , "the job should still exist" )
})
t . Run ( "deletes-the-key-when-the-job-is-completed" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job(key_id) values ($1)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "insert into kms_data_key_version_destruction_job_run(key_id, table_name, total_count) values ($1, 'auth_token', 100)" , kvToDestroy . Id )
require . NoError ( t , err )
_ , err = sqldb . ExecContext ( testCtx , "update kms_data_key_version_destruction_job_run set completed_count=100 where key_id=$1 and table_name='auth_token'" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "truncate kms_data_key_version_destruction_job, kms_data_key_version_destruction_job_run CASCADE" )
require . NoError ( t , err )
})
err = kmsCache . MonitorDataKeyVersionDestruction ( testCtx )
require . NoError ( t , err )
row := sqldb . QueryRowContext ( testCtx , "select exists(select 1 from kms_data_key_version_destruction_job where key_id=$1)" , kvToDestroy . Id )
exists := false
err = row . Scan ( & exists )
require . NoError ( t , err )
// The job is deleted by virtue of cascading foreign key references
assert . False ( t , exists , "the job should be deleted" )
})
}
func TestDestroyKeyVersion ( t * testing . T ) {
t . Parallel ()
testCtx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
extWrapper := db . TestWrapper ( t )
kmsCache := TestKms ( t , conn , extWrapper )
err := kmsCache . CreateKeys ( testCtx , "global" )
require . NoError ( t , err )
err = kmsCache . RotateKeys ( testCtx , "global" )
require . NoError ( t , err )
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
sqldb , err := conn . SqlDB ( testCtx )
require . NoError ( t , err )
t . Run ( "returns-an-error-when-given-an-empty-scope-id" , func ( t * testing . T ) {
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "" , "krkv_DoesntExist" )
assert . Error ( t , err )
})
t . Run ( "returns-an-error-when-given-an-empty-key-version-id" , func ( t * testing . T ) {
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , "" )
assert . Error ( t , err )
})
t . Run ( "returns-an-error-when-attempting-to-destroy-an-unknown-key-version" , func ( t * testing . T ) {
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , "krkv_DoesntExist" )
require . Error ( t , err )
assert . True ( t , errors . Match ( errors . T ( errors . KeyNotFound ), err ))
})
t . Run ( "returns-an-error-when-attempting-to-destroy-a-key-version-in-an-unknown-scope" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ len ( key . Versions ) - 1 ]
}
}
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "p_DoesntExist" , kvToDestroy . Id )
require . Error ( t , err )
assert . True ( t , errors . Match ( errors . T ( errors . RecordNotFound ), err ))
})
t . Run ( "returns-an-error-when-attempting-to-destroy-latest-key-version" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ len ( key . Versions ) - 1 ]
}
}
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . Error ( t , err )
assert . True ( t , errors . Match ( errors . T ( errors . InvalidParameter ), err ))
})
t . Run ( "returns-an-error-when-attempting-to-destroy-oplog-key-version" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeOplog . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
2023-09-06 18:25:49 -04:00
require . NotEmpty ( t , kvToDestroy . Id )
2022-11-16 17:17:28 -06:00
_ , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . Error ( t , err )
2023-09-06 18:25:49 -04:00
assert . True ( t , errors . Match ( errors . T ( errors . KeyNotFound ), err ))
2022-11-16 17:17:28 -06:00
})
t . Run ( "succeeds-synchronously-when-destroying-old-root-key-version" , func ( t * testing . T ) {
var chosenKey wrappingKms . Key
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeRootKey . String ()) {
kvToDestroy = key . Versions [ 0 ]
chosenKey = key
}
}
destroyed , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . NoError ( t , err )
assert . True ( t , destroyed )
// Check that the key version is destroyed
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
for _ , key := range keys {
if key . Id == chosenKey . Id {
require . Len ( t , key . Versions , 1 , "the root key should only have one version left" )
assert . NotEqual ( t , key . Versions [ 0 ]. Id , kvToDestroy . Id )
}
}
})
t . Run ( "succeeds-synchronously-when-destroying-old-data-key-version-that-encrypts-no-data" , func ( t * testing . T ) {
require . NoError ( t , err )
var chosenKey wrappingKms . Key
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeAudit . String ()) {
kvToDestroy = key . Versions [ 0 ]
chosenKey = key
}
}
destroyed , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . NoError ( t , err )
assert . True ( t , destroyed )
// Check that the key version is destroyed
keys , err := kmsCache . ListKeys ( testCtx , "global" )
require . NoError ( t , err )
for _ , key := range keys {
if key . Id == chosenKey . Id {
require . Len ( t , key . Versions , 1 , "the data key should only have one version left" )
assert . NotEqual ( t , key . Versions [ 0 ]. Id , kvToDestroy . Id )
}
}
})
t . Run ( "creates-asynchronous-job-and-runs-for-data-key-version-that-encrypt-data" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
// Create a row that references the key we want to destroy.
// Note: This could break if the underlying table definitions change.
// Future contributors; forgive me, but we do really want to test this
// functionality here, and we can't import any of the normal test helpers
// since it would create circular dependencies.
_ , err = sqldb . ExecContext ( testCtx , "insert into worker_auth_ca_certificate(certificate, not_valid_after, public_key, private_key, state, issuing_ca, key_id) values ('certificate', CURRENT_TIMESTAMP+'1h'::interval, 'public_key', 'private_key', 'current', 'roots', $1)" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "delete from worker_auth_ca_certificate" )
require . NoError ( t , err )
})
destroyed , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "delete from kms_data_key_version_destruction_job; delete from kms_data_key_version_destruction_job_run" )
require . NoError ( t , err )
})
assert . False ( t , destroyed )
// Check that the data key version destruction job was created
jobs , err := kmsCache . ListDataKeyVersionDestructionJobs ( testCtx , "global" )
require . NoError ( t , err )
require . Len ( t , jobs , 1 )
assert . Equal ( t , jobs [ 0 ]. KeyId , kvToDestroy . Id )
assert . Equal ( t , jobs [ 0 ]. CompletedCount , int64 ( 0 ))
assert . Equal ( t , jobs [ 0 ]. TotalCount , int64 ( 1 ))
assert . Equal ( t , jobs [ 0 ]. Status , "pending" )
assert . Equal ( t , jobs [ 0 ]. ScopeId , "global" )
assert . True ( t , jobs [ 0 ]. CreateTime . AsTime (). Before ( time . Now ()))
})
t . Run ( "errors-when-destroying-key-that-is-already-destroying" , func ( t * testing . T ) {
var kvToDestroy wrappingKms . KeyVersion
for _ , key := range keys {
if key . Purpose == wrappingKms . KeyPurpose ( KeyPurposeDatabase . String ()) {
kvToDestroy = key . Versions [ 0 ]
}
}
_ , err = sqldb . ExecContext ( testCtx , "insert into worker_auth_ca_certificate(certificate, not_valid_after, public_key, private_key, state, issuing_ca, key_id) values ('certificate', CURRENT_TIMESTAMP+'1h'::interval, 'public_key', 'private_key', 'current', 'roots', $1)" , kvToDestroy . Id )
require . NoError ( t , err )
t . Cleanup ( func () {
_ , err = sqldb . ExecContext ( testCtx , "delete from worker_auth_ca_certificate" )
require . NoError ( t , err )
})
destroyed , err := kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . NoError ( t , err )
assert . False ( t , destroyed )
_ , err = kmsCache . DestroyKeyVersion ( testCtx , "global" , kvToDestroy . Id )
require . Error ( t , err )
assert . True ( t , errors . Match ( errors . T ( errors . InvalidParameter ), err ), "error did not match InvalidParameter as expected: %v" , err )
})
}
func Test_RegisterTableRewrapFn ( t * testing . T ) {
2022-12-13 05:38:14 -05:00
rewrapFn := func ( ctx context . Context , dataKeyVersionId , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
return nil
}
RegisterTableRewrapFn ( "a_table" , rewrapFn )
t . Cleanup ( func () {
tableNameToRewrapFn = map [ string ] RewrapFn {}
})
assert . Panics ( t , func () {
RegisterTableRewrapFn ( "a_table" , rewrapFn )
})
}
func Test_ListTablesSupportingRewrap ( t * testing . T ) {
assert . Empty ( t , ListTablesSupportingRewrap ())
2022-12-13 05:38:14 -05:00
rewrapFn := func ( ctx context . Context , dataKeyVersionId , scopeId string , reader db . Reader , writer db . Writer , kms GetWrapperer ) error {
2022-11-16 17:17:28 -06:00
return nil
}
RegisterTableRewrapFn ( "a_table" , rewrapFn )
t . Cleanup ( func () {
tableNameToRewrapFn = map [ string ] RewrapFn {}
})
registeredTables := ListTablesSupportingRewrap ()
assert . Len ( t , registeredTables , 1 )
assert . Equal ( t , registeredTables [ 0 ], "a_table" )
}
2022-05-13 13:53:22 -04:00
type invalidReader struct {
db . Reader
}
2021-03-03 11:23:00 -05:00
2022-05-13 13:53:22 -04:00
type invalidWriter struct {
db . Writer
2020-08-19 16:47:18 -04:00
}