From 4335ad91a4c5efde27a7b0a7cb2bf17d9221de76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20OUDRY?= <107411+seboudry@users.noreply.github.com> Date: Thu, 20 Feb 2025 15:05:47 +0100 Subject: [PATCH] support specials on user mapping --- go.mod | 1 + go.sum | 1 + .../resource_postgresql_user_mapping.go | 35 +- .../resource_postgresql_user_mapping_test.go | 329 +++++++++++++++--- 4 files changed, 304 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index 15e4dade..70d26f40 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/sean-/postgresql-acl v0.0.0-20161225120419-d10489e5d217 github.com/stretchr/testify v1.9.0 gocloud.dev v0.34.0 + golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 golang.org/x/net v0.26.0 golang.org/x/oauth2 v0.10.0 google.golang.org/api v0.134.0 diff --git a/go.sum b/go.sum index d80aff2c..99990f8f 100644 --- a/go.sum +++ b/go.sum @@ -1386,6 +1386,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= diff --git a/postgresql/resource_postgresql_user_mapping.go b/postgresql/resource_postgresql_user_mapping.go index 891ef2b0..491b05dd 100644 --- a/postgresql/resource_postgresql_user_mapping.go +++ b/postgresql/resource_postgresql_user_mapping.go @@ -9,6 +9,8 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/lib/pq" + + "golang.org/x/exp/slices" ) const ( @@ -52,6 +54,16 @@ func resourcePostgreSQLUserMapping() *schema.Resource { } } +func quoteUsername(username string) string { + usernameSpecials := []string{"CURRENT_ROLE", "CURRENT_USER", "PUBLIC", "USER"} + if slices.Contains(usernameSpecials, username) { + // don't use quotes on specials + return username + } else { + return pq.QuoteIdentifier(username) + } +} + func resourcePostgreSQLUserMappingCreate(db *DBConnection, d *schema.ResourceData) error { if !db.featureSupported(featureServer) { return fmt.Errorf( @@ -64,7 +76,7 @@ func resourcePostgreSQLUserMappingCreate(db *DBConnection, d *schema.ResourceDat serverName := d.Get(userMappingServerNameAttr).(string) b := bytes.NewBufferString("CREATE USER MAPPING ") - fmt.Fprint(b, " FOR ", pq.QuoteIdentifier(username)) + fmt.Fprint(b, " FOR ", quoteUsername(username)) fmt.Fprint(b, " SERVER ", pq.QuoteIdentifier(serverName)) if options, ok := d.GetOk(userMappingOptionsAttr); ok { @@ -105,6 +117,15 @@ func resourcePostgreSQLUserMappingReadImpl(db *DBConnection, d *schema.ResourceD username := d.Get(userMappingUserNameAttr).(string) serverName := d.Get(userMappingServerNameAttr).(string) + var usernameSpecial string + if username == "PUBLIC" { + usernameSpecial = username + username = "public" + } else if slices.Contains([]string{"CURRENT_ROLE", "CURRENT_USER", "USER"}, username) { + usernameSpecial = username + username = db.client.config.getDatabaseUsername() + } + txn, err := startTransaction(db.client, "") if err != nil { return err @@ -115,7 +136,7 @@ func resourcePostgreSQLUserMappingReadImpl(db *DBConnection, d *schema.ResourceD query := "SELECT umoptions FROM information_schema._pg_user_mappings WHERE authorization_identifier = $1 and foreign_server_name = $2" err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) - if err != sql.ErrNoRows && err != nil { + if err != nil { // Fallback to pg_user_mappings table if information_schema._pg_user_mappings is not available query := "SELECT umoptions FROM pg_user_mappings WHERE usename = $1 and srvname = $2" err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) @@ -136,7 +157,11 @@ func resourcePostgreSQLUserMappingReadImpl(db *DBConnection, d *schema.ResourceD mappedOptions[pair[0]] = pair[1] } - d.Set(userMappingUserNameAttr, username) + if usernameSpecial != "" { + d.Set(userMappingUserNameAttr, usernameSpecial) + } else { + d.Set(userMappingUserNameAttr, username) + } d.Set(userMappingServerNameAttr, serverName) d.Set(userMappingOptionsAttr, mappedOptions) d.SetId(generateUserMappingID(d)) @@ -161,7 +186,7 @@ func resourcePostgreSQLUserMappingDelete(db *DBConnection, d *schema.ResourceDat } defer deferredRollback(txn) - sql := fmt.Sprintf("DROP USER MAPPING FOR %s SERVER %s ", pq.QuoteIdentifier(username), pq.QuoteIdentifier(serverName)) + sql := fmt.Sprintf("DROP USER MAPPING FOR %s SERVER %s ", quoteUsername(username), pq.QuoteIdentifier(serverName)) if _, err := txn.Exec(sql); err != nil { return err } @@ -199,7 +224,7 @@ func setUserMappingOptionsIfChanged(db *DBConnection, d *schema.ResourceData) er serverName := d.Get(userMappingServerNameAttr).(string) b := bytes.NewBufferString("ALTER USER MAPPING ") - fmt.Fprintf(b, " FOR %s SERVER %s ", pq.QuoteIdentifier(username), pq.QuoteIdentifier(serverName)) + fmt.Fprintf(b, " FOR %s SERVER %s ", quoteUsername(username), pq.QuoteIdentifier(serverName)) oldOptions, newOptions := d.GetChange(userMappingOptionsAttr) fmt.Fprint(b, " OPTIONS ( ") diff --git a/postgresql/resource_postgresql_user_mapping_test.go b/postgresql/resource_postgresql_user_mapping_test.go index 4d17fb10..81f07b49 100644 --- a/postgresql/resource_postgresql_user_mapping_test.go +++ b/postgresql/resource_postgresql_user_mapping_test.go @@ -80,6 +80,65 @@ func TestAccPostgresqlUserMapping_Update(t *testing.T) { }) } +func TestAccPostgresqlUserMapping_Specials(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featureServer) + testSuperuserPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlUserMappingDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPostgresqlUserMappingSpecialPublicLower, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlUserMappingExists("postgresql_user_mapping.remote"), + resource.TestCheckResourceAttr( + "postgresql_user_mapping.remote", "user_name", "public"), + testCheckUserMappingExists("public", "myserver_postgres"), + ), + }, + { + Config: testAccPostgresqlUserMappingSpecialCurrentRole, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), + resource.TestCheckResourceAttr( + "postgresql_user_mapping.remote", "user_name", "CURRENT_ROLE"), + testCheckUserMappingExists("postgres", "myserver_postgres"), + ), + }, + { + Config: testAccPostgresqlUserMappingSpecialCurrentUser, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), + resource.TestCheckResourceAttr( + "postgresql_user_mapping.remote", "user_name", "CURRENT_USER"), + testCheckUserMappingExists("postgres", "myserver_postgres"), + ), + }, + { + Config: testAccPostgresqlUserMappingSpecialPublic, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), + resource.TestCheckResourceAttr( + "postgresql_user_mapping.remote", "user_name", "PUBLIC"), + testCheckUserMappingExists("public", "myserver_postgres"), + ), + }, + { + Config: testAccPostgresqlUserMappingSpecialUser, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlServerExists("postgresql_user_mapping.remote"), + resource.TestCheckResourceAttr( + "postgresql_user_mapping.remote", "user_name", "USER"), + testCheckUserMappingExists("postgres", "myserver_postgres"), + ), + }, + }, + }) +} + func checkUserMappingExists(txn *sql.Tx, username string, serverName string) (bool, error) { var _rez bool err := txn.QueryRow("SELECT TRUE FROM pg_user_mappings WHERE usename = $1 AND srvname = $2", username, serverName).Scan(&_rez) @@ -108,14 +167,16 @@ func testAccCheckPostgresqlUserMappingDestroy(s *terraform.State) error { defer deferredRollback(txn) splitted := strings.Split(rs.Primary.ID, ".") - exists, err := checkUserMappingExists(txn, splitted[0], splitted[1]) + username := splitted[0] + serverName := splitted[1] + exists, err := checkUserMappingExists(txn, username, serverName) if err != nil { return fmt.Errorf("Error checking user mapping %s", err) } if exists { - return fmt.Errorf("User mapping still exists after destroy") + return fmt.Errorf("User mapping (%s) for server (%s) still exists after destroy", username, serverName) } } @@ -157,7 +218,30 @@ func testAccCheckPostgresqlUserMappingExists(n string) resource.TestCheckFunc { } if !exists { - return fmt.Errorf("User mapping not found") + return fmt.Errorf("User mapping (%s) for server (%s) not found", username, serverName) + } + + return nil + } +} + +func testCheckUserMappingExists(username string, serverName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + txn, err := startTransaction(client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + exists, err := checkUserMappingExists(txn, username, serverName) + + if err != nil { + return fmt.Errorf("Error checking user mapping %s", err) + } + + if !exists { + return fmt.Errorf("User mapping (%s) for server (%s) not found", username, serverName) } return nil @@ -166,102 +250,233 @@ func testAccCheckPostgresqlUserMappingExists(n string) resource.TestCheckFunc { var testAccPostgresqlUserMappingConfig = ` resource "postgresql_extension" "ext_postgres_fdw" { - name = "postgres_fdw" + name = "postgres_fdw" } resource "postgresql_server" "myserver_postgres" { - server_name = "myserver_postgres" - fdw_name = "postgres_fdw" - options = { - host = "foo" - dbname = "foodb" - port = "5432" - } + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } - depends_on = [postgresql_extension.ext_postgres_fdw] + depends_on = [postgresql_extension.ext_postgres_fdw] } resource "postgresql_role" "remote" { - name = "remote" + name = "remote" } resource "postgresql_user_mapping" "remote" { - server_name = postgresql_server.myserver_postgres.server_name - user_name = postgresql_role.remote.name - options = { - user = "admin" - password = "pass" - } + server_name = postgresql_server.myserver_postgres.server_name + user_name = postgresql_role.remote.name + options = { + user = "admin" + password = "pass" + } } resource "postgresql_role" "special" { - name = "special" + name = "special" } resource "postgresql_user_mapping" "special_chars" { - server_name = postgresql_server.myserver_postgres.server_name - user_name = postgresql_role.special.name - options = { - user = "admin" - password = "pass=$*'" - } + server_name = postgresql_server.myserver_postgres.server_name + user_name = postgresql_role.special.name + options = { + user = "admin" + password = "pass=$*'" + } } ` var testAccPostgresqlUserMappingChanges2 = ` resource "postgresql_extension" "ext_postgres_fdw" { name = "postgres_fdw" - } - - resource "postgresql_server" "myserver_postgres" { +} + +resource "postgresql_server" "myserver_postgres" { server_name = "myserver_postgres" fdw_name = "postgres_fdw" options = { - host = "foo" - dbname = "foodb" - port = "5432" + host = "foo" + dbname = "foodb" + port = "5432" } - + depends_on = [postgresql_extension.ext_postgres_fdw] - } - - resource "postgresql_role" "remote" { +} + +resource "postgresql_role" "remote" { name = "remote" - } - - resource "postgresql_user_mapping" "remote" { +} + +resource "postgresql_user_mapping" "remote" { server_name = postgresql_server.myserver_postgres.server_name user_name = postgresql_role.remote.name options = { - user = "admin" - password = "passUpdated" + user = "admin" + password = "passUpdated" } - } +} ` var testAccPostgresqlUserMappingChanges3 = ` resource "postgresql_extension" "ext_postgres_fdw" { name = "postgres_fdw" - } - - resource "postgresql_server" "myserver_postgres" { +} + +resource "postgresql_server" "myserver_postgres" { server_name = "myserver_postgres" fdw_name = "postgres_fdw" options = { - host = "foo" - dbname = "foodb" - port = "5432" + host = "foo" + dbname = "foodb" + port = "5432" } - + depends_on = [postgresql_extension.ext_postgres_fdw] - } - - resource "postgresql_role" "remote" { - name = "remote" - } - - resource "postgresql_user_mapping" "remote" { +} + +resource "postgresql_role" "remote" { +name = "remote" +} + +resource "postgresql_user_mapping" "remote" { server_name = postgresql_server.myserver_postgres.server_name user_name = postgresql_role.remote.name - } +} +` + +var testAccPostgresqlUserMappingSpecialCurrentRole = ` +resource "postgresql_extension" "ext_postgres_fdw" { + name = "postgres_fdw" +} + +resource "postgresql_server" "myserver_postgres" { + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } + + depends_on = [postgresql_extension.ext_postgres_fdw] +} + +resource "postgresql_user_mapping" "remote" { + server_name = postgresql_server.myserver_postgres.server_name + user_name = "CURRENT_ROLE" +} +` + +var testAccPostgresqlUserMappingSpecialCurrentUser = ` +resource "postgresql_extension" "ext_postgres_fdw" { + name = "postgres_fdw" +} + +resource "postgresql_server" "myserver_postgres" { + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } + + depends_on = [postgresql_extension.ext_postgres_fdw] +} + +resource "postgresql_user_mapping" "remote" { + server_name = postgresql_server.myserver_postgres.server_name + user_name = "CURRENT_USER" + options = { + user = "admin" + password = "pass" + } +} +` + +var testAccPostgresqlUserMappingSpecialPublicLower = ` +resource "postgresql_extension" "ext_postgres_fdw" { + name = "postgres_fdw" +} + +resource "postgresql_server" "myserver_postgres" { + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } + + depends_on = [postgresql_extension.ext_postgres_fdw] +} + +resource "postgresql_user_mapping" "remote" { + server_name = postgresql_server.myserver_postgres.server_name + user_name = "public" + options = { + user = "admin" + password = "pass" + } +} +` + +var testAccPostgresqlUserMappingSpecialPublic = ` +resource "postgresql_extension" "ext_postgres_fdw" { + name = "postgres_fdw" +} + +resource "postgresql_server" "myserver_postgres" { + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } + + depends_on = [postgresql_extension.ext_postgres_fdw] +} + +resource "postgresql_user_mapping" "remote" { + server_name = postgresql_server.myserver_postgres.server_name + user_name = "PUBLIC" + options = { + user = "admin" + password = "pass" + } +} +` + +var testAccPostgresqlUserMappingSpecialUser = ` +resource "postgresql_extension" "ext_postgres_fdw" { + name = "postgres_fdw" +} + +resource "postgresql_server" "myserver_postgres" { + server_name = "myserver_postgres" + fdw_name = "postgres_fdw" + options = { + host = "foo" + dbname = "foodb" + port = "5432" + } + + depends_on = [postgresql_extension.ext_postgres_fdw] +} + +resource "postgresql_user_mapping" "remote" { + server_name = postgresql_server.myserver_postgres.server_name + user_name = "USER" + options = { + user = "admin" + password = "pass'" + } +} `