Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ module "postgres_automation" {
| [postgresql_grant.schema_access](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
| [postgresql_grant.sequence_access](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
| [postgresql_grant.table_access](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/grant) | resource |
| [postgresql_role.role](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
| [postgresql_role.base_role](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
| [postgresql_role.dependent_role](https://registry.terraform.io/providers/cyrilgdn/postgresql/latest/docs/resources/role) | resource |
| [random_password.user_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |

## Inputs
Expand Down Expand Up @@ -193,14 +194,17 @@ module "postgres_automation" {

## Outputs

| Name | Description |
| ----------------------------------------------------------------------------------------- | ----------- |
| <a name="output_database_access"></a> [database_access](#output_database_access) | n/a |
| <a name="output_databases"></a> [databases](#output_databases) | n/a |
| <a name="output_default_privileges"></a> [default_privileges](#output_default_privileges) | n/a |
| <a name="output_schema_access"></a> [schema_access](#output_schema_access) | n/a |
| <a name="output_sequence_access"></a> [sequence_access](#output_sequence_access) | n/a |
| <a name="output_table_access"></a> [table_access](#output_table_access) | n/a |
| Name | Description |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| <a name="output_base_roles"></a> [base_roles](#output_base_roles) | Base roles (group roles, no dependencies on other custom roles) |
| <a name="output_database_access"></a> [database_access](#output_database_access) | n/a |
| <a name="output_databases"></a> [databases](#output_databases) | n/a |
| <a name="output_default_privileges"></a> [default_privileges](#output_default_privileges) | n/a |
| <a name="output_dependent_roles"></a> [dependent_roles](#output_dependent_roles) | Dependent roles (login roles that inherit from other custom roles) |
| <a name="output_roles"></a> [roles](#output_roles) | All created roles (both base and dependent) |
| <a name="output_schema_access"></a> [schema_access](#output_schema_access) | n/a |
| <a name="output_sequence_access"></a> [sequence_access](#output_sequence_access) | n/a |
| <a name="output_table_access"></a> [table_access](#output_table_access) | n/a |

<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->

Expand Down
41 changes: 41 additions & 0 deletions examples/llm_chat_app/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions examples/llm_chat_app/1_apply_terraform.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash
# Task 1: Apply Terraform Configuration

set -e

echo "=============================================="
echo "Applying Terraform Configuration"
echo "=============================================="
echo ""

cd /Users/weston/clients/masterpoint/terraform-postgres-config-dbs-users-roles/examples/llm_chat_app
Comment thread
westonplatter marked this conversation as resolved.
Outdated

tofu apply -auto-approve

echo ""
echo "=============================================="
echo "Terraform Apply Completed Successfully!"
echo "=============================================="
54 changes: 54 additions & 0 deletions examples/llm_chat_app/2_create_test_objects.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash
# Create test objects for verification

set -e

export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=llm_service

echo "=============================================="
echo "Creating Test Objects"
echo "=============================================="
echo ""

PGUSER=role_service_migration PGPASSWORD=demo-password-migration psql <<'EOF'
-- Create test table in app schema
CREATE TABLE IF NOT EXISTS app.test_users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO app.test_users (name) VALUES ('test') ON CONFLICT DO NOTHING;

-- Create test view in app schema
CREATE OR REPLACE VIEW app.test_users_view AS SELECT * FROM app.test_users;

-- Create test function in app schema
CREATE OR REPLACE FUNCTION app.test_func() RETURNS integer
LANGUAGE sql SECURITY INVOKER
AS $$ SELECT 1; $$;

-- Create test table in ref_data schemas
CREATE TABLE IF NOT EXISTS ref_data_pipeline_abc.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_abc.test_ref (value) VALUES ('abc') ON CONFLICT DO NOTHING;

CREATE TABLE IF NOT EXISTS ref_data_pipeline_xyz.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_xyz.test_ref (value) VALUES ('xyz') ON CONFLICT DO NOTHING;

-- Create views in ref_data schemas
CREATE OR REPLACE VIEW ref_data_pipeline_abc.test_ref_view AS SELECT * FROM ref_data_pipeline_abc.test_ref;
CREATE OR REPLACE VIEW ref_data_pipeline_xyz.test_ref_view AS SELECT * FROM ref_data_pipeline_xyz.test_ref;

SELECT 'Test objects created successfully!' AS result;
EOF
Comment on lines +4 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n examples/llm_chat_app/2_create_test_objects.sh

Repository: masterpointio/terraform-postgres-config-dbs-users-roles

Length of output: 2381


🌐 Web query:

psql ON_ERROR_STOP behavior exit code sql errors

💡 Result:

psql’s ON_ERROR_STOP controls whether psql keeps going after an error or stops immediately.

Behavior

  • Default (ON_ERROR_STOP off): psql reports SQL (or meta-command) errors but continues processing subsequent commands. (postgresql.org)
  • With ON_ERROR_STOP=on (or \set ON_ERROR_STOP on):
    • Non-interactive/script mode (-f, stdin, etc.): psql terminates immediately when the first error occurs. (postgresql.org)
    • Interactive mode: psql stops processing the current batch and returns you to the prompt. (postgresql.org)

Exit codes (what $? / process status means)

psql uses these exit statuses:

  • 0: finished normally
  • 1: fatal error in psql itself (or other “fatal” conditions)
  • 2: connection to server failed and session was not interactive
  • 3: an error occurred while running a script and ON_ERROR_STOP was set (superscript.com)

Practical implication

If you want CI/shell scripts to fail on the first SQL error, use e.g.

  • psql -v ON_ERROR_STOP=1 -f script.sql
  • or inside scripts: \set ON_ERROR_STOP on (postgresql.org)

Citations:


Add ON_ERROR_STOP to fail the script on SQL errors.

The set -e at the top of the script will only catch non-zero exit codes from psql. Without ON_ERROR_STOP=1, SQL errors in the here-doc don't cause psql to exit with a non-zero status—it continues executing and exits 0, masking failures. Add -v ON_ERROR_STOP=1 to ensure the script fails on the first SQL error.

🛠️ Proposed fix
-PGUSER=service_migrator PGPASSWORD=demo-password-migrator psql <<'EOF'
+PGUSER=service_migrator PGPASSWORD=demo-password-migrator psql -v ON_ERROR_STOP=1 <<'EOF'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set -e
export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=llm_service
echo "=============================================="
echo "Creating Test Objects"
echo "=============================================="
echo ""
PGUSER=service_migrator PGPASSWORD=demo-password-migrator psql <<'EOF'
-- Switch to group role so objects are owned by role_service_migration
-- This ensures default privileges apply correctly
SET ROLE role_service_migration;
-- Create test table in app schema
CREATE TABLE IF NOT EXISTS app.test_users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO app.test_users (name) VALUES ('test') ON CONFLICT DO NOTHING;
-- Create test view in app schema
CREATE OR REPLACE VIEW app.test_users_view AS SELECT * FROM app.test_users;
-- Create test function in app schema
CREATE OR REPLACE FUNCTION app.test_func() RETURNS integer
LANGUAGE sql SECURITY INVOKER
AS $$ SELECT 1; $$;
-- Create test table in ref_data schemas
CREATE TABLE IF NOT EXISTS ref_data_pipeline_abc.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_abc.test_ref (value) VALUES ('abc') ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS ref_data_pipeline_xyz.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_xyz.test_ref (value) VALUES ('xyz') ON CONFLICT DO NOTHING;
-- Create views in ref_data schemas
CREATE OR REPLACE VIEW ref_data_pipeline_abc.test_ref_view AS SELECT * FROM ref_data_pipeline_abc.test_ref;
CREATE OR REPLACE VIEW ref_data_pipeline_xyz.test_ref_view AS SELECT * FROM ref_data_pipeline_xyz.test_ref;
SELECT 'Test objects created successfully!' AS result;
EOF
set -e
export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=llm_service
echo "=============================================="
echo "Creating Test Objects"
echo "=============================================="
echo ""
PGUSER=service_migrator PGPASSWORD=demo-password-migrator psql -v ON_ERROR_STOP=1 <<'EOF'
-- Switch to group role so objects are owned by role_service_migration
-- This ensures default privileges apply correctly
SET ROLE role_service_migration;
-- Create test table in app schema
CREATE TABLE IF NOT EXISTS app.test_users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO app.test_users (name) VALUES ('test') ON CONFLICT DO NOTHING;
-- Create test view in app schema
CREATE OR REPLACE VIEW app.test_users_view AS SELECT * FROM app.test_users;
-- Create test function in app schema
CREATE OR REPLACE FUNCTION app.test_func() RETURNS integer
LANGUAGE sql SECURITY INVOKER
AS $$ SELECT 1; $$;
-- Create test table in ref_data schemas
CREATE TABLE IF NOT EXISTS ref_data_pipeline_abc.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_abc.test_ref (value) VALUES ('abc') ON CONFLICT DO NOTHING;
CREATE TABLE IF NOT EXISTS ref_data_pipeline_xyz.test_ref (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO ref_data_pipeline_xyz.test_ref (value) VALUES ('xyz') ON CONFLICT DO NOTHING;
-- Create views in ref_data schemas
CREATE OR REPLACE VIEW ref_data_pipeline_abc.test_ref_view AS SELECT * FROM ref_data_pipeline_abc.test_ref;
CREATE OR REPLACE VIEW ref_data_pipeline_xyz.test_ref_view AS SELECT * FROM ref_data_pipeline_xyz.test_ref;
SELECT 'Test objects created successfully!' AS result;
EOF
🤖 Prompt for AI Agents
In `@examples/llm_chat_app/2_create_test_objects.sh` around lines 4 - 53, The psql
invocation that runs the here-doc (the line starting with
"PGUSER=service_migrator PGPASSWORD=demo-password-migrator psql <<'EOF'") does
not pass ON_ERROR_STOP, so SQL errors inside the here-doc can be ignored despite
set -e; update that psql command to include the flag -v ON_ERROR_STOP=1 so psql
exits non‑zero on the first SQL error and the script fails as expected.


echo ""
echo "=============================================="
echo "Test Objects Created Successfully!"
echo "=============================================="
90 changes: 90 additions & 0 deletions examples/llm_chat_app/3_run_verification_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/bin/bash
# Run all verification tests

set -e

export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=llm_service

echo "=============================================="
echo "Running Verification Tests"
echo "=============================================="
echo ""

# Test 2: Migration Role DDL Access
echo "--- Test 2: Migration Role DDL Access ---"
PGUSER=role_service_migration PGPASSWORD=demo-password-migration psql -c "
CREATE TABLE app.migration_test (id int);
ALTER TABLE app.migration_test ADD COLUMN name text;
DROP TABLE app.migration_test;
SELECT 'TEST 2 PASSED: Migration role has DDL access' AS result;
"
echo ""

# Test 3: FastAPI RW Role
echo "--- Test 3: FastAPI RW Role - DML on app schema ---"
PGUSER=role_service_fastapi_rw PGPASSWORD=demo-password-fastapi-rw psql -c "
SELECT * FROM app.test_users;
INSERT INTO app.test_users (name) VALUES ('fastapi_test');
DELETE FROM app.test_users WHERE name = 'fastapi_test';
SELECT 'TEST 3 PASSED: FastAPI RW has app DML' AS result;
"
echo ""

# Test 4: FastAPI RO Role
echo "--- Test 4: FastAPI RO Role - SELECT only ---"
PGUSER=role_service_fastapi_ro PGPASSWORD=demo-password-fastapi-ro psql -c "
SELECT * FROM app.test_users;
SELECT 'TEST 4 PASSED: FastAPI RO has SELECT' AS result;
"
echo ""

# Test 5: Pipeline RW Role
echo "--- Test 5: Pipeline RW Role - All schemas access ---"
PGUSER=role_service_pipeline_rw PGPASSWORD=demo-password-pipeline-rw psql -c "
SELECT * FROM app.test_users;
SELECT * FROM ref_data_pipeline_abc.test_ref;
SELECT * FROM ref_data_pipeline_xyz.test_ref;
SELECT 'TEST 5 PASSED: Pipeline RW has all schemas access' AS result;
"
echo ""

# Test 6: Pipeline RO Role
echo "--- Test 6: Pipeline RO Role - Read access to all schemas ---"
PGUSER=role_service_pipeline_ro PGPASSWORD=demo-password-pipeline-ro psql -c "
SELECT * FROM app.test_users;
SELECT * FROM ref_data_pipeline_abc.test_ref;
SELECT * FROM ref_data_pipeline_xyz.test_ref;
SELECT 'TEST 6 PASSED: Pipeline RO has SELECT on all schemas' AS result;
"
echo ""

# Test 7: Connection Limits
echo "--- Test 7: Connection Limits ---"
PGUSER=role_service_migration PGPASSWORD=demo-password-migration psql -c "
SELECT rolname, rolconnlimit
FROM pg_roles
WHERE rolname LIKE 'role_service_%'
ORDER BY rolname;
"
echo ""

# Test 8: Role Inheritance
echo "--- Test 8: Role Inheritance ---"
PGUSER=role_service_migration PGPASSWORD=demo-password-migration psql -c "
SELECT
r.rolname AS role,
ARRAY_AGG(m.rolname) AS member_of
FROM pg_roles r
LEFT JOIN pg_auth_members am ON r.oid = am.member
LEFT JOIN pg_roles m ON am.roleid = m.oid
WHERE r.rolname LIKE 'role_service_%'
GROUP BY r.rolname
ORDER BY r.rolname;
"
echo ""

echo "=============================================="
echo "All Tests Completed!"
echo "=============================================="
71 changes: 71 additions & 0 deletions examples/llm_chat_app/4_cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/bin/bash
# Cleanup: Delete all resources created by the example (but not admin_user)

set -e

export PGHOST=localhost
export PGPORT=5432
export PGDATABASE=postgres # Connect to postgres db for cleanup

echo "=============================================="
echo "Cleaning Up Example Resources"
echo "=============================================="
echo ""

# Use admin_user to perform cleanup
export PGUSER=admin_user
export PGPASSWORD=insecure-pass-for-demo-admin-user

echo "Step 1: Terminating connections to llm_service database..."
psql -c "
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'llm_service' AND pid <> pg_backend_pid();
" 2>/dev/null || true

echo ""
echo "Step 2: Dropping database llm_service..."
psql -c "DROP DATABASE IF EXISTS llm_service;"

echo ""
echo "Step 3: Dropping login roles..."
# Drop login roles first (they depend on group roles)
psql <<'EOF'
DROP ROLE IF EXISTS role_service_migrator;
DROP ROLE IF EXISTS role_service_fastapi_rw;
DROP ROLE IF EXISTS role_service_fastapi_ro;
DROP ROLE IF EXISTS role_service_pipeline_rw;
DROP ROLE IF EXISTS role_service_pipeline_ro;
EOF

echo ""
echo "Step 4: Dropping group roles..."
# Drop group roles (no dependencies)
psql <<'EOF'
DROP ROLE IF EXISTS role_service_migration;
DROP ROLE IF EXISTS role_service_rw;
DROP ROLE IF EXISTS role_service_ro;
EOF

echo ""
echo "Step 5: Dropping cluster-wide roles..."
psql <<'EOF'
DROP ROLE IF EXISTS role_pg_cluster_admin;
DROP ROLE IF EXISTS role_pg_monitoring;
EOF

echo ""
echo "Step 6: Verifying cleanup..."
psql -c "
SELECT rolname FROM pg_roles
WHERE rolname LIKE 'role_service_%' OR rolname LIKE 'role_pg_%'
ORDER BY rolname;
"

echo ""
echo "=============================================="
echo "Cleanup Completed Successfully!"
echo "=============================================="
echo ""
echo "Note: admin_user was preserved."
echo "To re-run the example, start with: ./1_apply_terraform.sh"
Loading