Skip to content

Commit 1b2b6a2

Browse files
skearnesclaude
andcommitted
Grant readonly USAGE + SELECT on the ord and rdkit schemas
The readonly role only had grants on `public`, but the ord search database keeps its tables in non-public schemas: ord-schema's ORM tables live in `ord` and the RDKit cartridge tables in `rdkit`. As a result the role could connect to the `ord` database but `permission denied for schema ord` on every query — it could not read any of the actual data. Extend the grants so the readonly role gets USAGE + SELECT (current and future tables) on `ord` and `rdkit` in the ord database, matching what it already has on `public`. The per-schema grants are factored into `grant_schema_read()`; public keeps its existing resource names, so no existing grants churn (preview: 6 to create, 27 unchanged). The app/editor/app_staging databases are public-only and are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 796e345 commit 1b2b6a2

2 files changed

Lines changed: 46 additions & 19 deletions

File tree

stacks/database/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ dependency is quarantined here, in the one stack that actually needs it.
2020
- The **`readonly`** role (LOGIN), password sourced from the `rds_ro_password`
2121
secret that `backend` owns.
2222
- `CONNECT` + `USAGE` + `SELECT` on `public`, plus default privileges for future
23-
tables, across all four databases.
23+
tables, across all four databases. The `ord` search database also keeps tables
24+
in the `ord` (ord-schema ORM) and `rdkit` (cartridge) schemas, so the role gets
25+
the same USAGE + SELECT there.
2426

2527
## Deploying
2628

stacks/database/__main__.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@
3535
DATABASES = ["app", "ord", "editor", "app_staging"]
3636
PROD_DATABASES = {"app", "ord", "editor"}
3737

38+
# Every database exposes readable tables in public; the readonly role is granted
39+
# there for all of them. The ord search database additionally keeps tables in two
40+
# non-public schemas — ord-schema's ORM tables in `ord` and the RDKit cartridge
41+
# tables in `rdkit` — so the role needs USAGE + SELECT on those too. The
42+
# Alembic-managed app databases (app, app_staging) and the editor database use
43+
# public only.
44+
EXTRA_READONLY_SCHEMAS = {"ord": ["ord", "rdkit"]}
45+
3846
# Credentials come from the secrets the backend stack manages: the master user to
3947
# connect as, and the generated password the `readonly` role should have.
4048
master_password = aws.secretsmanager.get_secret_version_output(
@@ -106,31 +114,31 @@
106114
),
107115
)
108116

109-
for db, provider in providers.items():
110-
opts = pulumi.ResourceOptions(
111-
provider=provider, depends_on=[readonly, databases[db]]
112-
)
113-
postgresql.Grant(
114-
f"{db}_connect",
115-
database=db,
116-
role=readonly.name,
117-
object_type="database",
118-
privileges=["CONNECT"],
119-
opts=opts,
120-
)
117+
118+
def grant_schema_read(name_prefix: str, db: str, schema: str, opts) -> None:
119+
"""Grants the readonly role USAGE + SELECT (current and future tables) on a schema.
120+
121+
Args:
122+
name_prefix: Prefix for the Pulumi resource names. `public` uses the bare
123+
database name to preserve existing resource URNs; other schemas qualify
124+
it with the schema name.
125+
db: Database the grants apply to.
126+
schema: Schema to grant on.
127+
opts: Resource options carrying the database's provider and dependencies.
128+
"""
121129
postgresql.Grant(
122-
f"{db}_usage",
130+
f"{name_prefix}_usage",
123131
database=db,
124-
schema="public",
132+
schema=schema,
125133
role=readonly.name,
126134
object_type="schema",
127135
privileges=["USAGE"],
128136
opts=opts,
129137
)
130138
postgresql.Grant(
131-
f"{db}_select",
139+
f"{name_prefix}_select",
132140
database=db,
133-
schema="public",
141+
schema=schema,
134142
role=readonly.name,
135143
object_type="table",
136144
objects=[], # empty list = all tables currently in the schema
@@ -140,12 +148,29 @@
140148
# Future tables created by the master user are readable too, so loads don't
141149
# silently leave the readonly role unable to see new tables.
142150
postgresql.DefaultPrivileges(
143-
f"{db}_select_future",
151+
f"{name_prefix}_select_future",
144152
database=db,
145-
schema="public",
153+
schema=schema,
146154
owner="ord",
147155
role=readonly.name,
148156
object_type="table",
149157
privileges=["SELECT"],
150158
opts=opts,
151159
)
160+
161+
162+
for db, provider in providers.items():
163+
opts = pulumi.ResourceOptions(
164+
provider=provider, depends_on=[readonly, databases[db]]
165+
)
166+
postgresql.Grant(
167+
f"{db}_connect",
168+
database=db,
169+
role=readonly.name,
170+
object_type="database",
171+
privileges=["CONNECT"],
172+
opts=opts,
173+
)
174+
grant_schema_read(db, db, "public", opts)
175+
for schema in EXTRA_READONLY_SCHEMAS.get(db, []):
176+
grant_schema_read(f"{db}_{schema}", db, schema, opts)

0 commit comments

Comments
 (0)