Roles & Permissions¶
pycopg provides comprehensive role and permission management for PostgreSQL.
Creating Roles¶
Basic User¶
# Create a user that can log in
db.create_role("appuser", password="secret123", login=True)
With Options¶
db.create_role(
"admin",
password="secret",
login=True,
superuser=True, # Full privileges
createdb=True, # Can create databases
createrole=True, # Can create other roles
inherit=True, # Inherit privileges from member roles
replication=False, # Can initiate replication
connection_limit=10, # Max concurrent connections (-1 = unlimited)
valid_until="2025-12-31", # Password expiration
if_not_exists=True, # Don't error if exists
)
Group Roles¶
Group roles don’t log in but grant permissions to members.
# Create a group role
db.create_role("readonly", login=False)
# Create user in the group
db.create_role("analyst", password="secret", login=True, in_roles=["readonly"])
Managing Roles¶
Alter Role¶
# Change password
db.alter_role("appuser", password="newpassword")
# Change connection limit
db.alter_role("appuser", connection_limit=20)
# Disable login
db.alter_role("appuser", login=False)
# Rename role
db.alter_role("oldname", rename_to="newname")
# Multiple changes
db.alter_role(
"appuser",
password="newpass",
connection_limit=50,
valid_until="2025-06-30"
)
Drop Role¶
db.drop_role("olduser")
db.drop_role("olduser", if_exists=True)
Check Role Exists¶
if db.role_exists("appuser"):
print("Role exists")
List Roles¶
roles = db.list_roles()
# [
# {
# 'name': 'admin',
# 'superuser': True,
# 'createrole': True,
# 'createdb': True,
# 'login': True,
# 'replication': False,
# 'connection_limit': -1,
# 'valid_until': None
# },
# ...
# ]
# Include system roles (pg_*)
all_roles = db.list_roles(include_system=True)
Role Membership¶
Grant Role¶
# Make 'analyst' a member of 'readonly'
db.grant_role("readonly", "analyst")
# With admin option (can grant role to others)
db.grant_role("admin", "lead_dev", with_admin=True)
Revoke Role¶
db.revoke_role("readonly", "analyst")
List Role Members¶
members = db.list_role_members("readonly")
# ['analyst', 'reporter', 'dashboard_user']
Granting Privileges¶
On Tables¶
# Grant SELECT
db.grant("SELECT", "users", "readonly")
# Grant multiple privileges
db.grant(["SELECT", "INSERT", "UPDATE"], "orders", "appuser")
# Grant all privileges
db.grant("ALL", "products", "admin")
On All Tables in Schema¶
# Grant SELECT on all tables in public schema
db.grant("SELECT", "ALL TABLES", "readonly", schema="public")
# Grant all on all tables
db.grant("ALL", "ALL TABLES", "admin", schema="app")
On Schemas¶
# Grant USAGE (required to access objects in schema)
db.grant("USAGE", "app", "appuser", object_type="SCHEMA")
# Grant CREATE (can create objects in schema)
db.grant("CREATE", "app", "admin", object_type="SCHEMA")
# Grant all
db.grant("ALL", "app", "admin", object_type="SCHEMA")
On Databases¶
# Grant CONNECT
db.grant("CONNECT", "mydb", "appuser", object_type="DATABASE")
# Grant CREATE (can create schemas)
db.grant("CREATE", "mydb", "admin", object_type="DATABASE")
With Grant Option¶
# Allow grantee to grant to others
db.grant("SELECT", "users", "team_lead", with_grant_option=True)
Revoking Privileges¶
Basic Revoke¶
db.revoke("INSERT", "users", "readonly")
db.revoke(["INSERT", "UPDATE", "DELETE"], "orders", "readonly")
Cascade Revoke¶
# Revoke from dependent grants too
db.revoke("ALL", "users", "admin", cascade=True)
On Schemas and Databases¶
db.revoke("USAGE", "app", "olduser", object_type="SCHEMA")
db.revoke("CONNECT", "mydb", "olduser", object_type="DATABASE")
Listing Grants¶
grants = db.list_role_grants("appuser")
# [
# {'schema': 'public', 'object_name': 'users', 'privilege': 'SELECT'},
# {'schema': 'public', 'object_name': 'users', 'privilege': 'INSERT'},
# {'schema': 'public', 'object_name': 'orders', 'privilege': 'SELECT'},
# ...
# ]
Common Patterns¶
Read-Only User¶
# Create read-only role
db.create_role("readonly", login=False)
db.grant("USAGE", "public", "readonly", object_type="SCHEMA")
db.grant("SELECT", "ALL TABLES", "readonly", schema="public")
# Create read-only user
db.create_role("reader", password="secret", login=True, in_roles=["readonly"])
Application User¶
# Create role with typical app permissions
db.create_role("appuser", password="secret", login=True)
db.grant("USAGE", "public", "appuser", object_type="SCHEMA")
db.grant(["SELECT", "INSERT", "UPDATE", "DELETE"], "ALL TABLES", "appuser", schema="public")
db.grant(["USAGE", "SELECT"], "ALL SEQUENCES", "appuser", schema="public")
Admin User¶
db.create_role(
"admin",
password="secret",
login=True,
superuser=False,
createdb=True,
createrole=True,
)
db.grant("ALL", "ALL TABLES", "admin", schema="public")
db.grant("ALL", "public", "admin", object_type="SCHEMA")
Team Hierarchy¶
# Create team roles
db.create_role("engineering", login=False)
db.create_role("senior_engineers", login=False, in_roles=["engineering"])
# Grant permissions to groups
db.grant("SELECT", "ALL TABLES", "engineering", schema="public")
db.grant(["INSERT", "UPDATE"], "code_reviews", "engineering")
db.grant("DELETE", "code_reviews", "senior_engineers")
# Create users in teams
db.create_role("alice", password="secret", login=True, in_roles=["engineering"])
db.create_role("bob", password="secret", login=True, in_roles=["senior_engineers"])
Row-Level Security¶
For fine-grained access control, use PostgreSQL’s RLS.
# Enable RLS on table
db.execute("ALTER TABLE orders ENABLE ROW LEVEL SECURITY")
# Create policy for users to see only their orders
db.execute("""
CREATE POLICY orders_user_policy ON orders
FOR ALL
TO appuser
USING (user_id = current_setting('app.current_user_id')::integer)
""")
# Force RLS for table owner too
db.execute("ALTER TABLE orders FORCE ROW LEVEL SECURITY")
Best Practices¶
1. Use Role Hierarchy¶
# Don't grant directly to users
# Instead, create role groups
# Base roles
db.create_role("read_access", login=False)
db.create_role("write_access", login=False, in_roles=["read_access"])
db.create_role("admin_access", login=False, in_roles=["write_access"])
# Users inherit from roles
db.create_role("alice", password="...", in_roles=["write_access"])
2. Principle of Least Privilege¶
# Grant minimum required permissions
db.grant("SELECT", "users", "readonly") # Not ALL
# Be specific about tables
db.grant("SELECT", "public_data", "readonly") # Not ALL TABLES
3. Use Separate Roles for Apps¶
# Each app gets its own role
db.create_role("webapp", password="...", login=True)
db.create_role("worker", password="...", login=True)
db.create_role("analytics", password="...", login=True)
4. Regular Audits¶
# Review all roles
for role in db.list_roles():
print(f"{role['name']}: login={role['login']}, super={role['superuser']}")
grants = db.list_role_grants(role['name'])
for g in grants:
print(f" - {g['privilege']} on {g['schema']}.{g['object_name']}")