From 12f5a4231725008de95928fe7767d09479c3a0de Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Tue, 22 Oct 2019 09:35:48 -0400 Subject: [PATCH 01/14] TestSysRekey_Verification would fail sometimes when recovery=true (#7710) because when unsealing it wouldn't wait for core 0 to come up and become the active node. Much of our testing code assumes that core0 is the active node. --- .../external_tests/api/sys_rekey_ext_test.go | 11 ++-- vault/testing.go | 54 ++++++++----------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/vault/external_tests/api/sys_rekey_ext_test.go b/vault/external_tests/api/sys_rekey_ext_test.go index c84d802dc3f8..34f5df74c964 100644 --- a/vault/external_tests/api/sys_rekey_ext_test.go +++ b/vault/external_tests/api/sys_rekey_ext_test.go @@ -139,12 +139,9 @@ func testSysRekey_Verification(t *testing.T, recovery bool, legacyShamir bool) { // Sealing should clear state, so after this we should be able to perform // the above again cluster.EnsureCoresSealed(t) - if recovery { - cluster.UnsealWithStoredKeys(t) - } else { - cluster.UnsealCores(t) + if err := cluster.UnsealCoresWithError(recovery); err != nil { + t.Fatal(err) } - vault.TestWaitActive(t, cluster.Cores[0].Core) doRekeyInitialSteps() doStartVerify := func() { @@ -258,7 +255,7 @@ func testSysRekey_Verification(t *testing.T, recovery bool, legacyShamir bool) { cluster.Start() defer cluster.Cleanup() - if err := cluster.UnsealCoresWithError(); err == nil { + if err := cluster.UnsealCoresWithError(false); err == nil { t.Fatal("expected error") } @@ -272,7 +269,7 @@ func testSysRekey_Verification(t *testing.T, recovery bool, legacyShamir bool) { newKeyBytes = append(newKeyBytes, val) } cluster.BarrierKeys = newKeyBytes - if err := cluster.UnsealCoresWithError(); err != nil { + if err := cluster.UnsealCoresWithError(false); err != nil { t.Fatal(err) } } else { diff --git a/vault/testing.go b/vault/testing.go index ba6e554dd54a..7f56e402bc11 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -829,19 +829,29 @@ func (c *TestCluster) Start() { // UnsealCores uses the cluster barrier keys to unseal the test cluster cores func (c *TestCluster) UnsealCores(t testing.T) { t.Helper() - if err := c.UnsealCoresWithError(); err != nil { + if err := c.UnsealCoresWithError(false); err != nil { t.Fatal(err) } } -func (c *TestCluster) UnsealCoresWithError() error { - numCores := len(c.Cores) +func (c *TestCluster) UnsealCoresWithError(useStoredKeys bool) error { + unseal := func(core *Core) error { + for _, key := range c.BarrierKeys { + if _, err := core.Unseal(TestKeyCopy(key)); err != nil { + return err + } + } + return nil + } + if useStoredKeys { + unseal = func(core *Core) error { + return core.UnsealWithStoredKeys(context.Background()) + } + } // Unseal first core - for _, key := range c.BarrierKeys { - if _, err := c.Cores[0].Unseal(TestKeyCopy(key)); err != nil { - return fmt.Errorf("unseal core %d err: %s", 0, err) - } + if err := unseal(c.Cores[0].Core); err != nil { + return fmt.Errorf("unseal core %d err: %s", 0, err) } // Verify unsealed @@ -854,11 +864,9 @@ func (c *TestCluster) UnsealCoresWithError() error { } // Unseal other cores - for i := 1; i < numCores; i++ { - for _, key := range c.BarrierKeys { - if _, err := c.Cores[i].Core.Unseal(TestKeyCopy(key)); err != nil { - return fmt.Errorf("unseal core %d err: %s", i, err) - } + for i := 1; i < len(c.Cores); i++ { + if err := unseal(c.Cores[i].Core); err != nil { + return fmt.Errorf("unseal core %d err: %s", i, err) } } @@ -867,7 +875,7 @@ func (c *TestCluster) UnsealCoresWithError() error { // Ensure cluster connection info is populated. // Other cores should not come up as leaders. - for i := 1; i < numCores; i++ { + for i := 1; i < len(c.Cores); i++ { isLeader, _, _, err := c.Cores[i].Leader() if err != nil { return err @@ -989,26 +997,6 @@ func (c *TestCluster) ensureCoresSealed() error { return nil } -// UnsealWithStoredKeys uses stored keys to unseal the test cluster cores -func (c *TestCluster) UnsealWithStoredKeys(t testing.T) error { - for _, core := range c.Cores { - if err := core.UnsealWithStoredKeys(context.Background()); err != nil { - return err - } - timeout := time.Now().Add(60 * time.Second) - for { - if time.Now().After(timeout) { - return fmt.Errorf("timeout waiting for core to unseal") - } - if !core.Sealed() { - break - } - time.Sleep(250 * time.Millisecond) - } - } - return nil -} - func SetReplicationFailureMode(core *TestClusterCore, mode uint32) { atomic.StoreUint32(core.Core.replicationFailure, mode) } From ef9b20aa768f84f5f725fc410e918286ae3acb3b Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Tue, 22 Oct 2019 09:41:16 -0400 Subject: [PATCH 02/14] changelog++ --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 116770548492..aac4e84c726f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ FEATURES: password will be rotated when it's checked back in. * **New UI Features** The UI now supports managing users and groups for the Userpass, Cert, Okta, and Radius auth methods. * **Vault Agent Template** Vault Agent now supports rendering templates containing Vault secrets to disk, similar to Consul Template [GH-7652] + * **Shamir with Stored Master Key** The on disk format for Shamir seals has changed, + allowing for a secondary cluster using Shamir downstream from a primary cluster + using AutoUnseal. [GH-7694] CHANGES: From 7db0d2e8d9a6045106a6a4ebcbb25e1e3eeb27f0 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Tue, 22 Oct 2019 09:57:24 -0400 Subject: [PATCH 03/14] Fix a nil map pointer in mergeEntity. (#7711) --- vault/identity_store_entities.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vault/identity_store_entities.go b/vault/identity_store_entities.go index 1ff68f1d246e..388b52fc446c 100644 --- a/vault/identity_store_entities.go +++ b/vault/identity_store_entities.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/errwrap" memdb "github.com/hashicorp/go-memdb" "github.com/hashicorp/vault/helper/identity" + "github.com/hashicorp/vault/helper/identity/mfa" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/storagepacker" "github.com/hashicorp/vault/sdk/framework" @@ -649,6 +650,9 @@ func (i *IdentityStore) mergeEntity(ctx context.Context, txn *memdb.Txn, toEntit if ok && !force { return nil, fmt.Errorf("conflicting MFA config ID %q in entity ID %q", configID, fromEntity.ID) } else { + if toEntity.MFASecrets == nil { + toEntity.MFASecrets = make(map[string]*mfa.Secret) + } toEntity.MFASecrets[configID] = configSecret } } From a54091144ebf55340aca628649b88ab0abaefc47 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Tue, 22 Oct 2019 10:47:42 -0400 Subject: [PATCH 04/14] changelog++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aac4e84c726f..6539431053c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ BUG FIXES: always returned an empty object [GH-7705] * identity (enterprise): Fixed identity case sensitive loading in secondary cluster [GH-7327] + * identity: Fixed nil pointer panic when merging entities [GH-7711] * raft: Fixed VAULT_CLUSTER_ADDR env being ignored at startup [GH-7619] * ui: using the `wrapped_token` query param will work with `redirect_to` and will automatically log in as intended [GH-7398] From c49cb5b362e191c3a4c678b6654103ff536cc837 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Tue, 22 Oct 2019 13:37:41 -0400 Subject: [PATCH 05/14] Use docker instead of an external LDAP server that sometimes goes down (#7522) --- builtin/credential/ldap/backend_test.go | 230 ++++++++++-------- go.mod | 1 + helper/testhelpers/ldap/ldaphelper.go | 66 +++++ vault/external_tests/identity/groups_test.go | 55 +++-- .../external_tests/identity/identity_test.go | 218 ++++++++--------- vault/external_tests/token/token_test.go | 30 ++- 6 files changed, 349 insertions(+), 251 deletions(-) create mode 100644 helper/testhelpers/ldap/ldaphelper.go diff --git a/builtin/credential/ldap/backend_test.go b/builtin/credential/ldap/backend_test.go index f8f8b6583a99..3bc0b1d96c87 100644 --- a/builtin/credential/ldap/backend_test.go +++ b/builtin/credential/ldap/backend_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/helper/testhelpers/ldap" logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" "github.com/hashicorp/vault/sdk/helper/ldaputil" "github.com/hashicorp/vault/sdk/helper/policyutil" @@ -200,7 +201,7 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { "groups": "EngineerS", "policies": "userpolicy", }, - Path: "users/teSlA", + Path: "users/hermeS conRad", Storage: storage, } resp, err = b.HandleRequest(ctx, userReq) @@ -213,11 +214,11 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { } switch caseSensitive { case true: - if keys[0] != "teSlA" { + if keys[0] != "hermeS conRad" { t.Fatalf("bad: %s", keys[0]) } default: - if keys[0] != "tesla" { + if keys[0] != "hermes conrad" { t.Fatalf("bad: %s", keys[0]) } } @@ -231,7 +232,7 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { "groups": "EngineerS", "policies": "userpolicy", }, - Path: "users/tesla", + Path: "users/Hermes Conrad", Storage: storage, Connection: &logical.Connection{}, } @@ -243,9 +244,9 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { loginReq := &logical.Request{ Operation: logical.UpdateOperation, - Path: "login/tesla", + Path: "login/Hermes Conrad", Data: map[string]interface{}{ - "password": "password", + "password": "hermes", }, Storage: storage, Connection: &logical.Connection{}, @@ -260,17 +261,19 @@ func TestLdapAuthBackend_CaseSensitivity(t *testing.T) { } } + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() configReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, }, Storage: storage, } @@ -304,17 +307,19 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) { var err error b, storage := createBackendWithStorage(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() configReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpassword": cfg.BindPassword, }, Storage: storage, } @@ -343,7 +348,7 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) { "groups": "engineers", "policies": "userpolicy", }, - Path: "users/tesla", + Path: "users/hermes conrad", Storage: storage, Connection: &logical.Connection{}, } @@ -355,9 +360,9 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) { loginReq := &logical.Request{ Operation: logical.UpdateOperation, - Path: "login/tesla", + Path: "login/hermes conrad", Data: map[string]interface{}{ - "password": "password", + "password": "hermes", }, Storage: storage, Connection: &logical.Connection{}, @@ -376,18 +381,18 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) { /* * Acceptance test for LDAP Auth Method * - * The tests here rely on a public LDAP server: - * [http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/] + * The tests here rely on a docker LDAP server: + * [https://github.com/rroemhild/docker-test-openldap] * - * ...as well as existence of a person object, `uid=tesla,dc=example,dc=com`, - * which is a member of a group, `ou=scientists,dc=example,dc=com` + * ...as well as existence of a person object, `cn=Hermes Conrad,dc=example,dc=com`, + * which is a member of a group, `cn=admin_staff,ou=people,dc=example,dc=com` * * Querying the server from the command line: - * $ ldapsearch -x -H ldap://ldap.forumsys.com -b dc=example,dc=com -s sub \ - * '(&(objectClass=groupOfUniqueNames)(uniqueMember=uid=tesla,dc=example,dc=com))' - * - * $ ldapsearch -x -H ldap://ldap.forumsys.com -b dc=example,dc=com -s sub uid=tesla - */ + * $ docker run --privileged -d -p 389:389 --name ldap --rm rroemhild/test-openldap + * $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub uid=hermes + * $ ldapsearch -x -H ldap://localhost -b dc=planetexpress,dc=com -s sub \ + 'member=cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com' +*/ func factory(t *testing.T) logical.Backend { defaultLeaseTTLVal := time.Hour * 24 maxLeaseTTLVal := time.Hour * 24 * 32 @@ -406,59 +411,67 @@ func factory(t *testing.T) logical.Backend { func TestBackend_basic(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t), - // Map Scientists group (from LDAP server) with foo policy - testAccStepGroup(t, "Scientists", "foo"), + testAccStepConfigUrl(t, cfg), + // Map Admin_staff group (from LDAP server) with foo policy + testAccStepGroup(t, "admin_staff", "foo"), // Map engineers group (local) with bar policy testAccStepGroup(t, "engineers", "bar"), - // Map tesla user with local engineers group - testAccStepUser(t, "tesla", "engineers"), + // Map hermes conrad user with local engineers group + testAccStepUser(t, "hermes conrad", "engineers"), // Authenticate - testAccStepLogin(t, "tesla", "password"), + testAccStepLogin(t, "hermes conrad", "hermes"), // Verify both groups mappings can be listed back - testAccStepGroupList(t, []string{"engineers", "Scientists"}), + testAccStepGroupList(t, []string{"engineers", "admin_staff"}), // Verify user mapping can be listed back - testAccStepUserList(t, []string{"tesla"}), + testAccStepUserList(t, []string{"hermes conrad"}), }, }) } func TestBackend_basic_noPolicies(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t), + testAccStepConfigUrl(t, cfg), // Create LDAP user - testAccStepUser(t, "tesla", ""), + testAccStepUser(t, "hermes conrad", ""), // Authenticate - testAccStepLoginNoAttachedPolicies(t, "tesla", "password"), - testAccStepUserList(t, []string{"tesla"}), + testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), + testAccStepUserList(t, []string{"hermes conrad"}), }, }) } func TestBackend_basic_group_noPolicies(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() + logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrl(t), + testAccStepConfigUrl(t, cfg), // Create engineers group with no policies testAccStepGroup(t, "engineers", ""), - // Map tesla user with local engineers group - testAccStepUser(t, "tesla", "engineers"), + // Map hermes conrad user with local engineers group + testAccStepUser(t, "hermes conrad", "engineers"), // Authenticate - testAccStepLoginNoAttachedPolicies(t, "tesla", "password"), + testAccStepLoginNoAttachedPolicies(t, "hermes conrad", "hermes"), // Verify group mapping can be listed back testAccStepGroupList(t, []string{"engineers"}), }, @@ -467,45 +480,51 @@ func TestBackend_basic_group_noPolicies(t *testing.T) { func TestBackend_basic_authbind(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithAuthBind(t), - testAccStepGroup(t, "Scientists", "foo"), + testAccStepConfigUrlWithAuthBind(t, cfg), + testAccStepGroup(t, "admin_staff", "foo"), testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "tesla", "engineers"), - testAccStepLogin(t, "tesla", "password"), + testAccStepUser(t, "hermes conrad", "engineers"), + testAccStepLogin(t, "hermes conrad", "hermes"), }, }) } func TestBackend_basic_discover(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrlWithDiscover(t), - testAccStepGroup(t, "Scientists", "foo"), + testAccStepConfigUrlWithDiscover(t, cfg), + testAccStepGroup(t, "admin_staff", "foo"), testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "tesla", "engineers"), - testAccStepLogin(t, "tesla", "password"), + testAccStepUser(t, "hermes conrad", "engineers"), + testAccStepLogin(t, "hermes conrad", "hermes"), }, }) } func TestBackend_basic_nogroupdn(t *testing.T) { b := factory(t) + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() logicaltest.Test(t, logicaltest.TestCase{ CredentialBackend: b, Steps: []logicaltest.TestStep{ - testAccStepConfigUrlNoGroupDN(t), - testAccStepGroup(t, "Scientists", "foo"), + testAccStepConfigUrlNoGroupDN(t, cfg), + testAccStepGroup(t, "admin_staff", "foo"), testAccStepGroup(t, "engineers", "bar"), - testAccStepUser(t, "tesla", "engineers"), - testAccStepLoginNoGroupDN(t, "tesla", "password"), + testAccStepUser(t, "hermes conrad", "engineers"), + testAccStepLoginNoGroupDN(t, "hermes conrad", "hermes"), }, }) } @@ -575,54 +594,55 @@ func TestBackend_configDefaultsAfterUpdate(t *testing.T) { }) } -func testAccStepConfigUrl(t *testing.T) logicaltest.TestStep { +func testAccStepConfigUrl(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, "case_sensitive_names": true, "token_policies": "abc,xyz", }, } } -func testAccStepConfigUrlWithAuthBind(t *testing.T) logicaltest.TestStep { +func testAccStepConfigUrlWithAuthBind(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ // In this test we also exercise multiple URL support - "url": "foobar://ldap.example.com,ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", - "bindpass": "password", + "url": "foobar://ldap.example.com," + cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, "case_sensitive_names": true, "token_policies": "abc,xyz", }, } } -func testAccStepConfigUrlWithDiscover(t *testing.T) logicaltest.TestStep { +func testAccStepConfigUrlWithDiscover(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, "discoverdn": true, "case_sensitive_names": true, "token_policies": "abc,xyz", @@ -630,16 +650,16 @@ func testAccStepConfigUrlWithDiscover(t *testing.T) logicaltest.TestStep { } } -func testAccStepConfigUrlNoGroupDN(t *testing.T) logicaltest.TestStep { +func testAccStepConfigUrlNoGroupDN(t *testing.T, cfg *ldaputil.ConfigEntry) logicaltest.TestStep { return logicaltest.TestStep{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - // Online LDAP test server - // http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, "discoverdn": true, "case_sensitive_names": true, }, @@ -760,7 +780,7 @@ func testAccStepLogin(t *testing.T, user string, pass string) logicaltest.TestSt }, Unauthenticated: true, - // Verifies user tesla maps to groups via local group (engineers) as well as remote group (Scientists) + // Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists) Check: logicaltest.TestCheckAuth([]string{"abc", "bar", "default", "foo", "xyz"}), } } @@ -774,7 +794,7 @@ func testAccStepLoginNoAttachedPolicies(t *testing.T, user string, pass string) }, Unauthenticated: true, - // Verifies user tesla maps to groups via local group (engineers) as well as remote group (Scientists) + // Verifies user hermes conrad maps to groups via local group (engineers) as well as remote group (Scientists) Check: logicaltest.TestCheckAuth([]string{"abc", "default", "xyz"}), } } @@ -856,16 +876,19 @@ func TestLdapAuthBackend_ConfigUpgrade(t *testing.T) { ctx := context.Background() - // Write in some initial config + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() configReq := &logical.Request{ Operation: logical.UpdateOperation, Path: "config", Data: map[string]interface{}{ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, "token_period": "5m", "token_explicit_max_ttl": "24h", }, @@ -894,14 +917,15 @@ func TestLdapAuthBackend_ConfigUpgrade(t *testing.T) { TokenExplicitMaxTTL: 24 * time.Hour, }, ConfigEntry: &ldaputil.ConfigEntry{ - Url: "ldap://ldap.forumsys.com", - UserAttr: "uid", - UserDN: "dc=example,dc=com", - GroupDN: "dc=example,dc=com", - BindDN: "cn=read-only-admin,dc=example,dc=com", + Url: cfg.Url, + UserAttr: cfg.UserAttr, + UserDN: cfg.UserDN, + GroupDN: cfg.GroupDN, + GroupAttr: cfg.GroupAttr, + BindDN: cfg.BindDN, + BindPassword: cfg.BindPassword, GroupFilter: defParams.GroupFilter, DenyNullBind: defParams.DenyNullBind, - GroupAttr: defParams.GroupAttr, TLSMinVersion: defParams.TLSMinVersion, TLSMaxVersion: defParams.TLSMaxVersion, CaseSensitiveNames: falseBool, diff --git a/go.mod b/go.mod index 2bacaa87e88a..d4b28e3a2c76 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 github.com/go-errors/errors v1.0.1 + github.com/go-ldap/ldap v3.0.2+incompatible github.com/go-sql-driver/mysql v1.4.1 github.com/go-test/deep v1.0.2 github.com/gocql/gocql v0.0.0-20190402132108-0e1d5de854df diff --git a/helper/testhelpers/ldap/ldaphelper.go b/helper/testhelpers/ldap/ldaphelper.go new file mode 100644 index 000000000000..5322684feac2 --- /dev/null +++ b/helper/testhelpers/ldap/ldaphelper.go @@ -0,0 +1,66 @@ +package ldap + +import ( + "fmt" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/helper/testhelpers/docker" + "github.com/hashicorp/vault/sdk/helper/ldaputil" + "github.com/ory/dockertest" + "testing" +) + +func PrepareTestContainer(t *testing.T, version string) (cleanup func(), cfg *ldaputil.ConfigEntry) { + pool, err := dockertest.NewPool("") + if err != nil { + t.Fatalf("Failed to connect to docker: %s", err) + } + + dockerOptions := &dockertest.RunOptions{ + Repository: "rroemhild/test-openldap", + Tag: version, + Privileged: true, + //Env: []string{"LDAP_DEBUG_LEVEL=384"}, + } + resource, err := pool.RunWithOptions(dockerOptions) + if err != nil { + t.Fatalf("Could not start local LDAP %s docker container: %s", version, err) + } + + cleanup = func() { + docker.CleanupResource(t, pool, resource) + } + + //pool.MaxWait = time.Second + // exponential backoff-retry + if err = pool.Retry(func() error { + logger := hclog.New(nil) + client := ldaputil.Client{ + LDAP: ldaputil.NewLDAP(), + Logger: logger, + } + + cfg = new(ldaputil.ConfigEntry) + cfg.Url = fmt.Sprintf("ldap://localhost:%s", resource.GetPort("389/tcp")) + cfg.UserDN = "ou=people,dc=planetexpress,dc=com" + cfg.UserAttr = "cn" + cfg.BindDN = "cn=admin,dc=planetexpress,dc=com" + cfg.BindPassword = "GoodNewsEveryone" + cfg.GroupDN = "ou=people,dc=planetexpress,dc=com" + cfg.GroupAttr = "memberOf" + conn, err := client.DialLDAP(cfg) + if err != nil { + return err + } + defer conn.Close() + + if _, err := client.GetUserBindDN(cfg, conn, "Philip J. Fry"); err != nil { + return err + } + return nil + }); err != nil { + cleanup() + t.Fatalf("Could not connect to docker: %s", err) + } + + return cleanup, cfg +} diff --git a/vault/external_tests/identity/groups_test.go b/vault/external_tests/identity/groups_test.go index 90996ee4190c..12dd29a51f55 100644 --- a/vault/external_tests/identity/groups_test.go +++ b/vault/external_tests/identity/groups_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/hashicorp/vault/api" + ldaphelper "github.com/hashicorp/vault/helper/testhelpers/ldap" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -178,13 +179,18 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } ldapMountAccessor1 := auths["ldap/"].Accessor + cleanup, cfg := ldaphelper.PrepareTestContainer(t, "latest") + defer cleanup() + // Configure LDAP auth - _, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + secret, err := client.Logical().Write("auth/ldap/config", map[string]interface{}{ + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, }) if err != nil { t.Fatal(err) @@ -199,7 +205,7 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } // Tie the group to a user - _, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{ + _, err = client.Logical().Write("auth/ldap/users/hermes conrad", map[string]interface{}{ "policies": "default", "groups": "testgroup1", }) @@ -208,7 +214,7 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } // Create an external group - secret, err := client.Logical().Write("identity/group", map[string]interface{}{ + secret, err = client.Logical().Write("identity/group", map[string]interface{}{ "type": "external", }) if err != nil { @@ -227,8 +233,8 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } // Login using LDAP - secret, err = client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{ - "password": "password", + secret, err = client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{ + "password": "hermes", }) if err != nil { t.Fatal(err) @@ -264,10 +270,10 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } ldapMountAccessor2 := auths["ldap2/"].Accessor - // Create an entity-alias asserting that the user "tesla" from the first + // Create an entity-alias asserting that the user "hermes conrad" from the first // and second LDAP mounts as the same. _, err = client.Logical().Write("identity/entity-alias", map[string]interface{}{ - "name": "tesla", + "name": "hermes conrad", "mount_accessor": ldapMountAccessor2, "canonical_id": entityID, }) @@ -275,13 +281,18 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { t.Fatal(err) } - // Configure second LDAP auth - _, err = client.Logical().Write("auth/ldap2/config", map[string]interface{}{ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + cleanup2, cfg2 := ldaphelper.PrepareTestContainer(t, "latest") + defer cleanup2() + + // Configure LDAP auth + secret, err = client.Logical().Write("auth/ldap2/config", map[string]interface{}{ + "url": cfg2.Url, + "userattr": cfg2.UserAttr, + "userdn": cfg2.UserDN, + "groupdn": cfg2.GroupDN, + "groupattr": cfg2.GroupAttr, + "binddn": cfg2.BindDN, + "bindpass": cfg2.BindPassword, }) if err != nil { t.Fatal(err) @@ -296,7 +307,7 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } // Create a user in second LDAP auth - _, err = client.Logical().Write("auth/ldap2/users/tesla", map[string]interface{}{ + _, err = client.Logical().Write("auth/ldap2/users/hermes conrad", map[string]interface{}{ "policies": "default", "groups": "testgroup2", }) @@ -324,8 +335,8 @@ func TestIdentityStore_ExternalGroupMembershipsAcrossMounts(t *testing.T) { } // Login using second LDAP - _, err = client.Logical().Write("auth/ldap2/login/tesla", map[string]interface{}{ - "password": "password", + _, err = client.Logical().Write("auth/ldap2/login/hermes conrad", map[string]interface{}{ + "password": "hermes", }) if err != nil { t.Fatal(err) diff --git a/vault/external_tests/identity/identity_test.go b/vault/external_tests/identity/identity_test.go index 8b499cb03414..f82f061cc69e 100644 --- a/vault/external_tests/identity/identity_test.go +++ b/vault/external_tests/identity/identity_test.go @@ -1,12 +1,15 @@ package identity import ( + "github.com/go-ldap/ldap" + "github.com/hashicorp/vault/sdk/helper/ldaputil" "testing" log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/builtin/credential/ldap" + ldapcred "github.com/hashicorp/vault/builtin/credential/ldap" "github.com/hashicorp/vault/helper/namespace" + ldaphelper "github.com/hashicorp/vault/helper/testhelpers/ldap" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" @@ -19,7 +22,7 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { DisableCache: true, Logger: log.NewNullLogger(), CredentialBackends: map[string]logical.Factory{ - "ldap": ldap.Factory, + "ldap": ldapcred.Factory, }, } @@ -52,21 +55,21 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { secret, err := client.Logical().Write("identity/group", map[string]interface{}{ "type": "external", - "name": "ldap_Italians", + "name": "ldap_ship_crew", }) if err != nil { t.Fatal(err) } - italiansGroupID := secret.Data["id"].(string) + shipCrewGroupID := secret.Data["id"].(string) secret, err = client.Logical().Write("identity/group", map[string]interface{}{ "type": "external", - "name": "ldap_Scientists", + "name": "ldap_admin_staff", }) if err != nil { t.Fatal(err) } - scientistsGroupID := secret.Data["id"].(string) + adminStaffGroupID := secret.Data["id"].(string) secret, err = client.Logical().Write("identity/group", map[string]interface{}{ "type": "external", @@ -78,8 +81,8 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { devopsGroupID := secret.Data["id"].(string) secret, err = client.Logical().Write("identity/group-alias", map[string]interface{}{ - "name": "Italians", - "canonical_id": italiansGroupID, + "name": "ship_crew", + "canonical_id": shipCrewGroupID, "mount_accessor": accessor, }) if err != nil { @@ -87,8 +90,8 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { } secret, err = client.Logical().Write("identity/group-alias", map[string]interface{}{ - "name": "Scientists", - "canonical_id": scientistsGroupID, + "name": "admin_staff", + "canonical_id": adminStaffGroupID, "mount_accessor": accessor, }) if err != nil { @@ -104,35 +107,40 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { t.Fatal(err) } - secret, err = client.Logical().Read("identity/group/id/" + italiansGroupID) + secret, err = client.Logical().Read("identity/group/id/" + shipCrewGroupID) if err != nil { t.Fatal(err) } aliasMap := secret.Data["alias"].(map[string]interface{}) - if aliasMap["canonical_id"] != italiansGroupID || - aliasMap["name"] != "Italians" || + if aliasMap["canonical_id"] != shipCrewGroupID || + aliasMap["name"] != "ship_crew" || aliasMap["mount_accessor"] != accessor { t.Fatalf("bad: group alias: %#v\n", aliasMap) } - secret, err = client.Logical().Read("identity/group/id/" + scientistsGroupID) + secret, err = client.Logical().Read("identity/group/id/" + adminStaffGroupID) if err != nil { t.Fatal(err) } aliasMap = secret.Data["alias"].(map[string]interface{}) - if aliasMap["canonical_id"] != scientistsGroupID || - aliasMap["name"] != "Scientists" || + if aliasMap["canonical_id"] != adminStaffGroupID || + aliasMap["name"] != "admin_staff" || aliasMap["mount_accessor"] != accessor { t.Fatalf("bad: group alias: %#v\n", aliasMap) } - // Configure LDAP auth backend + cleanup, cfg := ldaphelper.PrepareTestContainer(t, "latest") + defer cleanup() + + // Configure LDAP auth secret, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, }) if err != nil { t.Fatal(err) @@ -155,7 +163,7 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { } // Create a local user in LDAP - secret, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{ + secret, err = client.Logical().Write("auth/ldap/users/hermes conrad", map[string]interface{}{ "policies": "default", "groups": "engineers,devops", }) @@ -164,8 +172,8 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { } // Login with LDAP and create a token - secret, err = client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{ - "password": "password", + secret, err = client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{ + "password": "hermes", }) if err != nil { t.Fatal(err) @@ -179,56 +187,80 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { } entityID := secret.Data["entity_id"].(string) - // Re-read the Scientists, Italians and devops group. This entity ID should have - // been added to both of these groups by now. - secret, err = client.Logical().Read("identity/group/id/" + italiansGroupID) - if err != nil { - t.Fatal(err) - } - groupMap := secret.Data - found := false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true + // Re-read the admin_staff, ship_crew and devops group. This entity ID should have + // been added to admin_staff but not ship_crew. + assertMember := func(groupName, groupID string, expectFound bool) { + secret, err = client.Logical().Read("identity/group/id/" + groupID) + if err != nil { + t.Fatal(err) + } + groupMap := secret.Data + found := false + for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { + if entityIDRaw.(string) == entityID { + found = true + } + } + if found != expectFound { + negation := "" + if !expectFound { + negation = "not " + } + t.Fatalf("expected entity ID %q to %sbe part of %q group", entityID, negation, groupName) } - } - if !found { - t.Fatalf("expected entity ID %q to be part of Italians group", entityID) } - secret, err = client.Logical().Read("identity/group/id/" + scientistsGroupID) - if err != nil { - t.Fatal(err) - } - groupMap = secret.Data - found = false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true + assertMember("ship_crew", shipCrewGroupID, false) + assertMember("admin_staff", adminStaffGroupID, true) + assertMember("devops", devopsGroupID, true) + assertMember("engineer", devopsGroupID, true) + + // Now add Hermes to ship_crew + { + logger := log.New(nil) + ldapClient := ldaputil.Client{LDAP: ldaputil.NewLDAP(), Logger: logger} + // LDAP server won't accept changes unless we connect with TLS. This + // isn't the default config returned by PrepareTestContainer because + // the Vault LDAP backend won't work with it, even with InsecureTLS, + // because the ServerName should be planetexpress.com and not localhost. + conn, err := ldapClient.DialLDAP(cfg) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + + err = conn.Bind(cfg.BindDN, cfg.BindPassword) + if err != nil { + t.Fatal(err) + } + + hermesDn := "cn=Hermes Conrad,ou=people,dc=planetexpress,dc=com" + shipCrewDn := "cn=ship_crew,ou=people,dc=planetexpress,dc=com" + ldapreq := ldap.ModifyRequest{DN: shipCrewDn} + ldapreq.Add("member", []string{hermesDn}) + err = conn.Modify(&ldapreq) + if err != nil { + t.Fatal(err) } - } - if !found { - t.Fatalf("expected entity ID %q to be part of Scientists group", entityID) } - secret, err = client.Logical().Read("identity/group/id/" + devopsGroupID) + // Re-login with LDAP + secret, err = client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{ + "password": "hermes", + }) if err != nil { t.Fatal(err) } - groupMap = secret.Data - found = false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true - } - } - if !found { - t.Fatalf("expected entity ID %q to be part of devops group", entityID) - } + + // Hermes should now be in ship_crew external group + assertMember("ship_crew", shipCrewGroupID, true) + assertMember("admin_staff", adminStaffGroupID, true) + assertMember("devops", devopsGroupID, true) + assertMember("engineer", devopsGroupID, true) identityStore := cores[0].IdentityStore() - group, err := identityStore.MemDBGroupByID(italiansGroupID, true) + group, err := identityStore.MemDBGroupByID(shipCrewGroupID, true) if err != nil { t.Fatal(err) } @@ -243,7 +275,7 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { t.Fatal(err) } - group, err = identityStore.MemDBGroupByID(italiansGroupID, true) + group, err = identityStore.MemDBGroupByID(shipCrewGroupID, true) if err != nil { t.Fatal(err) } @@ -251,7 +283,7 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { t.Fatalf("failed to remove entity ID from the group") } - group, err = identityStore.MemDBGroupByID(scientistsGroupID, true) + group, err = identityStore.MemDBGroupByID(adminStaffGroupID, true) if err != nil { t.Fatal(err) } @@ -264,7 +296,7 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { t.Fatal(err) } - group, err = identityStore.MemDBGroupByID(scientistsGroupID, true) + group, err = identityStore.MemDBGroupByID(adminStaffGroupID, true) if err != nil { t.Fatal(err) } @@ -298,55 +330,13 @@ func TestIdentityStore_Integ_GroupAliases(t *testing.T) { t.Fatal(err) } - // EntityIDs should have been added to the groups again during renewal - secret, err = client.Logical().Read("identity/group/id/" + italiansGroupID) - if err != nil { - t.Fatal(err) - } - groupMap = secret.Data - found = false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true - } - } - if !found { - t.Fatalf("expected entity ID %q to be part of Italians group", entityID) - } - - secret, err = client.Logical().Read("identity/group/id/" + scientistsGroupID) - if err != nil { - t.Fatal(err) - } - groupMap = secret.Data - found = false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true - } - } - if !found { - t.Fatalf("expected entity ID %q to be part of scientists group", entityID) - } - - secret, err = client.Logical().Read("identity/group/id/" + devopsGroupID) - if err != nil { - t.Fatal(err) - } - - groupMap = secret.Data - found = false - for _, entityIDRaw := range groupMap["member_entity_ids"].([]interface{}) { - if entityIDRaw.(string) == entityID { - found = true - } - } - if !found { - t.Fatalf("expected entity ID %q to be part of devops group", entityID) - } + assertMember("ship_crew", shipCrewGroupID, true) + assertMember("admin_staff", adminStaffGroupID, true) + assertMember("devops", devopsGroupID, true) + assertMember("engineer", devopsGroupID, true) - // Remove user tesla from the devops group in LDAP backend - secret, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{ + // Remove user hermes conrad from the devops group in LDAP backend + secret, err = client.Logical().Write("auth/ldap/users/hermes conrad", map[string]interface{}{ "policies": "default", "groups": "engineers", }) diff --git a/vault/external_tests/token/token_test.go b/vault/external_tests/token/token_test.go index ac0582ffa6ca..7766095155b4 100644 --- a/vault/external_tests/token/token_test.go +++ b/vault/external_tests/token/token_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/vault/api" credLdap "github.com/hashicorp/vault/builtin/credential/ldap" credUserpass "github.com/hashicorp/vault/builtin/credential/userpass" + "github.com/hashicorp/vault/helper/testhelpers/ldap" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/logical" @@ -127,13 +128,18 @@ func TestTokenStore_IdentityPolicies(t *testing.T) { t.Fatal(err) } + cleanup, cfg := ldap.PrepareTestContainer(t, "latest") + defer cleanup() + // Configure LDAP auth _, err = client.Logical().Write("auth/ldap/config", map[string]interface{}{ - "url": "ldap://ldap.forumsys.com", - "userattr": "uid", - "userdn": "dc=example,dc=com", - "groupdn": "dc=example,dc=com", - "binddn": "cn=read-only-admin,dc=example,dc=com", + "url": cfg.Url, + "userattr": cfg.UserAttr, + "userdn": cfg.UserDN, + "groupdn": cfg.GroupDN, + "groupattr": cfg.GroupAttr, + "binddn": cfg.BindDN, + "bindpass": cfg.BindPassword, }) if err != nil { t.Fatal(err) @@ -149,7 +155,7 @@ func TestTokenStore_IdentityPolicies(t *testing.T) { // Create user in LDAP auth. We add two groups, but we should filter out // the ones that don't match aliases later (we will check for this) - _, err = client.Logical().Write("auth/ldap/users/tesla", map[string]interface{}{ + _, err = client.Logical().Write("auth/ldap/users/hermes conrad", map[string]interface{}{ "policies": "default", "groups": "testgroup1,testgroup2", }) @@ -158,8 +164,8 @@ func TestTokenStore_IdentityPolicies(t *testing.T) { } // Login using LDAP - secret, err := client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{ - "password": "password", + secret, err := client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{ + "password": "hermes", }) if err != nil { t.Fatal(err) @@ -327,8 +333,8 @@ func TestTokenStore_IdentityPolicies(t *testing.T) { // Log in and get a new token, then renew it. See issue #4829. The logic is // continued after the next block. - secret, err = client.Logical().Write("auth/ldap/login/tesla", map[string]interface{}{ - "password": "password", + secret, err = client.Logical().Write("auth/ldap/login/hermes conrad", map[string]interface{}{ + "password": "hermes", }) if err != nil { t.Fatal(err) @@ -338,12 +344,12 @@ func TestTokenStore_IdentityPolicies(t *testing.T) { // Check that the lease for the token contains only the single group; this // should be true for both as one was fresh and the other was a renew // (which is why we do the renew check on the 4839 token after this block) - secret, err = client.Logical().List("sys/raw/sys/expire/id/auth/ldap/login/tesla/") + secret, err = client.Logical().List("sys/raw/sys/expire/id/auth/ldap/login/hermes conrad/") if err != nil { t.Fatal(err) } for _, key := range secret.Data["keys"].([]interface{}) { - secret, err := client.Logical().Read("sys/raw/sys/expire/id/auth/ldap/login/tesla/" + key.(string)) + secret, err := client.Logical().Read("sys/raw/sys/expire/id/auth/ldap/login/hermes conrad/" + key.(string)) if err != nil { t.Fatal(err) } From e71d856c34484b209225f25e850ea624d8b11f87 Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Tue, 22 Oct 2019 10:42:56 -0700 Subject: [PATCH 06/14] agent: fix data race on inmemSink's token (#7707) * agent: fix data race on inmemSink's token * use uber/atomic instead --- command/agent/sink/inmem/inmem_sink.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/command/agent/sink/inmem/inmem_sink.go b/command/agent/sink/inmem/inmem_sink.go index d382eb805420..2dfa09115ca7 100644 --- a/command/agent/sink/inmem/inmem_sink.go +++ b/command/agent/sink/inmem/inmem_sink.go @@ -6,13 +6,14 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/command/agent/cache" "github.com/hashicorp/vault/command/agent/sink" + "go.uber.org/atomic" ) // inmemSink retains the auto-auth token in memory and exposes it via // sink.SinkReader interface. type inmemSink struct { logger hclog.Logger - token string + token *atomic.String leaseCache *cache.LeaseCache } @@ -25,11 +26,12 @@ func New(conf *sink.SinkConfig, leaseCache *cache.LeaseCache) (sink.Sink, error) return &inmemSink{ logger: conf.Logger, leaseCache: leaseCache, + token: atomic.NewString(""), }, nil } func (s *inmemSink) WriteToken(token string) error { - s.token = token + s.token.Store(token) if s.leaseCache != nil { s.leaseCache.RegisterAutoAuthToken(token) @@ -39,5 +41,5 @@ func (s *inmemSink) WriteToken(token string) error { } func (s *inmemSink) Token() string { - return s.token + return s.token.Load() } From 3019f1932925969bd212b64b4a9936d7121c190b Mon Sep 17 00:00:00 2001 From: Calvin Leung Huang Date: Tue, 22 Oct 2019 10:44:26 -0700 Subject: [PATCH 07/14] changelog++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6539431053c1..c277edaeb813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,7 @@ IMPROVEMENTS: BUG FIXES: + * agent: Fix a data race on the token value for inmemsink [GH-7707] * auth/gcp: Fix a bug where region information in instance groups names could cause an authorization attempt to fail [GCP-74] * cli: Fix a bug where a token of an unknown format (e.g. in ~/.vault-token) From b3a2ab5ac3cd7496846f6e381372e21520b02019 Mon Sep 17 00:00:00 2001 From: Amitosh Swain Mahapatra Date: Tue, 22 Oct 2019 22:45:20 +0000 Subject: [PATCH 08/14] Show versions that are active when delete_version_after is configured (#7685) --- ui/app/models/secret-v2-version.js | 8 +++-- .../unit/models/secret-v2-version-test.js | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 ui/tests/unit/models/secret-v2-version-test.js diff --git a/ui/app/models/secret-v2-version.js b/ui/app/models/secret-v2-version.js index 442f79e4f50d..4ad9c11dc1a2 100644 --- a/ui/app/models/secret-v2-version.js +++ b/ui/app/models/secret-v2-version.js @@ -1,6 +1,6 @@ import Secret from './secret'; import DS from 'ember-data'; -import { bool } from '@ember/object/computed'; +import { computed } from '@ember/object'; const { attr, belongsTo } = DS; @@ -12,7 +12,11 @@ export default Secret.extend({ path: attr('string'), deletionTime: attr('string'), createdTime: attr('string'), - deleted: bool('deletionTime'), + deleted: computed('deletionTime', function() { + const deletionTime = new Date(this.get('deletionTime')); + const now = new Date(); + return deletionTime <= now; + }), destroyed: attr('boolean'), currentVersion: attr('number'), }); diff --git a/ui/tests/unit/models/secret-v2-version-test.js b/ui/tests/unit/models/secret-v2-version-test.js new file mode 100644 index 000000000000..d9dfb0148ac1 --- /dev/null +++ b/ui/tests/unit/models/secret-v2-version-test.js @@ -0,0 +1,31 @@ +import { run } from '@ember/runloop'; +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Model | secret-v2-version', function(hooks) { + setupTest(hooks); + + test('deleted is true for a past deletionTime', function(assert) { + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-v2-version', { + deletionTime: '2000-10-14T00:00:00.000000Z', + }) + ); + assert.equal(model.get('deleted'), true); + }); + }); + + test('deleted is false for a future deletionTime', function(assert) { + let model; + run(() => { + model = run(() => + this.owner.lookup('service:store').createRecord('secret-v2-version', { + deletionTime: '2999-10-14T00:00:00.000000Z', + }) + ); + assert.equal(model.get('deleted'), false); + }); + }); +}); From ae741402e38b1435c8edde12961cbf442081d9b9 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Wed, 23 Oct 2019 10:26:11 -0400 Subject: [PATCH 09/14] Update transit docs to add aes128/p384/p521 information (#7718) --- website/source/api/secret/transit/index.html.md | 6 +++++- website/source/docs/secrets/transit/index.html.md | 10 ++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/website/source/api/secret/transit/index.html.md b/website/source/api/secret/transit/index.html.md index e83cf3cbf155..f0ae62c87619 100644 --- a/website/source/api/secret/transit/index.html.md +++ b/website/source/api/secret/transit/index.html.md @@ -51,14 +51,18 @@ values set here cannot be changed after key creation. - `type` `(string: "aes256-gcm96")` – Specifies the type of key to create. The currently-supported types are: - - `aes256-gcm96` – AES-256 wrapped with GCM using a 96-bit nonce size AEAD + - `aes128-gcm96` – AES-128 wrapped with GCM using a 96-bit nonce size AEAD (symmetric, supports derivation and convergent encryption) + - `aes256-gcm96` – AES-256 wrapped with GCM using a 96-bit nonce size AEAD + (symmetric, supports derivation and convergent encryption, default) - `chacha20-poly1305` – ChaCha20-Poly1305 AEAD (symmetric, supports derivation and convergent encryption) - `ed25519` – ED25519 (asymmetric, supports derivation). When using derivation, a sign operation with the same context will derive the same key and signature; this is a signing analogue to `convergent_encryption`. - `ecdsa-p256` – ECDSA using the P-256 elliptic curve (asymmetric) + - `ecdsa-p384` – ECDSA using the P-384 elliptic curve (asymmetric) + - `ecdsa-p521` – ECDSA using the P-521 elliptic curve (asymmetric) - `rsa-2048` - RSA with bit size of 2048 (asymmetric) - `rsa-4096` - RSA with bit size of 4096 (asymmetric) diff --git a/website/source/docs/secrets/transit/index.html.md b/website/source/docs/secrets/transit/index.html.md index b0bb3c784b1a..20998ded728a 100644 --- a/website/source/docs/secrets/transit/index.html.md +++ b/website/source/docs/secrets/transit/index.html.md @@ -55,13 +55,19 @@ time. As of now, the transit secrets engine supports the following key types (all key types also generate separate HMAC keys): -* `aes256-gcm96`: AES-GCM with a 256-bit AES key and a 96-bit nonce; supports +* `aes128-gcm96`: AES-GCM with a 128-bit AES key and a 96-bit nonce; supports encryption, decryption, key derivation, and convergent encryption +* `aes256-gcm96`: AES-GCM with a 256-bit AES key and a 96-bit nonce; supports + encryption, decryption, key derivation, and convergent encryption (default) * `chacha20-poly1305`: ChaCha20-Poly1305 with a 256-bit key; supports encryption, decryption, key derivation, and convergent encryption * `ed25519`: Ed25519; supports signing, signature verification, and key derivation -* `ecdsa-p256`: ECDSA using curve P256; supports signing and signature +* `ecdsa-p256`: ECDSA using curve P-256; supports signing and signature + verification +* `ecdsa-p384`: ECDSA using curve P-384; supports signing and signature + verification +* `ecdsa-p521`: ECDSA using curve P-521; supports signing and signature verification * `rsa-2048`: 2048-bit RSA key; supports encryption, decryption, signing, and signature verification From d251fbfedf162e08da2aec034171a84bd9b87566 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Wed, 23 Oct 2019 10:49:43 -0400 Subject: [PATCH 10/14] changelog++ --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c277edaeb813..ef3d26f79c3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -109,6 +109,7 @@ CHANGES: IMPROVEMENTS: * cli: Ignore existing token during CLI login [GH-7508] * core: Log proxy settings from environment on startup [GH-7528] + * core: Cache whether we've been initialized to reduce load on storage [GH-7549] BUG FIXES: From 2f65be45379631fcb65f7556995c642136886b17 Mon Sep 17 00:00:00 2001 From: Michael Gaffney Date: Wed, 23 Oct 2019 11:29:53 -0400 Subject: [PATCH 11/14] Changelog: clarify enterprise seal migration fix --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef3d26f79c3f..e848dc9adfd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,7 +116,8 @@ BUG FIXES: * agent: Fix handling of gzipped responses [GH-7470] * cli: Fix panic when pgp keys list is empty [GH-7546] * core: add hook for initializing seals for migration [GH-7666] - * core (enterprise): Fix seal migration in enterprise + * core (enterprise): Migrating from one auto unseal method to another never + worked on enterprise, now it does. * identity: Add required field `response_types_supported` to identity token `.well-known/openid-configuration` response [GH-7533] * secrets/database: Fix bug in combined DB secrets engine that can result in From fe8cfc178d8d56e6b535a4fc038df56e2448d4ca Mon Sep 17 00:00:00 2001 From: Vishal Nayak Date: Wed, 23 Oct 2019 12:52:28 -0400 Subject: [PATCH 12/14] Abstract generate-root authentication into the strategy interface (#7698) * Abstract generate-root authentication into the strategy interface * Generate root strategy ncabatoff (#7700) * Adapt to new shamir-as-kek reality. * Don't try to verify the master key when we might still be sealed (in recovery mode). Instead, verify it in the authenticate methods. --- vault/core.go | 47 +++++++++++++++++++ vault/generate_root.go | 82 +++++++-------------------------- vault/generate_root_recovery.go | 21 ++++++++- 3 files changed, 83 insertions(+), 67 deletions(-) diff --git a/vault/core.go b/vault/core.go index 9d9fc0029514..644c6243cf43 100644 --- a/vault/core.go +++ b/vault/core.go @@ -2041,6 +2041,53 @@ func (c *Core) SetSealsForMigration(migrationSeal, newSeal, unwrapSeal Seal) { } } +// unsealKeyToMasterKey takes a key provided by the user, either a recovery key +// if using an autoseal or an unseal key with Shamir. It returns a nil error +// if the key is valid and an error otherwise. It also returns the master key +// that can be used to unseal the barrier. +func (c *Core) unsealKeyToMasterKey(ctx context.Context, combinedKey []byte) ([]byte, error) { + switch c.seal.StoredKeysSupported() { + case StoredKeysSupportedGeneric: + if err := c.seal.VerifyRecoveryKey(ctx, combinedKey); err != nil { + return nil, errwrap.Wrapf("recovery key verification failed: {{err}}", err) + } + + storedKeys, err := c.seal.GetStoredKeys(ctx) + if err == nil && len(storedKeys) != 1 { + err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys)) + } + if err != nil { + return nil, errwrap.Wrapf("unable to retrieve stored keys", err) + } + return storedKeys[0], nil + + case StoredKeysSupportedShamirMaster: + testseal := NewDefaultSeal(shamirseal.NewSeal(c.logger.Named("testseal"))) + testseal.SetCore(c) + cfg, err := c.seal.BarrierConfig(ctx) + if err != nil { + return nil, errwrap.Wrapf("failed to setup test barrier config: {{err}}", err) + } + testseal.SetCachedBarrierConfig(cfg) + err = testseal.GetAccess().(*shamirseal.ShamirSeal).SetKey(combinedKey) + if err != nil { + return nil, errwrap.Wrapf("failed to setup unseal key: {{err}}", err) + } + storedKeys, err := testseal.GetStoredKeys(ctx) + if err == nil && len(storedKeys) != 1 { + err = fmt.Errorf("expected exactly one stored key, got %d", len(storedKeys)) + } + if err != nil { + return nil, errwrap.Wrapf("unable to retrieve stored keys", err) + } + return storedKeys[0], nil + + case StoredKeysNotSupported: + return combinedKey, nil + } + return nil, fmt.Errorf("invalid seal") +} + func (c *Core) IsInSealMigration() bool { c.stateLock.RLock() defer c.stateLock.RUnlock() diff --git a/vault/generate_root.go b/vault/generate_root.go index 56033fdb4df3..c0521d30ece9 100644 --- a/vault/generate_root.go +++ b/vault/generate_root.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/vault/helper/xor" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/shamir" - shamirseal "github.com/hashicorp/vault/vault/seal/shamir" ) const coreDROperationTokenPath = "core/dr-operation-token" @@ -32,12 +31,25 @@ var ( // create a token upon completion of the generate root process. type GenerateRootStrategy interface { generate(context.Context, *Core) (string, func(), error) + authenticate(context.Context, *Core, []byte) error } // generateStandardRootToken implements the GenerateRootStrategy and is in // charge of creating standard root tokens. type generateStandardRootToken struct{} +func (g generateStandardRootToken) authenticate(ctx context.Context, c *Core, combinedKey []byte) error { + _, err := c.unsealKeyToMasterKey(ctx, combinedKey) + if err != nil { + return errwrap.Wrapf("unable to authenticate: {{err}}", err) + } + if err := c.barrier.VerifyMaster(combinedKey); err != nil { + return errwrap.Wrapf("master key verification failed: {{err}}", err) + } + + return nil +} + func (g generateStandardRootToken) generate(ctx context.Context, c *Core) (string, func(), error) { te, err := c.tokenStore.rootToken(ctx) if err != nil { @@ -296,71 +308,9 @@ func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, } } - switch { - case c.seal.RecoveryKeySupported(): - // Ensure that the combined recovery key is valid - if err := c.seal.VerifyRecoveryKey(ctx, combinedKey); err != nil { - c.logger.Error("root generation aborted, recovery key verification failed", "error", err) - return nil, err - } - // If we are in recovery mode, then retrieve - // the stored keys and unseal the barrier - if c.recoveryMode { - storedKeys, err := c.seal.GetStoredKeys(ctx) - if err != nil { - return nil, errwrap.Wrapf("unable to retrieve stored keys in recovery mode: {{err}}", err) - } - - // Use the retrieved master key to unseal the barrier - if err := c.barrier.Unseal(ctx, storedKeys[0]); err != nil { - c.logger.Error("root generation aborted, recovery operation token verification failed", "error", err) - return nil, err - } - } - default: - masterKey := combinedKey - if c.seal.StoredKeysSupported() == StoredKeysSupportedShamirMaster { - testseal := NewDefaultSeal(shamirseal.NewSeal(c.logger.Named("testseal"))) - testseal.SetCore(c) - cfg, err := c.seal.BarrierConfig(ctx) - if err != nil { - return nil, errwrap.Wrapf("failed to setup test barrier config: {{err}}", err) - } - testseal.SetCachedBarrierConfig(cfg) - err = testseal.GetAccess().(*shamirseal.ShamirSeal).SetKey(combinedKey) - if err != nil { - return nil, errwrap.Wrapf("failed to setup unseal key: {{err}}", err) - } - stored, err := testseal.GetStoredKeys(ctx) - if err != nil { - return nil, errwrap.Wrapf("failed to read master key: {{err}}", err) - } - masterKey = stored[0] - } - switch { - case c.recoveryMode: - // If we are in recovery mode, being able to unseal - // the barrier is how we establish authentication - if err := c.barrier.Unseal(ctx, masterKey); err != nil { - c.logger.Error("root generation aborted, recovery operation token verification failed", "error", err) - return nil, err - } - default: - if err := c.barrier.VerifyMaster(masterKey); err != nil { - c.logger.Error("root generation aborted, master key verification failed", "error", err) - return nil, err - } - } - } - - // Authentication in recovery mode is successful - if c.recoveryMode { - // Run any post unseal functions that are set - for _, v := range c.postRecoveryUnsealFuncs { - if err := v(); err != nil { - return nil, errwrap.Wrapf("failed to run post unseal func: {{err}}", err) - } - } + if err := strategy.authenticate(ctx, c, combinedKey); err != nil { + c.logger.Error("root generation aborted", "error", err.Error()) + return nil, errwrap.Wrapf("root generation aborted: {{err}}", err) } // Run the generate strategy diff --git a/vault/generate_root_recovery.go b/vault/generate_root_recovery.go index 73b9c2c336a9..e19c12ca0b56 100644 --- a/vault/generate_root_recovery.go +++ b/vault/generate_root_recovery.go @@ -2,7 +2,7 @@ package vault import ( "context" - + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/sdk/helper/base62" "go.uber.org/atomic" ) @@ -19,6 +19,25 @@ type generateRecoveryToken struct { token *atomic.String } +func (g *generateRecoveryToken) authenticate(ctx context.Context, c *Core, combinedKey []byte) error { + key, err := c.unsealKeyToMasterKey(ctx, combinedKey) + if err != nil { + return errwrap.Wrapf("unable to authenticate: {{err}}", err) + } + + // Use the retrieved master key to unseal the barrier + if err := c.barrier.Unseal(ctx, key); err != nil { + return errwrap.Wrapf("recovery operation token generation failed, cannot unseal barrier: {{err}}", err) + } + + for _, v := range c.postRecoveryUnsealFuncs { + if err := v(); err != nil { + return errwrap.Wrapf("failed to run post unseal func: {{err}}", err) + } + } + return nil +} + func (g *generateRecoveryToken) generate(ctx context.Context, c *Core) (string, func(), error) { id, err := base62.Random(TokenLength) if err != nil { From eac8b996725d80ade4148e8a86bcf4175421afec Mon Sep 17 00:00:00 2001 From: Noelle Daley Date: Wed, 23 Oct 2019 12:05:15 -0700 Subject: [PATCH 13/14] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e848dc9adfd6..94fc98ef12ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ IMPROVEMENTS: from `telemetry` due to potential sensitive entries in those fields. * ui: when using raft storage, you can now join a raft cluster, download a snapshot, and restore a snapshot from the UI [GH-7410] + * ui: clarify when secret version is deleted in the secret version history dropdown [GH-7714] * sys: Add a new `sys/internal/counters/tokens` endpoint, that counts the total number of active service token accessors in the shared token storage. [GH-7541] @@ -94,6 +95,7 @@ BUG FIXES: * ui: using the `wrapped_token` query param will work with `redirect_to` and will automatically log in as intended [GH-7398] * ui: fix an error when initializing from the UI using PGP keys [GH-7542] + * ui: show all active kv v2 secret versions even when `delete_version_after` is configured [GH-7685] * cli: Command timeouts are now always specified solely by the `VAULT_CLIENT_TIMEOUT` value. [GH-7469] From 33ff82bfcb65dd35a5710b1a8112bc98bdcfa0e4 Mon Sep 17 00:00:00 2001 From: ncabatoff Date: Wed, 23 Oct 2019 15:58:02 -0400 Subject: [PATCH 14/14] changelog++ --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94fc98ef12ff..1b5066c8fbaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,7 +90,6 @@ BUG FIXES: always returned an empty object [GH-7705] * identity (enterprise): Fixed identity case sensitive loading in secondary cluster [GH-7327] - * identity: Fixed nil pointer panic when merging entities [GH-7711] * raft: Fixed VAULT_CLUSTER_ADDR env being ignored at startup [GH-7619] * ui: using the `wrapped_token` query param will work with `redirect_to` and will automatically log in as intended [GH-7398] @@ -122,6 +121,7 @@ BUG FIXES: worked on enterprise, now it does. * identity: Add required field `response_types_supported` to identity token `.well-known/openid-configuration` response [GH-7533] + * identity: Fixed nil pointer panic when merging entities [GH-7712] * secrets/database: Fix bug in combined DB secrets engine that can result in writes to static-roles endpoints timing out [GH-7518] * secrets/pki: Improve tidy to continue when value is nil [GH-7589]