mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-08 07:37:01 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
1241 lines
36 KiB
Go
1241 lines
36 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package postgresql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/helper/testhelpers/postgresql"
|
|
"github.com/hashicorp/vault/sdk/database/dbplugin/v5"
|
|
dbtesting "github.com/hashicorp/vault/sdk/database/dbplugin/v5/testing"
|
|
"github.com/hashicorp/vault/sdk/database/helper/dbutil"
|
|
"github.com/hashicorp/vault/sdk/helper/docker"
|
|
"github.com/hashicorp/vault/sdk/helper/template"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func getPostgreSQL(t *testing.T, options map[string]interface{}) (*PostgreSQL, func()) {
|
|
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
|
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
for k, v := range options {
|
|
connectionDetails[k] = v
|
|
}
|
|
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
db := new()
|
|
dbtesting.AssertInitialize(t, db, req)
|
|
|
|
if !db.Initialized {
|
|
t.Fatal("Database should be initialized")
|
|
}
|
|
return db, cleanup
|
|
}
|
|
|
|
func TestPostgreSQL_Initialize(t *testing.T) {
|
|
db, cleanup := getPostgreSQL(t, map[string]interface{}{
|
|
"max_open_connections": 5,
|
|
})
|
|
defer cleanup()
|
|
|
|
if err := db.Close(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestPostgreSQL_InitializeWithStringVals(t *testing.T) {
|
|
db, cleanup := getPostgreSQL(t, map[string]interface{}{
|
|
"max_open_connections": "5",
|
|
})
|
|
defer cleanup()
|
|
|
|
if err := db.Close(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestPostgreSQL_Initialize_ConnURLWithDSNFormat(t *testing.T) {
|
|
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
|
defer cleanup()
|
|
|
|
dsnConnURL, err := dbutil.ParseURL(connURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": dsnConnURL,
|
|
}
|
|
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
db := new()
|
|
dbtesting.AssertInitialize(t, db, req)
|
|
|
|
if !db.Initialized {
|
|
t.Fatal("Database should be initialized")
|
|
}
|
|
}
|
|
|
|
// TestPostgreSQL_PasswordAuthentication tests that the default "password_authentication" is "none", and that
|
|
// an error is returned if an invalid "password_authentication" is provided.
|
|
func TestPostgreSQL_PasswordAuthentication(t *testing.T) {
|
|
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
|
defer cleanup()
|
|
|
|
dsnConnURL, err := dbutil.ParseURL(connURL)
|
|
assert.NoError(t, err)
|
|
db := new()
|
|
|
|
ctx := context.Background()
|
|
|
|
t.Run("invalid-password-authentication", func(t *testing.T) {
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": dsnConnURL,
|
|
"password_authentication": "invalid-password-authentication",
|
|
}
|
|
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
_, err := db.Initialize(ctx, req)
|
|
assert.EqualError(t, err, "'invalid-password-authentication' is not a valid password authentication type")
|
|
})
|
|
|
|
t.Run("default-is-none", func(t *testing.T) {
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": dsnConnURL,
|
|
}
|
|
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
_ = dbtesting.AssertInitialize(t, db, req)
|
|
assert.Equal(t, passwordAuthenticationPassword, db.passwordAuthentication)
|
|
})
|
|
}
|
|
|
|
// TestPostgreSQL_PasswordAuthentication_SCRAMSHA256 tests that password_authentication works when set to scram-sha-256.
|
|
// When sending an encrypted password, the raw password should still successfully authenticate the user.
|
|
func TestPostgreSQL_PasswordAuthentication_SCRAMSHA256(t *testing.T) {
|
|
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
|
defer cleanup()
|
|
|
|
dsnConnURL, err := dbutil.ParseURL(connURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": dsnConnURL,
|
|
"password_authentication": string(passwordAuthenticationSCRAMSHA256),
|
|
}
|
|
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
db := new()
|
|
resp := dbtesting.AssertInitialize(t, db, req)
|
|
assert.Equal(t, string(passwordAuthenticationSCRAMSHA256), resp.Config["password_authentication"])
|
|
|
|
if !db.Initialized {
|
|
t.Fatal("Database should be initialized")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
newUserRequest := dbplugin.NewUserRequest{
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{name}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
}
|
|
newUserResponse, err := db.NewUser(ctx, newUserRequest)
|
|
|
|
assertCredsExist(t, db.ConnectionURL, newUserResponse.Username, newUserRequest.Password)
|
|
}
|
|
|
|
func TestPostgreSQL_NewUser(t *testing.T) {
|
|
type testCase struct {
|
|
req dbplugin.NewUserRequest
|
|
expectErr bool
|
|
credsAssertion credsAssertion
|
|
}
|
|
|
|
tests := map[string]testCase{
|
|
"no creation statements": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
// No statements
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: true,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^$"),
|
|
assertCredsDoNotExist,
|
|
),
|
|
},
|
|
"admin name": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{name}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsExist,
|
|
),
|
|
},
|
|
"admin username": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{username}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{username}}";`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsExist,
|
|
),
|
|
},
|
|
"read only name": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{name}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
|
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "{{name}}";`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsExist,
|
|
),
|
|
},
|
|
"read only username": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{username}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{username}}";
|
|
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO "{{username}}";`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsExist,
|
|
),
|
|
},
|
|
// https://github.com/hashicorp/vault/issues/6098
|
|
"reproduce GH-6098": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
// NOTE: "rolname" in the following line is not a typo.
|
|
"DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE my_role; END IF; END $$",
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsDoNotExist,
|
|
),
|
|
},
|
|
"reproduce issue with template": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE "{{username}}"; END IF; END $$`,
|
|
},
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsDoNotExist,
|
|
),
|
|
},
|
|
"large block statements": {
|
|
req: dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: newUserLargeBlockStatements,
|
|
},
|
|
Password: "somesecurepassword",
|
|
Expiration: time.Now().Add(1 * time.Minute),
|
|
},
|
|
expectErr: false,
|
|
credsAssertion: assertCreds(
|
|
assertUsernameRegex("^v-test-test-[a-zA-Z0-9]{20}-[0-9]{10}$"),
|
|
assertCredsExist,
|
|
),
|
|
},
|
|
}
|
|
|
|
// Shared test container for speed - there should not be any overlap between the tests
|
|
db, cleanup := getPostgreSQL(t, nil)
|
|
defer cleanup()
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
// Give a timeout just in case the test decides to be problematic
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := db.NewUser(ctx, test.req)
|
|
if test.expectErr && err == nil {
|
|
t.Fatalf("err expected, got nil")
|
|
}
|
|
if !test.expectErr && err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
test.credsAssertion(t, db.ConnectionURL, resp.Username, test.req.Password)
|
|
|
|
// Ensure that the role doesn't expire immediately
|
|
time.Sleep(2 * time.Second)
|
|
|
|
test.credsAssertion(t, db.ConnectionURL, resp.Username, test.req.Password)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUpdateUser_Password(t *testing.T) {
|
|
type testCase struct {
|
|
statements []string
|
|
expectErr bool
|
|
credsAssertion credsAssertion
|
|
}
|
|
|
|
tests := map[string]testCase{
|
|
"default statements": {
|
|
statements: nil,
|
|
expectErr: false,
|
|
credsAssertion: assertCredsExist,
|
|
},
|
|
"explicit default statements": {
|
|
statements: []string{defaultChangePasswordStatement},
|
|
expectErr: false,
|
|
credsAssertion: assertCredsExist,
|
|
},
|
|
"name instead of username": {
|
|
statements: []string{`ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';`},
|
|
expectErr: false,
|
|
credsAssertion: assertCredsExist,
|
|
},
|
|
"bad statements": {
|
|
statements: []string{`asdofyas8uf77asoiajv`},
|
|
expectErr: true,
|
|
credsAssertion: assertCredsDoNotExist,
|
|
},
|
|
}
|
|
|
|
// Shared test container for speed - there should not be any overlap between the tests
|
|
db, cleanup := getPostgreSQL(t, nil)
|
|
defer cleanup()
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
initialPass := "myreallysecurepassword"
|
|
createReq := dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{createAdminUser},
|
|
},
|
|
Password: initialPass,
|
|
Expiration: time.Now().Add(2 * time.Second),
|
|
}
|
|
createResp := dbtesting.AssertNewUser(t, db, createReq)
|
|
|
|
assertCredsExist(t, db.ConnectionURL, createResp.Username, initialPass)
|
|
|
|
newPass := "somenewpassword"
|
|
updateReq := dbplugin.UpdateUserRequest{
|
|
Username: createResp.Username,
|
|
Password: &dbplugin.ChangePassword{
|
|
NewPassword: newPass,
|
|
Statements: dbplugin.Statements{
|
|
Commands: test.statements,
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, err := db.UpdateUser(ctx, updateReq)
|
|
if test.expectErr && err == nil {
|
|
t.Fatalf("err expected, got nil")
|
|
}
|
|
if !test.expectErr && err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
test.credsAssertion(t, db.ConnectionURL, createResp.Username, newPass)
|
|
})
|
|
}
|
|
|
|
t.Run("user does not exist", func(t *testing.T) {
|
|
newPass := "somenewpassword"
|
|
updateReq := dbplugin.UpdateUserRequest{
|
|
Username: "missing-user",
|
|
Password: &dbplugin.ChangePassword{
|
|
NewPassword: newPass,
|
|
Statements: dbplugin.Statements{},
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, err := db.UpdateUser(ctx, updateReq)
|
|
if err == nil {
|
|
t.Fatalf("err expected, got nil")
|
|
}
|
|
|
|
assertCredsDoNotExist(t, db.ConnectionURL, updateReq.Username, newPass)
|
|
})
|
|
}
|
|
|
|
func TestUpdateUser_Expiration(t *testing.T) {
|
|
type testCase struct {
|
|
initialExpiration time.Time
|
|
newExpiration time.Time
|
|
expectedExpiration time.Time
|
|
statements []string
|
|
expectErr bool
|
|
}
|
|
|
|
now := time.Now()
|
|
tests := map[string]testCase{
|
|
"no statements": {
|
|
initialExpiration: now.Add(1 * time.Minute),
|
|
newExpiration: now.Add(5 * time.Minute),
|
|
expectedExpiration: now.Add(5 * time.Minute),
|
|
statements: nil,
|
|
expectErr: false,
|
|
},
|
|
"default statements with name": {
|
|
initialExpiration: now.Add(1 * time.Minute),
|
|
newExpiration: now.Add(5 * time.Minute),
|
|
expectedExpiration: now.Add(5 * time.Minute),
|
|
statements: []string{defaultExpirationStatement},
|
|
expectErr: false,
|
|
},
|
|
"default statements with username": {
|
|
initialExpiration: now.Add(1 * time.Minute),
|
|
newExpiration: now.Add(5 * time.Minute),
|
|
expectedExpiration: now.Add(5 * time.Minute),
|
|
statements: []string{`ALTER ROLE "{{username}}" VALID UNTIL '{{expiration}}';`},
|
|
expectErr: false,
|
|
},
|
|
"bad statements": {
|
|
initialExpiration: now.Add(1 * time.Minute),
|
|
newExpiration: now.Add(5 * time.Minute),
|
|
expectedExpiration: now.Add(1 * time.Minute),
|
|
statements: []string{"ladshfouay09sgj"},
|
|
expectErr: true,
|
|
},
|
|
}
|
|
|
|
// Shared test container for speed - there should not be any overlap between the tests
|
|
db, cleanup := getPostgreSQL(t, nil)
|
|
defer cleanup()
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
password := "myreallysecurepassword"
|
|
initialExpiration := test.initialExpiration.Truncate(time.Second)
|
|
createReq := dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{createAdminUser},
|
|
},
|
|
Password: password,
|
|
Expiration: initialExpiration,
|
|
}
|
|
createResp := dbtesting.AssertNewUser(t, db, createReq)
|
|
|
|
assertCredsExist(t, db.ConnectionURL, createResp.Username, password)
|
|
|
|
actualExpiration := getExpiration(t, db, createResp.Username)
|
|
if actualExpiration.IsZero() {
|
|
t.Fatalf("Initial expiration is zero but should be set")
|
|
}
|
|
if !actualExpiration.Equal(initialExpiration) {
|
|
t.Fatalf("Actual expiration: %s Expected expiration: %s", actualExpiration, initialExpiration)
|
|
}
|
|
|
|
newExpiration := test.newExpiration.Truncate(time.Second)
|
|
updateReq := dbplugin.UpdateUserRequest{
|
|
Username: createResp.Username,
|
|
Expiration: &dbplugin.ChangeExpiration{
|
|
NewExpiration: newExpiration,
|
|
Statements: dbplugin.Statements{
|
|
Commands: test.statements,
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
_, err := db.UpdateUser(ctx, updateReq)
|
|
if test.expectErr && err == nil {
|
|
t.Fatalf("err expected, got nil")
|
|
}
|
|
if !test.expectErr && err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
expectedExpiration := test.expectedExpiration.Truncate(time.Second)
|
|
actualExpiration = getExpiration(t, db, createResp.Username)
|
|
if !actualExpiration.Equal(expectedExpiration) {
|
|
t.Fatalf("Actual expiration: %s Expected expiration: %s", actualExpiration, expectedExpiration)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getExpiration(t testing.TB, db *PostgreSQL, username string) time.Time {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
query := fmt.Sprintf("select valuntil from pg_catalog.pg_user where usename = '%s'", username)
|
|
conn, err := db.getConnection(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get connection to database: %s", err)
|
|
}
|
|
|
|
stmt, err := conn.PrepareContext(ctx, query)
|
|
if err != nil {
|
|
t.Fatalf("Failed to prepare statement: %s", err)
|
|
}
|
|
defer stmt.Close()
|
|
|
|
rows, err := stmt.QueryContext(ctx)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute query to get expiration: %s", err)
|
|
}
|
|
|
|
if !rows.Next() {
|
|
return time.Time{} // No expiration
|
|
}
|
|
rawExp := ""
|
|
err = rows.Scan(&rawExp)
|
|
if err != nil {
|
|
t.Fatalf("Unable to get raw expiration: %s", err)
|
|
}
|
|
if rawExp == "" {
|
|
return time.Time{} // No expiration
|
|
}
|
|
exp, err := time.Parse(time.RFC3339, rawExp)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse expiration %q: %s", rawExp, err)
|
|
}
|
|
return exp
|
|
}
|
|
|
|
func TestDeleteUser(t *testing.T) {
|
|
type testCase struct {
|
|
revokeStmts []string
|
|
expectErr bool
|
|
credsAssertion credsAssertion
|
|
}
|
|
|
|
tests := map[string]testCase{
|
|
"no statements": {
|
|
revokeStmts: nil,
|
|
expectErr: false,
|
|
// Wait for a short time before failing because postgres takes a moment to finish deleting the user
|
|
credsAssertion: waitUntilCredsDoNotExist(2 * time.Second),
|
|
},
|
|
"statements with name": {
|
|
revokeStmts: []string{`
|
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{name}}";
|
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{name}}";
|
|
REVOKE USAGE ON SCHEMA public FROM "{{name}}";
|
|
|
|
DROP ROLE IF EXISTS "{{name}}";`},
|
|
expectErr: false,
|
|
// Wait for a short time before failing because postgres takes a moment to finish deleting the user
|
|
credsAssertion: waitUntilCredsDoNotExist(2 * time.Second),
|
|
},
|
|
"statements with username": {
|
|
revokeStmts: []string{`
|
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";
|
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{username}}";
|
|
REVOKE USAGE ON SCHEMA public FROM "{{username}}";
|
|
|
|
DROP ROLE IF EXISTS "{{username}}";`},
|
|
expectErr: false,
|
|
// Wait for a short time before failing because postgres takes a moment to finish deleting the user
|
|
credsAssertion: waitUntilCredsDoNotExist(2 * time.Second),
|
|
},
|
|
"bad statements": {
|
|
revokeStmts: []string{`8a9yhfoiasjff`},
|
|
expectErr: true,
|
|
// Wait for a short time before checking because postgres takes a moment to finish deleting the user
|
|
credsAssertion: assertCredsExistAfter(100 * time.Millisecond),
|
|
},
|
|
"multiline": {
|
|
revokeStmts: []string{`
|
|
DO $$ BEGIN
|
|
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM "{{username}}";
|
|
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM "{{username}}";
|
|
REVOKE USAGE ON SCHEMA public FROM "{{username}}";
|
|
DROP ROLE IF EXISTS "{{username}}";
|
|
END $$;
|
|
`},
|
|
expectErr: false,
|
|
// Wait for a short time before checking because postgres takes a moment to finish deleting the user
|
|
credsAssertion: waitUntilCredsDoNotExist(2 * time.Second),
|
|
},
|
|
}
|
|
|
|
// Shared test container for speed - there should not be any overlap between the tests
|
|
db, cleanup := getPostgreSQL(t, nil)
|
|
defer cleanup()
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
password := "myreallysecurepassword"
|
|
createReq := dbplugin.NewUserRequest{
|
|
UsernameConfig: dbplugin.UsernameMetadata{
|
|
DisplayName: "test",
|
|
RoleName: "test",
|
|
},
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{createAdminUser},
|
|
},
|
|
Password: password,
|
|
Expiration: time.Now().Add(2 * time.Second),
|
|
}
|
|
createResp := dbtesting.AssertNewUser(t, db, createReq)
|
|
|
|
assertCredsExist(t, db.ConnectionURL, createResp.Username, password)
|
|
|
|
deleteReq := dbplugin.DeleteUserRequest{
|
|
Username: createResp.Username,
|
|
Statements: dbplugin.Statements{
|
|
Commands: test.revokeStmts,
|
|
},
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := db.DeleteUser(ctx, deleteReq)
|
|
if test.expectErr && err == nil {
|
|
t.Fatalf("err expected, got nil")
|
|
}
|
|
if !test.expectErr && err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
test.credsAssertion(t, db.ConnectionURL, createResp.Username, password)
|
|
})
|
|
}
|
|
}
|
|
|
|
type credsAssertion func(t testing.TB, connURL, username, password string)
|
|
|
|
func assertCreds(assertions ...credsAssertion) credsAssertion {
|
|
return func(t testing.TB, connURL, username, password string) {
|
|
t.Helper()
|
|
for _, assertion := range assertions {
|
|
assertion(t, connURL, username, password)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertUsernameRegex(rawRegex string) credsAssertion {
|
|
return func(t testing.TB, _, username, _ string) {
|
|
t.Helper()
|
|
require.Regexp(t, rawRegex, username)
|
|
}
|
|
}
|
|
|
|
func assertCredsExist(t testing.TB, connURL, username, password string) {
|
|
t.Helper()
|
|
err := testCredsExist(t, connURL, username, password)
|
|
if err != nil {
|
|
t.Fatalf("user does not exist: %s", err)
|
|
}
|
|
}
|
|
|
|
func assertCredsDoNotExist(t testing.TB, connURL, username, password string) {
|
|
t.Helper()
|
|
err := testCredsExist(t, connURL, username, password)
|
|
if err == nil {
|
|
t.Fatalf("user should not exist but does")
|
|
}
|
|
}
|
|
|
|
func waitUntilCredsDoNotExist(timeout time.Duration) credsAssertion {
|
|
return func(t testing.TB, connURL, username, password string) {
|
|
t.Helper()
|
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
defer cancel()
|
|
|
|
ticker := time.NewTicker(10 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
t.Fatalf("Timed out waiting for user %s to be deleted", username)
|
|
case <-ticker.C:
|
|
err := testCredsExist(t, connURL, username, password)
|
|
if err != nil {
|
|
// Happy path
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertCredsExistAfter(timeout time.Duration) credsAssertion {
|
|
return func(t testing.TB, connURL, username, password string) {
|
|
t.Helper()
|
|
time.Sleep(timeout)
|
|
assertCredsExist(t, connURL, username, password)
|
|
}
|
|
}
|
|
|
|
func testCredsExist(t testing.TB, connURL, username, password string) error {
|
|
t.Helper()
|
|
// Log in with the new creds
|
|
connURL = strings.Replace(connURL, "postgres:secret", fmt.Sprintf("%s:%s", username, password), 1)
|
|
db, err := sql.Open("pgx", connURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer db.Close()
|
|
return db.Ping()
|
|
}
|
|
|
|
const createAdminUser = `
|
|
CREATE ROLE "{{name}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
|
|
`
|
|
|
|
var newUserLargeBlockStatements = []string{
|
|
`
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT * FROM pg_catalog.pg_roles WHERE rolname='foo-role') THEN
|
|
CREATE ROLE "foo-role";
|
|
CREATE SCHEMA IF NOT EXISTS foo AUTHORIZATION "foo-role";
|
|
ALTER ROLE "foo-role" SET search_path = foo;
|
|
GRANT TEMPORARY ON DATABASE "postgres" TO "foo-role";
|
|
GRANT ALL PRIVILEGES ON SCHEMA foo TO "foo-role";
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO "foo-role";
|
|
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA foo TO "foo-role";
|
|
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA foo TO "foo-role";
|
|
END IF;
|
|
END
|
|
$$
|
|
`,
|
|
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';`,
|
|
`GRANT "foo-role" TO "{{name}}";`,
|
|
`ALTER ROLE "{{name}}" SET search_path = foo;`,
|
|
`GRANT CONNECT ON DATABASE "postgres" TO "{{name}}";`,
|
|
}
|
|
|
|
func TestContainsMultilineStatement(t *testing.T) {
|
|
type testCase struct {
|
|
Input string
|
|
Expected bool
|
|
}
|
|
|
|
testCases := map[string]*testCase{
|
|
"issue 6098 repro": {
|
|
Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname='my_role') THEN CREATE ROLE my_role; END IF; END $$`,
|
|
Expected: true,
|
|
},
|
|
"multiline with template fields": {
|
|
Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname="{{name}}") THEN CREATE ROLE {{name}}; END IF; END $$`,
|
|
Expected: true,
|
|
},
|
|
"docs example": {
|
|
Input: `CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
|
|
Expected: false,
|
|
},
|
|
}
|
|
|
|
for tName, tCase := range testCases {
|
|
t.Run(tName, func(t *testing.T) {
|
|
if containsMultilineStatement(tCase.Input) != tCase.Expected {
|
|
t.Fatalf("%q should be %t for multiline input", tCase.Input, tCase.Expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractQuotedStrings(t *testing.T) {
|
|
type testCase struct {
|
|
Input string
|
|
Expected []string
|
|
}
|
|
|
|
testCases := map[string]*testCase{
|
|
"no quotes": {
|
|
Input: `Five little monkeys jumping on the bed`,
|
|
Expected: []string{},
|
|
},
|
|
"two of both quote types": {
|
|
Input: `"Five" little 'monkeys' "jumping on" the' 'bed`,
|
|
Expected: []string{`"Five"`, `"jumping on"`, `'monkeys'`, `' '`},
|
|
},
|
|
"one single quote": {
|
|
Input: `Five little monkeys 'jumping on the bed`,
|
|
Expected: []string{},
|
|
},
|
|
"empty string": {
|
|
Input: ``,
|
|
Expected: []string{},
|
|
},
|
|
"templated field": {
|
|
Input: `DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname="{{name}}") THEN CREATE ROLE {{name}}; END IF; END $$`,
|
|
Expected: []string{`"{{name}}"`},
|
|
},
|
|
}
|
|
|
|
for tName, tCase := range testCases {
|
|
t.Run(tName, func(t *testing.T) {
|
|
results, err := extractQuotedStrings(tCase.Input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(results) != len(tCase.Expected) {
|
|
t.Fatalf("%s isn't equal to %s", results, tCase.Expected)
|
|
}
|
|
for i := range results {
|
|
if results[i] != tCase.Expected[i] {
|
|
t.Fatalf(`expected %q but received %q`, tCase.Expected, results[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUsernameGeneration(t *testing.T) {
|
|
type testCase struct {
|
|
data dbplugin.UsernameMetadata
|
|
expectedRegex string
|
|
}
|
|
|
|
tests := map[string]testCase{
|
|
"simple display and role names": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token",
|
|
RoleName: "myrole",
|
|
},
|
|
expectedRegex: `v-token-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"display name has dash": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token-foo",
|
|
RoleName: "myrole",
|
|
},
|
|
expectedRegex: `v-token-fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"display name has underscore": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token_foo",
|
|
RoleName: "myrole",
|
|
},
|
|
expectedRegex: `v-token_fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"display name has period": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token.foo",
|
|
RoleName: "myrole",
|
|
},
|
|
expectedRegex: `v-token.fo-myrole-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"role name has dash": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token",
|
|
RoleName: "myrole-foo",
|
|
},
|
|
expectedRegex: `v-token-myrole-f-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"role name has underscore": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token",
|
|
RoleName: "myrole_foo",
|
|
},
|
|
expectedRegex: `v-token-myrole_f-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
"role name has period": {
|
|
data: dbplugin.UsernameMetadata{
|
|
DisplayName: "token",
|
|
RoleName: "myrole.foo",
|
|
},
|
|
expectedRegex: `v-token-myrole.f-[a-zA-Z0-9]{20}-[0-9]{10}`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(fmt.Sprintf("new-%s", name), func(t *testing.T) {
|
|
up, err := template.NewTemplate(
|
|
template.Template(defaultUserNameTemplate),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
for i := 0; i < 1000; i++ {
|
|
username, err := up.Generate(test.data)
|
|
require.NoError(t, err)
|
|
require.Regexp(t, test.expectedRegex, username)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewUser_CustomUsername(t *testing.T) {
|
|
cleanup, connURL := postgresql.PrepareTestContainer(t, "13.4-buster")
|
|
defer cleanup()
|
|
|
|
type testCase struct {
|
|
usernameTemplate string
|
|
newUserData dbplugin.UsernameMetadata
|
|
expectedRegex string
|
|
}
|
|
|
|
tests := map[string]testCase{
|
|
"default template": {
|
|
usernameTemplate: "",
|
|
newUserData: dbplugin.UsernameMetadata{
|
|
DisplayName: "displayname",
|
|
RoleName: "longrolename",
|
|
},
|
|
expectedRegex: "^v-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$",
|
|
},
|
|
"explicit default template": {
|
|
usernameTemplate: defaultUserNameTemplate,
|
|
newUserData: dbplugin.UsernameMetadata{
|
|
DisplayName: "displayname",
|
|
RoleName: "longrolename",
|
|
},
|
|
expectedRegex: "^v-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$",
|
|
},
|
|
"unique template": {
|
|
usernameTemplate: "foo-bar",
|
|
newUserData: dbplugin.UsernameMetadata{
|
|
DisplayName: "displayname",
|
|
RoleName: "longrolename",
|
|
},
|
|
expectedRegex: "^foo-bar$",
|
|
},
|
|
"custom prefix": {
|
|
usernameTemplate: "foobar-{{.DisplayName | truncate 8}}-{{.RoleName | truncate 8}}-{{random 20}}-{{unix_time}}",
|
|
newUserData: dbplugin.UsernameMetadata{
|
|
DisplayName: "displayname",
|
|
RoleName: "longrolename",
|
|
},
|
|
expectedRegex: "^foobar-displayn-longrole-[a-zA-Z0-9]{20}-[0-9]{10}$",
|
|
},
|
|
"totally custom template": {
|
|
usernameTemplate: "foobar_{{random 10}}-{{.RoleName | uppercase}}.{{unix_time}}x{{.DisplayName | truncate 5}}",
|
|
newUserData: dbplugin.UsernameMetadata{
|
|
DisplayName: "displayname",
|
|
RoleName: "longrolename",
|
|
},
|
|
expectedRegex: `^foobar_[a-zA-Z0-9]{10}-LONGROLENAME\.[0-9]{10}xdispl$`,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
initReq := dbplugin.InitializeRequest{
|
|
Config: map[string]interface{}{
|
|
"connection_url": connURL,
|
|
"username_template": test.usernameTemplate,
|
|
},
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
db := new()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
_, err := db.Initialize(ctx, initReq)
|
|
require.NoError(t, err)
|
|
|
|
newUserReq := dbplugin.NewUserRequest{
|
|
UsernameConfig: test.newUserData,
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`
|
|
CREATE ROLE "{{name}}" WITH
|
|
LOGIN
|
|
PASSWORD '{{password}}'
|
|
VALID UNTIL '{{expiration}}';
|
|
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";`,
|
|
},
|
|
},
|
|
Password: "myReally-S3curePassword",
|
|
Expiration: time.Now().Add(1 * time.Hour),
|
|
}
|
|
ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
newUserResp, err := db.NewUser(ctx, newUserReq)
|
|
require.NoError(t, err)
|
|
|
|
require.Regexp(t, test.expectedRegex, newUserResp.Username)
|
|
})
|
|
}
|
|
}
|
|
|
|
// This is a long-running integration test which tests the functionality of Postgres's multi-host
|
|
// connection strings. It uses two Postgres containers preconfigured with Replication Manager
|
|
// provided by Bitnami. This test currently does not run in CI and must be run manually. This is
|
|
// due to the test length, as it requires multiple sleep calls to ensure cluster setup and
|
|
// primary node failover occurs before the test steps continue.
|
|
//
|
|
// To run the test, set the environment variable POSTGRES_MULTIHOST_NET to the value of
|
|
// a docker network you've preconfigured, e.g.
|
|
// 'docker network create -d bridge postgres-repmgr'
|
|
// 'export POSTGRES_MULTIHOST_NET=postgres-repmgr'
|
|
func TestPostgreSQL_Repmgr(t *testing.T) {
|
|
_, exists := os.LookupEnv("POSTGRES_MULTIHOST_NET")
|
|
if !exists {
|
|
t.Skipf("POSTGRES_MULTIHOST_NET not set, skipping test")
|
|
}
|
|
|
|
// Run two postgres-repmgr containers in a replication cluster
|
|
db0, runner0, url0, container0 := testPostgreSQL_Repmgr_Container(t, "psql-repl-node-0")
|
|
_, _, url1, _ := testPostgreSQL_Repmgr_Container(t, "psql-repl-node-1")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second)
|
|
defer cancel()
|
|
|
|
time.Sleep(10 * time.Second)
|
|
|
|
// Write a read role to the cluster
|
|
_, err := db0.NewUser(ctx, dbplugin.NewUserRequest{
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`CREATE ROLE "ro" NOINHERIT;
|
|
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "ro";`,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
// Open a connection to both databases using the multihost connection string
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": fmt.Sprintf("postgresql://{{username}}:{{password}}@%s,%s/postgres?target_session_attrs=read-write", getHost(url0), getHost(url1)),
|
|
"username": "postgres",
|
|
"password": "secret",
|
|
}
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
|
|
db := new()
|
|
dbtesting.AssertInitialize(t, db, req)
|
|
if !db.Initialized {
|
|
t.Fatal("Database should be initialized")
|
|
}
|
|
defer db.Close()
|
|
|
|
// Add a user to the cluster, then stop the primary container
|
|
if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
postgresql.StopContainer(t, ctx, runner0, container0)
|
|
|
|
// Try adding a new user immediately - expect failure as the database
|
|
// cluster is still switching primaries
|
|
err = testPostgreSQL_Repmgr_AddUser(ctx, db)
|
|
if !strings.HasSuffix(err.Error(), "ValidateConnect failed (read only connection)") {
|
|
t.Fatalf("expected error was not received, got: %s", err)
|
|
}
|
|
|
|
time.Sleep(20 * time.Second)
|
|
|
|
// Try adding a new user again which should succeed after the sleep
|
|
// as the primary failover should have finished. Then, restart
|
|
// the first container which should become a secondary DB.
|
|
if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
postgresql.RestartContainer(t, ctx, runner0, container0)
|
|
|
|
time.Sleep(10 * time.Second)
|
|
|
|
// A final new user to add, which should succeed after the secondary joins.
|
|
if err = testPostgreSQL_Repmgr_AddUser(ctx, db); err != nil {
|
|
t.Fatalf("no error expected, got: %s", err)
|
|
}
|
|
|
|
if err := db.Close(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
}
|
|
|
|
func testPostgreSQL_Repmgr_Container(t *testing.T, name string) (*PostgreSQL, *docker.Runner, string, string) {
|
|
envVars := []string{
|
|
"REPMGR_NODE_NAME=" + name,
|
|
"REPMGR_NODE_NETWORK_NAME=" + name,
|
|
}
|
|
|
|
runner, cleanup, connURL, containerID := postgresql.PrepareTestContainerRepmgr(t, name, "13.4.0", envVars)
|
|
t.Cleanup(cleanup)
|
|
|
|
connectionDetails := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
req := dbplugin.InitializeRequest{
|
|
Config: connectionDetails,
|
|
VerifyConnection: true,
|
|
}
|
|
db := new()
|
|
dbtesting.AssertInitialize(t, db, req)
|
|
if !db.Initialized {
|
|
t.Fatal("Database should be initialized")
|
|
}
|
|
|
|
if err := db.Close(); err != nil {
|
|
t.Fatalf("err: %s", err)
|
|
}
|
|
|
|
return db, runner, connURL, containerID
|
|
}
|
|
|
|
func testPostgreSQL_Repmgr_AddUser(ctx context.Context, db *PostgreSQL) error {
|
|
_, err := db.NewUser(ctx, dbplugin.NewUserRequest{
|
|
Statements: dbplugin.Statements{
|
|
Commands: []string{
|
|
`CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}' INHERIT;
|
|
GRANT ro TO "{{name}}";`,
|
|
},
|
|
},
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func getHost(url string) string {
|
|
splitCreds := strings.Split(url, "@")[1]
|
|
|
|
return strings.Split(splitCreds, "/")[0]
|
|
}
|