diff --git a/postgresql/resource_postgresql_role.go b/postgresql/resource_postgresql_role.go index 5b346f3b..1186496c 100644 --- a/postgresql/resource_postgresql_role.go +++ b/postgresql/resource_postgresql_role.go @@ -33,6 +33,7 @@ const ( roleValidUntilAttr = "valid_until" roleRolesAttr = "roles" roleSearchPathAttr = "search_path" + roleSearchPathDBAttr = "search_path_db" roleStatementTimeoutAttr = "statement_timeout" roleAssumeRoleAttr = "assume_role" @@ -83,6 +84,12 @@ func resourcePostgreSQLRole() *schema.Resource { MinItems: 0, Description: "Sets the role's search path", }, + roleSearchPathDBAttr: { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Sets the role's search path for specific databases", + }, roleEncryptedPassAttr: { Type: schema.TypeBool, Optional: true, @@ -299,6 +306,10 @@ func resourcePostgreSQLRoleCreate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = alterSearchPathDB(txn, d); err != nil { + return err + } + if err = setStatementTimeout(txn, d); err != nil { return err } @@ -456,6 +467,13 @@ func resourcePostgreSQLRoleReadImpl(db *DBConnection, d *schema.ResourceData) er d.Set(roleBypassRLSAttr, roleBypassRLS) d.Set(roleRolesAttr, pgArrayToSet(roleRoles)) d.Set(roleSearchPathAttr, readSearchPath(roleConfig)) + + searchPathDB, err := readSearchPathDB(db, roleName) + if err != nil { + return err + } + d.Set(roleSearchPathDBAttr, searchPathDB) + d.Set(roleAssumeRoleAttr, readAssumeRole(roleConfig)) statementTimeout, err := readStatementTimeout(roleConfig) @@ -499,6 +517,48 @@ func readSearchPath(roleConfig pq.ByteaArray) []string { return nil } +// readSearchPathDB reads database-specific search_path values for a role. +// It returns a map of database names to search_path values. +func readSearchPathDB(db *DBConnection, roleName string) (map[string]interface{}, error) { + searchPathDBMap := make(map[string]interface{}) + + // Query to get database-specific search_path settings + query := ` + SELECT d.datname, unnest(rs.setconfig) as config + FROM pg_db_role_setting rs + JOIN pg_database d ON rs.setdatabase = d.oid + JOIN pg_roles r ON rs.setrole = r.oid + WHERE r.rolname = $1 + ` + + rows, err := db.Query(query, roleName) + if err != nil { + return searchPathDBMap, fmt.Errorf("Error reading search_path_db settings: %w", err) + } + defer rows.Close() + + for rows.Next() { + var dbName, configItem string + if err := rows.Scan(&dbName, &configItem); err != nil { + return searchPathDBMap, fmt.Errorf("Error scanning search_path_db row: %w", err) + } + + // Only process search_path settings + if strings.HasPrefix(configItem, "search_path=") { + path := strings.TrimPrefix(configItem, "search_path=") + // Remove quotes if present + path = strings.Trim(path, `"`) + searchPathDBMap[dbName] = path + } + } + + if err := rows.Err(); err != nil { + return searchPathDBMap, fmt.Errorf("Error iterating search_path_db rows: %w", err) + } + + return searchPathDBMap, nil +} + // readIdleInTransactionSessionTimeout searches for an idle_in_transaction_session_timeout entry in the rolconfig array. // In case no such value is present, it returns nil. func readIdleInTransactionSessionTimeout(roleConfig pq.ByteaArray) (int, error) { @@ -677,6 +737,10 @@ func resourcePostgreSQLRoleUpdate(db *DBConnection, d *schema.ResourceData) erro return err } + if err = alterSearchPathDB(txn, d); err != nil { + return err + } + if err = setStatementTimeout(txn, d); err != nil { return err } @@ -988,6 +1052,40 @@ func alterSearchPath(txn *sql.Tx, d *schema.ResourceData) error { return nil } +func alterSearchPathDB(txn *sql.Tx, d *schema.ResourceData) error { + role := d.Get(roleNameAttr).(string) + searchPathDBMap := d.Get(roleSearchPathDBAttr).(map[string]interface{}) + + // Nothing to do if map is empty + if len(searchPathDBMap) == 0 { + return nil + } + + for dbName, searchPathValue := range searchPathDBMap { + searchPathString := searchPathValue.(string) + if strings.Contains(searchPathString, ", ") { + return fmt.Errorf("search_path_db values cannot contain `, `: %v", searchPathString) + } + + // Verify the searchPathString isn't empty + if searchPathString == "" { + return fmt.Errorf("empty search_path value for database %s", dbName) + } + + query := fmt.Sprintf( + "ALTER ROLE %s IN DATABASE %s SET search_path TO %s", + pq.QuoteIdentifier(role), + pq.QuoteIdentifier(dbName), + pq.QuoteIdentifier(searchPathString), + ) + if _, err := txn.Exec(query); err != nil { + return fmt.Errorf("could not set search_path %s for %s in database %s: %w", + searchPathString, role, dbName, err) + } + } + return nil +} + func setStatementTimeout(txn *sql.Tx, d *schema.ResourceData) error { if !d.HasChange(roleStatementTimeoutAttr) { return nil diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index fc958840..308b57d3 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -446,4 +446,12 @@ resource "postgresql_role" "role_with_search_path" { name = "role_with_search_path" search_path = ["bar", "foo-with-hyphen"] } + +resource "postgresql_role" "role_with_search_path_db" { + name = "role_with_search_path_db" + search_path_db = { + "postgres" = "schema1" + "template1" = "schema2" + } +} ` diff --git a/website/docs/r/postgresql_role.html.markdown b/website/docs/r/postgresql_role.html.markdown index 2c9431be..cb2395a5 100644 --- a/website/docs/r/postgresql_role.html.markdown +++ b/website/docs/r/postgresql_role.html.markdown @@ -91,6 +91,12 @@ resource "postgresql_role" "my_replication_role" { due to limitations in the implementation, values cannot contain the substring `", "`. +* `search_path_db` - (Optional) Alters the search path of this role for specific databases. + This is a map where keys are database names and values are schema names. + For example, setting `search_path_db = { "db1" = "schema1", "db2" = "schema2" }` will + generate `ALTER ROLE role_name IN DATABASE db1 SET search_path TO schema1` and + `ALTER ROLE role_name IN DATABASE db2 SET search_path TO schema2`. + * `valid_until` - (Optional) Defines the date and time after which the role's password is no longer valid. Established connections past this `valid_time` will have to be manually terminated. This value corresponds to a PostgreSQL