Skip to content

Commit 7bd7ef0

Browse files
committed
πŸ› fix: only require CREATE on the POSTGRES_SCHEMA target, not fallbacks
Requiring CREATE on every listed schema rejected the documented least-privilege setup (POSTGRES_SCHEMA=myapp,extensions where the app writes only to myapp and merely reads types from extensions) β€” the validation was stricter than PostgreSQL's own behavior, which only needs CREATE on the first schema in search_path. Now require USAGE on every schema (needed for type/function resolution at query time) but CREATE only on the target (schemas[0]). The error message distinguishes target-level CREATE gaps from USAGE gaps so the operator knows exactly which grant to fix.
1 parent b472355 commit 7bd7ef0

1 file changed

Lines changed: 31 additions & 13 deletions

File tree

β€Žapp/services/vector_store/factory.pyβ€Ž

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,23 @@ def _build_search_path(schemas: List[str]) -> str:
3434

3535

3636
def _verify_schemas_exist(connection_string: str, schemas: List[str]) -> None:
37-
"""Raise if any requested schema is missing or lacks USAGE/CREATE for the
38-
connecting role.
37+
"""Raise if the POSTGRES_SCHEMA config won't let the app write to the
38+
target schema or read types from its fallbacks.
39+
40+
schemas[0] is the write target β€” the schema pgvector's tables will land
41+
in β€” so the role needs USAGE + CREATE there. schemas[1:] are read-only
42+
search_path fallbacks for type/function resolution (e.g. an `extensions`
43+
schema that holds the `vector` type), so USAGE alone is sufficient; a
44+
role with CREATE here is fine too, but demanding it would reject the
45+
common least-privilege setup where writes are intentionally confined to
46+
the target schema.
3947
4048
Silent fallback is worse than failing fast here: PostgreSQL resolves
4149
unqualified CREATE TABLE against the first schema in search_path where
42-
the role has CREATE privileges. A typo or a privilege gap on the target
43-
schema would therefore land the pgvector tables in `public` (the next
44-
entry in the search_path appended by _build_search_path) instead of the
45-
intended namespace, silently defeating the isolation this feature is
46-
meant to provide.
50+
the role has CREATE privileges, so a typo or a missing grant on the
51+
target would land the pgvector tables in `public` (the entry appended
52+
by _build_search_path) instead of the intended namespace, silently
53+
defeating the isolation this feature is meant to provide.
4754
"""
4855
from sqlalchemy import create_engine, text
4956

@@ -62,25 +69,36 @@ def _verify_schemas_exist(connection_string: str, schemas: List[str]) -> None:
6269
).fetchall()
6370
found = {row[0]: (row[1], row[2]) for row in rows}
6471

72+
target_schema = schemas[0]
73+
fallback_schemas = schemas[1:]
74+
6575
missing = [s for s in schemas if s not in found]
76+
# USAGE is required on every listed schema (target to write,
77+
# fallbacks to look up types/functions at query time).
6678
no_usage = [s for s in schemas if s in found and not found[s][0]]
67-
no_create = [s for s in schemas if s in found and not found[s][1]]
79+
# CREATE is only required on the target schema β€” the fallback
80+
# entries exist solely to make type resolution work.
81+
no_create_target = (
82+
[target_schema]
83+
if target_schema in found and not found[target_schema][1]
84+
else []
85+
)
6886

6987
problems = []
7088
if missing:
7189
problems.append(f"does not exist: {missing!r}")
7290
if no_usage:
7391
problems.append(f"role lacks USAGE on: {no_usage!r}")
74-
if no_create:
75-
problems.append(f"role lacks CREATE on: {no_create!r}")
92+
if no_create_target:
93+
problems.append(f"role lacks CREATE on target: {no_create_target!r}")
7694

7795
if problems:
78-
target = (missing or no_usage or no_create)[0]
96+
hint_target = (missing or no_usage or no_create_target)[0]
7997
raise ValueError(
8098
"POSTGRES_SCHEMA: " + "; ".join(problems) + ". "
8199
"Create/grant out-of-band first (e.g. "
82-
f"`CREATE SCHEMA IF NOT EXISTS {target}; "
83-
f"GRANT USAGE, CREATE ON SCHEMA {target} TO <app_user>;`)."
100+
f"`CREATE SCHEMA IF NOT EXISTS {hint_target}; "
101+
f"GRANT USAGE, CREATE ON SCHEMA {hint_target} TO <app_user>;`)."
84102
)
85103
finally:
86104
engine.dispose()

0 commit comments

Comments
Β (0)