Skip to content

Commit 5800c64

Browse files
committed
update example with app and ro users
1 parent 96e2d39 commit 5800c64

File tree

9 files changed

+409
-7
lines changed

9 files changed

+409
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[![Banner][banner-image]](https://masterpoint.io/)
1+
![Banner][banner-image]](https://masterpoint.io/)
22

33
# terraform-postgres-logical-dbs
44

examples/complete/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Example: Complete Setup for PostgreSQL Logical Databases
2+
3+
This example demonstrates how to set up PostgreSQL logical databases using Terraform. It includes configurations for both an application user and a read-only user.
4+
5+
## Prerequisites
6+
7+
- Terraform installed on your local machine.
8+
- Access to a PostgreSQL instance where you can apply these configurations.
9+
10+
## Usage
11+
12+
1. Clone the repository and navigate to the `examples/complete` directory.
13+
2. Review and update the `fixtures.tfvars` file with your specific configuration details.
14+
3. Run the following Terraform commands to apply the configuration:
15+
16+
```bash
17+
terraform init
18+
terraform plan -var-file="fixtures.tfvars"
19+
terraform apply -var-file="fixtures.tfvars"
20+
```
21+
22+
## Roles and Permissions
23+
24+
The `fixtures.tfvars` file defines two roles:
25+
26+
- **app1_app_user**: This role is intended for application use with the following permissions:
27+
28+
- Can log in and is not a superuser.
29+
- Has all privileges on tables and sequences in the `app1_db` database.
30+
- Can use and create within the `public` schema.
31+
32+
- **app1_readonly_user**: This role is intended for read-only access with the following permissions:
33+
- Can log in and is not a superuser.
34+
- Has `SELECT` privileges on tables and `USAGE`, `SELECT` on sequences in the `app1_db` database.

examples/complete/fixtures.tfvars

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,106 @@ db_password = "insecure-pass-for-demo"
88
db_scheme = "postgres"
99
db_hostname = "localhost"
1010
db_port = 5432
11-
db_superuser = true
11+
db_superuser = false
1212
db_sslmode = "disable"
1313

1414
databases = [
1515
{
1616
name = "app1_db"
1717
connection_limit = 10
18-
},
18+
}
19+
]
20+
21+
roles = [
1922
{
20-
name = "app2_db"
21-
connection_limit = 20
23+
role = {
24+
name = "app1_app_user"
25+
login = true
26+
superuser = false
27+
password = "securepassword1"
28+
}
29+
30+
table_grants = {
31+
role = "app1_app_user"
32+
database = "app1_db"
33+
schema = "public"
34+
object_type = "table"
35+
objects = [] # empty list to grant all tables
36+
privileges = ["ALL"]
37+
}
38+
39+
schema_grants = {
40+
role = "app1_app_user"
41+
database = "app1_db"
42+
schema = "public"
43+
object_type = "schema"
44+
privileges = ["USAGE", "CREATE"]
45+
}
46+
47+
sequence_grants = {
48+
role = "app1_app_user"
49+
database = "app1_db"
50+
schema = "public"
51+
object_type = "sequence"
52+
objects = [] # empty list to grant all sequences
53+
privileges = ["ALL"]
54+
}
55+
56+
default_privileges = [{
57+
role = "app1_app_user"
58+
database = "app1_db"
59+
schema = "public"
60+
owner = "app1_app_user"
61+
object_type = "table"
62+
objects = [] # empty list to grant all tables
63+
privileges = ["DELETE", "INSERT", "REFERENCES", "SELECT", "TRIGGER", "TRUNCATE", "UPDATE"]
64+
}]
2265
},
2366
{
24-
name = "app3_db"
25-
connection_limit = 30
67+
role = {
68+
name = "app1_readonly_user"
69+
login = true
70+
password = "readonlypassword1"
71+
superuser = false
72+
}
73+
74+
table_grants = {
75+
role = "app1_readonly_user"
76+
database = "app1_db"
77+
schema = "public"
78+
object_type = "table"
79+
objects = [] # empty list to grant all tables
80+
privileges = ["SELECT"]
81+
}
82+
83+
sequence_grants = {
84+
role = "app1_readonly_user"
85+
database = "app1_db"
86+
schema = "public"
87+
object_type = "sequence"
88+
objects = [] # empty list to grant all sequences
89+
privileges = ["USAGE", "SELECT"]
90+
}
91+
92+
default_privileges = [
93+
{
94+
role = "app1_readonly_user"
95+
database = "app1_db"
96+
schema = "public"
97+
owner = "app1_app_user"
98+
object_type = "table"
99+
objects = [] # empty list to grant all tables
100+
privileges = ["SELECT"]
101+
},
102+
{
103+
role = "app1_readonly_user"
104+
database = "app1_db"
105+
schema = "public"
106+
owner = "app1_app_user"
107+
object_type = "sequence"
108+
objects = [] # empty list to grant all sequences
109+
privileges = ["USAGE", "SELECT"]
110+
},
111+
]
26112
}
27113
]

