Skip to content

fix #526: adding support for search_path_db #527

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions postgresql/resource_postgresql_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
roleValidUntilAttr = "valid_until"
roleRolesAttr = "roles"
roleSearchPathAttr = "search_path"
roleSearchPathDBAttr = "search_path_db"
roleStatementTimeoutAttr = "statement_timeout"
roleAssumeRoleAttr = "assume_role"

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions postgresql/resource_postgresql_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
`
6 changes: 6 additions & 0 deletions website/docs/r/postgresql_role.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down