open-vault/builtin/logical/database/rotation_test.go
Clint b55303eddb
Combined Database Backend: Static Accounts (#6834)
* Add priority queue to sdk

* fix issue of storing pointers and now copy

* update to use copy structure

* Remove file, put Item struct def. into other file

* add link

* clean up docs

* refactor internal data structure to hide heap method implementations. Other cleanup after feedback

* rename PushItem and PopItem to just Push/Pop, after encapsulating the heap methods

* updates after feedback

* refactoring/renaming

* guard against pushing a nil item

* minor updates after feedback

* Add SetCredentials, GenerateCredentials gRPC methods to combined database backend gPRC

* Initial Combined database backend implementation of static accounts and automatic rotation

* vendor updates

* initial implementation of static accounts with Combined database backend, starting with PostgreSQL implementation

* add lock and setup of rotation queue

* vendor the queue

* rebase on new method signature of queue

* remove mongo tests for now

* update default role sql

* gofmt after rebase

* cleanup after rebasing to remove checks for ErrNotFound error

* rebase cdcr-priority-queue

* vendor dependencies with 'go mod vendor'

* website database docs for Static Role support

* document the rotate-role API endpoint

* postgres specific static role docs

* use constants for paths

* updates from review

* remove dead code

* combine and clarify error message for older plugins

* Update builtin/logical/database/backend.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* cleanups from feedback

* code and comment cleanups

* move db.RLock higher to protect db.GenerateCredentials call

* Return output with WALID if we failed to delete the WAL

* Update builtin/logical/database/path_creds_create.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* updates after running 'make fmt'

* update after running 'make proto'

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* update comment and remove and rearrange some dead code

* Update website/source/api/secret/databases/index.html.md

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* cleanups after review

* Update sdk/database/dbplugin/grpc_transport.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* code cleanup after feedback

* remove PasswordLastSet; it's not used

* document GenerateCredentials and SetCredentials

* Update builtin/logical/database/path_rotate_credentials.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* wrap pop and popbykey in backend methods to protect against nil cred rotation queue

* use strings.HasPrefix instead of direct equality check for path

* Forgot to commit this

* updates after feedback

* re-purpose an outdated test to now check that static and dynamic roles cannot share a name

* check for unique name across dynamic and static roles

* refactor loadStaticWALs to return a map of name/setCredentialsWAL struct to consolidate where we're calling set credentials

* remove commented out code

* refactor to have loadstaticwals filter out wals for roles that no longer exist

* return error if nil input given

* add nil check for input into setStaticAccount

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* add constant for queue tick time in seconds, used for comparrison in updates

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Jim Kalafut <jim@kalafut.net>

* code cleanup after review

* remove misplaced code comment

* remove commented out code

* create a queue in the Factory method, even if it's never used

* update path_roles to use a common set of fields, with specific overrides for dynamic/static roles by type

* document new method

* move rotation things into a specific file

* rename test file and consolidate some static account tests

* Update builtin/logical/database/path_roles.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* Update builtin/logical/database/rotation.go

Co-Authored-By: Brian Kassouf <briankassouf@users.noreply.github.com>

* update code comments, method names, and move more methods into rotation.go

* update comments to be capitalized

* remove the item from the queue before we try to destroy it

* findStaticWAL returns an error

* use lowercase keys when encoding WAL entries

* small cleanups

* remove vestigial static account check

* remove redundant DeleteWAL call in populate queue

* if we error on loading role, push back to queue with 10 second backoff

* poll in initqueue to make sure the backend is setup and can write/delete data

* add revoke_user_on_delete flag to allow users to opt-in to revoking the static database user on delete of the Vault role. Default false

* add code comments on read-only loop

* code comment updates

* re-push if error returned from find static wal

* add locksutil and acquire locks when pop'ing from the queue

* grab exclusive locks for updating static roles

* Add SetCredentials and GenerateCredentials stubs to mockPlugin

* add a switch in initQueue to listen for cancelation

* remove guard on zero time, it should have no affect

* create a new context in Factory to pass on and use for closing the backend queue

* restore master copy of vendor dir
2019-06-19 14:45:39 -05:00

815 lines
22 KiB
Go

package database
import (
"context"
"strings"
"testing"
"time"
"database/sql"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
_ "github.com/lib/pq"
)
func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"rotation_period": "5400s",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// Re-read the creds, verifying they aren't changing on read
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if username != resp.Data["username"].(string) || password != resp.Data["password"].(string) {
t.Fatal("expected re-read username/password to match, but didn't")
}
// Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
if resp != nil {
t.Fatalf("Expected empty response from rotate-role: (%#v)", resp)
}
// Re-Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
newPassword := resp.Data["password"].(string)
if password == newPassword {
t.Fatalf("expected passwords to differ, got (%s)", newPassword)
}
// Verify new username/password
if err := verifyPgConn(t, username, newPassword, connURL); err != nil {
t.Fatal(err)
}
}
// Sanity check to make sure we don't allow an attempt of rotating credentials
// for non-static accounts, which doesn't make sense anyway, but doesn't hurt to
// verify we return an error
func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate-role/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
// expect resp to be an error
resp, _ = b.HandleRequest(namespace.RootContext(nil), req)
if !resp.IsError() {
t.Fatalf("expected error rotating non-static role")
}
if resp.Error().Error() != "no static role found for role name" {
t.Fatalf("wrong error message: %s", err)
}
}
func TestBackend_StaticRole_Revoke_user(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(context.Background())
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
testCases := map[string]struct {
revoke *bool
expectVerifyErr bool
}{
// Default case: user does not specify, Vault leaves the database user
// untouched, and the final connection check passes because the user still
// exists
"unset": {},
// Revoke on delete. The final connection check should fail because the user
// no longer exists
"revoke": {
revoke: newBoolPtr(true),
expectVerifyErr: true,
},
// Revoke false, final connection check should still pass
"persist": {
revoke: newBoolPtr(false),
},
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"rotation_period": "5400s",
}
if tc.revoke != nil {
data["revoke_user_on_delete"] = *tc.revoke
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Read the creds
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/plugin-role-test",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("empty username (%s) or password (%s)", username, password)
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
// delete the role, expect the default where the user is not destroyed
// Read the creds
req = &logical.Request{
Operation: logical.DeleteOperation,
Path: "static-roles/plugin-role-test",
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Verify new username/password still work
if err := verifyPgConn(t, username, password, connURL); err != nil {
if !tc.expectVerifyErr {
t.Fatal(err)
}
}
})
}
}
func verifyPgConn(t *testing.T, username, password, connURL string) error {
cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1)
db, err := sql.Open("postgres", cURL)
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
return db.Close()
}
// WAL testing
//
// First scenario, WAL contains a role name that does not exist.
func TestBackend_Static_QueueWAL_discard_role_not_found(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
ctx := context.Background()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
_, err := framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
RoleName: "doesnotexist",
})
if err != nil {
t.Fatalf("error with PutWAL: %s", err)
}
assertWALCount(t, config.StorageView, 1)
b, err := Factory(ctx, config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(ctx)
time.Sleep(5 * time.Second)
bd := b.(*databaseBackend)
if bd.credRotationQueue == nil {
t.Fatal("database backend had no credential rotation queue")
}
// Verify empty queue
if bd.credRotationQueue.Len() != 0 {
t.Fatalf("expected zero queue items, got: %d", bd.credRotationQueue.Len())
}
assertWALCount(t, config.StorageView, 0)
}
// Second scenario, WAL contains a role name that does exist, but the role's
// LastVaultRotation is greater than the WAL has
func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
ctx := context.Background()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
roleName := "test-discard-by-date"
lb, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok := lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Save Now() to make sure rotation time is after this, as well as the WAL
// time
roleTime := time.Now()
// Create role
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
// Low value here, to make sure the backend rotates this password at least
// once before we compare it to the WAL
"rotation_period": "10s",
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Allow the first rotation to occur, setting LastVaultRotation
time.Sleep(time.Second * 12)
// Cleanup the backend, then create a WAL for the role with a
// LastVaultRotation of 1 hour ago, so that when we recreate the backend the
// WAL will be read but discarded
b.Cleanup(ctx)
b = nil
time.Sleep(time.Second * 3)
// Make a fake WAL entry with an older time
oldRotationTime := roleTime.Add(time.Hour * -1)
walPassword := "somejunkpassword"
_, err = framework.PutWAL(ctx, config.StorageView, staticWALKey, &setCredentialsWAL{
RoleName: roleName,
NewPassword: walPassword,
LastVaultRotation: oldRotationTime,
Username: "statictest",
})
if err != nil {
t.Fatalf("error with PutWAL: %s", err)
}
assertWALCount(t, config.StorageView, 1)
// Reload backend
lb, err = Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
b, ok = lb.(*databaseBackend)
if !ok {
t.Fatal("could not convert to db backend")
}
defer b.Cleanup(ctx)
// Allow enough time for populateQueue to work after boot
time.Sleep(time.Second * 12)
// PopulateQueue should have processed the entry
assertWALCount(t, config.StorageView, 0)
// Read the role
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
lastVaultRotation := resp.Data["last_vault_rotation"].(time.Time)
if !lastVaultRotation.After(oldRotationTime) {
t.Fatal("last vault rotation time not greater than WAL time")
}
if !lastVaultRotation.After(roleTime) {
t.Fatal("last vault rotation time not greater than role creation time")
}
// Grab password to verify it didn't change
req = &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/" + roleName,
Storage: config.StorageView,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
password := resp.Data["password"].(string)
if password == walPassword {
t.Fatalf("expected password to not be changed by WAL, but was")
}
}
// Helper to assert the number of WAL entries is what we expect
func assertWALCount(t *testing.T, s logical.Storage, expected int) {
var count int
ctx := context.Background()
keys, err := framework.ListWAL(ctx, s)
if err != nil {
t.Fatal("error listing WALs")
}
// Loop through WAL keys and process any rotation ones
for _, k := range keys {
walEntry, _ := framework.GetWAL(ctx, s, k)
if walEntry == nil {
continue
}
if walEntry.Kind != staticWALKey {
continue
}
count++
}
if expected != count {
t.Fatalf("WAL count mismatch, expected (%d), got (%d)", expected, count)
}
}
//
// End WAL testing
//
func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
config.System = sys
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
defer b.Cleanup(context.Background())
bd := b.(*databaseBackend)
if bd.credRotationQueue == nil {
t.Fatal("database backend had no credential rotation queue")
}
// Configure backend, add item and confirm length
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
"plugin_name": "postgresql-database-plugin",
"verify_connection": false,
"allowed_roles": []string{"*"},
"name": "plugin-test",
}
req := &logical.Request{
Operation: logical.UpdateOperation,
Path: "config/plugin-test",
Storage: config.StorageView,
Data: data,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
// Create three static roles with different rotation periods
testCases := []string{"65", "130", "5400"}
for _, tc := range testCases {
roleName := "plugin-static-role-" + tc
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest" + tc,
"rotation_period": tc,
}
req = &logical.Request{
Operation: logical.CreateOperation,
Path: "static-roles/" + roleName,
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
}
// Verify the queue has 3 items in it
if bd.credRotationQueue.Len() != 3 {
t.Fatalf("expected 3 items in the rotation queue, got: (%d)", bd.credRotationQueue.Len())
}
// List the roles
data = map[string]interface{}{}
req = &logical.Request{
Operation: logical.ListOperation,
Path: "static-roles/",
Storage: config.StorageView,
Data: data,
}
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 3 {
t.Fatalf("expected 3 roles, got: (%d)", len(keys))
}
// Capture initial passwords, before the periodic function is triggered
pws := make(map[string][]string, 0)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep to make sure the 65s role will be up for rotation by the time the
// periodic function ticks
time.Sleep(7 * time.Second)
// Sleep 75 to make sure the periodic func has time to actually run
time.Sleep(75 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Sleep more, this should allow both sr65 and sr130 to rotate
time.Sleep(140 * time.Second)
pws = capturePasswords(t, b, config, testCases, pws)
// Verify all pws are as they should
pass := true
for k, v := range pws {
switch {
case k == "plugin-static-role-65":
// expect all passwords to be different
if v[0] == v[1] || v[1] == v[2] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-130":
// expect the first two to be equal, but different from the third
if v[0] != v[1] || v[0] == v[2] {
pass = false
}
case k == "plugin-static-role-5400":
// expect all passwords to be equal
if v[0] != v[1] || v[1] != v[2] {
pass = false
}
}
}
if !pass {
t.Fatalf("password rotations did not match expected: %#v", pws)
}
}
// capturePasswords captures the current passwords at the time of calling, and
// returns a map of username / passwords building off of the input map
func capturePasswords(t *testing.T, b logical.Backend, config *logical.BackendConfig, testCases []string, pws map[string][]string) map[string][]string {
new := make(map[string][]string, 0)
for _, tc := range testCases {
// Read the role
roleName := "plugin-static-role-" + tc
req := &logical.Request{
Operation: logical.ReadOperation,
Path: "static-creds/" + roleName,
Storage: config.StorageView,
}
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, resp)
}
username := resp.Data["username"].(string)
password := resp.Data["password"].(string)
if username == "" || password == "" {
t.Fatalf("expected both username/password for (%s), got (%s), (%s)", roleName, username, password)
}
new[roleName] = append(new[roleName], password)
}
for k, v := range new {
pws[k] = append(pws[k], v...)
}
return pws
}
func newBoolPtr(b bool) *bool {
v := b
return &v
}