examples/complete/main.tf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ module "app_dbs" {
44
source = "../../"
55

66
databases = var.databases
7+
8+
roles = var.roles
79
}

examples/complete/outputs.tf

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
output "database_access" {
2+
value = module.app_dbs.database_access
3+
}
4+
5+
output "default_privileges" {
6+
value = module.app_dbs.default_privileges
7+
}
8+
9+
output "schema_access" {
10+
value = module.app_dbs.schema_access
11+
}
12+
13+
output "table_access" {
14+
value = module.app_dbs.table_access
15+
}

examples/complete/variables.tf

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,68 @@ variable "databases" {
4141
connection_limit = number
4242
}))
4343
}
44+
45+
46+
variable "roles" {
47+
type = list(object({
48+
role = object({
49+
# See defaults: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/postgresql_role
50+
name = string
51+
superuser = optional(bool)
52+
create_database = optional(bool)
53+
create_role = optional(bool)
54+
inherit = optional(bool)
55+
login = optional(bool)
56+
replication = optional(bool)
57+
bypass_row_level_security = optional(bool)
58+
connection_limit = optional(number)
59+
encrypted_password = optional(bool)
60+
password = optional(string)
61+
roles = optional(list(string))
62+
search_path = optional(list(string))
63+
valid_until = optional(string)
64+
skip_drop_role = optional(bool)
65+
skip_reassign_owned = optional(bool)
66+
statement_timeout = optional(number)
67+
assume_role = optional(string)
68+
})
69+
default_privileges = optional(list(object({
70+
role = string
71+
database = string
72+
schema = string
73+
owner = string
74+
object_type = string
75+
privileges = list(string)
76+
})))
77+
database_grants = optional(object({
78+
role = string
79+
database = string
80+
object_type = string
81+
privileges = list(string)
82+
}))
83+
schema_grants = optional(object({
84+
role = string
85+
database = string
86+
schema = string
87+
object_type = string
88+
privileges = list(string)
89+
}))
90+
table_grants = optional(object({
91+
role = string
92+
database = string
93+
schema = string
94+
object_type = string
95+
objects = list(string)
96+
privileges = list(string)
97+
}))
98+
sequence_grants = optional(object({
99+
role = string
100+
database = string
101+
schema = string
102+
object_type = string
103+
objects = list(string)
104+
privileges = list(string)
105+
}))
106+
}))
107+
description = "List of static postgres roles to create and related permissions. These are for applications that use static credentials and don't use IAM DB Auth. See defaults: https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/postgresql_role"
108+
}

main.tf

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,120 @@ resource "postgresql_database" "logical_db" {
33
name = each.key
44
connection_limit = each.value.connection_limit
55
}
6+
7+
8+
locals {
9+
roles_with_passwords = [for idx, role_data in var.roles : merge(role_data,
10+
{
11+
role : merge(role_data["role"],
12+
lookup(role_data["role"], "password", null) != null ? # Or if it's empty string?
13+
{
14+
password : role_data["role"]["password"]
15+
} :
16+
{
17+
password : random_password.user_password[idx].result
18+
}
19+
)
20+
}
21+
)]
22+
23+
_database_grants = [for role in local.roles_with_passwords : role.database_grants if try(role.database_grants, null) != null]
24+
_default_privileges = flatten([for role in local.roles_with_passwords : role.default_privileges if try(role.default_privileges, null) != null])
25+
_schema_grants = [for role in local.roles_with_passwords : role.schema_grants if try(role.schema_grants, null) != null]
26+
_sequence_grants = [for role in local.roles_with_passwords : role.sequence_grants if try(role.sequence_grants, null) != null]
27+
_table_grants = [for role in local.roles_with_passwords : role.table_grants if try(role.table_grants, null) != null]
28+
}
29+
30+
# If no password passed in, then use this to generate one
31+
resource "random_password" "user_password" {
32+
count = length(var.roles)
33+
34+
length = 33
35+
# Leave special characters out to avoid quoting and other issues.
36+
# Special characters have no additional security compared to increasing length.
37+
special = false
38+
override_special = "!#$%^&*()<>-_"
39+
}
40+
41+
# In Postgres 15, now new users cannot create tables or write data to Postgres public schema by default. You have to grant create privilege to the new user manually.
42+
# https://www.postgresql.org/docs/current/ddl-priv.html#DDL-PRIV-CREATE
43+
resource "postgresql_role" "role" {
44+
depends_on = [postgresql_database.logical_db]
45+
46+
for_each = { for role in local.roles_with_passwords : role.role.name => role }
47+
48+
name = each.value.role.name
49+
superuser = each.value.role.superuser
50+
create_database = each.value.role.create_database
51+
create_role = each.value.role.create_role
52+
inherit = each.value.role.inherit
53+
login = each.value.role.login
54+
replication = each.value.role.replication
55+
bypass_row_level_security = each.value.role.bypass_row_level_security
56+
connection_limit = each.value.role.connection_limit
57+
encrypted_password = each.value.role.encrypted_password
58+
password = each.value.role.password
59+
roles = each.value.role.roles
60+
search_path = each.value.role.search_path
61+
valid_until = each.value.role.valid_until
62+
skip_drop_role = each.value.role.skip_drop_role
63+
skip_reassign_owned = each.value.role.skip_reassign_owned
64+
statement_timeout = each.value.role.statement_timeout
65+
assume_role = each.value.role.assume_role
66+
}
67+
68+
resource "postgresql_grant" "database_access" {
69+
70+
for_each = { for grant in local._database_grants : format("%s-%s", grant.role, grant.database) => grant }
71+
72+
role = each.value.role
73+
database = each.value.database
74+
object_type = each.value.object_type
75+
privileges = each.value.privileges
76+
}
77+
78+
resource "postgresql_grant" "schema_access" {
79+
80+
for_each = { for grant in local._schema_grants : format("%s-%s-%s", grant.role, grant.schema, grant.database) => grant }
81+
82+
role = each.value.role
83+
database = each.value.database
84+
schema = each.value.schema
85+
object_type = each.value.object_type
86+
privileges = each.value.privileges
87+
}
88+
89+
resource "postgresql_grant" "table_access" {
90+
91+
for_each = { for grant in local._table_grants : format("%s-%s-%s", grant.role, grant.schema, grant.database) => grant }
92+
93+
role = each.value.role
94+
database = each.value.database
95+
schema = each.value.schema
96+
object_type = each.value.object_type
97+
privileges = each.value.privileges
98+
objects = each.value.objects
99+
}
100+
101+
resource "postgresql_grant" "sequence_access" {
102+
103+
for_each = { for grant in local._sequence_grants : format("%s-%s-%s", grant.role, grant.schema, grant.database) => grant }
104+
105+
role = each.value.role
106+
database = each.value.database
107+
schema = each.value.schema
108+
object_type = each.value.object_type
109+
privileges = each.value.privileges
110+
}
111+
112+
resource "postgresql_default_privileges" "privileges" {
113+
114+
for_each = { for grant in local._default_privileges : format("%s-%s-%s-%s", grant.role, grant.database, grant.schema, grant.object_type) => grant }
115+
116+
role = each.value.role
117+
database = each.value.database
118+
schema = each.value.schema
119+
owner = each.value.owner
120+
object_type = each.value.object_type
121+
privileges = each.value.privileges
122+
}

outputs.tf

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
output "database_access" {
2+
value = postgresql_grant.database_access
3+
}
4+
5+
output "schema_access" {
6+
value = postgresql_grant.schema_access
7+
}
8+
9+
output "table_access" {
10+
value = postgresql_grant.table_access
11+
}
12+
13+
output "sequence_access" {
14+
value = postgresql_grant.sequence_access
15+
}
16+
17+
output "default_privileges" {
18+
value = postgresql_default_privileges.privileges
19+
}

0 commit comments

Comments
 (0)