Standardize server-file privilege on roles (#330) - #331
Conversation
…cmd#300) The symmetric counterpart to parallel_copy (parallel import). N read-only background workers each write a disjoint slice of the source to its own part-NNNN.parquet file under one directory, which pgcolumnar.read_parquet and the parquet FDW read back as a single relation. Export is read-only, so unlike parallel_copy there is no coordinator and no two-phase commit -- the SQL function is the dispatcher. All workers restore ONE serialized MVCC snapshot (SerializeSnapshot/RestoreSnapshot), so the directory is a consistent point-in-time image. Two target kinds mirror parallel_copy: - a single columnar table, split by row-group index ranges (each worker calls ColumnarReadRestrictToGroups on its slice); - a partitioned columnar table, one file per leaf partition (oid-sorted list sliced across workers). - src/columnar_parquet.c: extract the serial writer body into a reusable ColumnarWriteParquetFile(rel, snapshot, filepath, restrictGroups, n); the serial columnar_export_parquet becomes a thin wrapper. Add ColumnarParquetCheckExportable for a fail-fast dispatcher pre-check. - src/columnar_parallel_export.c (new): the dispatcher + read-only worker, reusing the parallel_copy DSM/bgworker scaffolding. Privilege: pg_write_server_files + SELECT. Create-or-require-empty output directory (read_parquet unions every *.parquet, so a stale file must not linger). - SQL: pgcolumnar.parallel_export_parquet(target, path, workers DEFAULT NULL). - test/parallel_export_parquet.sh: parallel == serial == source via read_parquet + pgc_set_hash, single-table + partitioned + empty, W distinct files, and error cases. Registered in run_all_versions.sh. Compiles clean; 27/27 on pg18a assert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1jrWiQsJJA1t4pkmkb4T
…atcmd#329 review) jdatcmd's blocker, reproduced: exporting inside a transaction with uncommitted rows duplicated the table and dropped the caller's rows. Two defects compounding: 1. Snapshot handoff used the PARALLEL-QUERY mechanism (SerializeSnapshot / RestoreSnapshot), which is only sound for members of the leader's parallel group. These workers are independent backends, so the serialized snapshot lacked the dispatcher's own xid and the workers saw fewer row groups. Switch to the cross-transaction mechanism: dispatcher ExportSnapshot(), workers run a repeatable-read transaction and ImportSnapshot() -- the pg_dump parallel mechanism. Every worker now sees the identical committed image. 2. ColumnarWriteParquetFile treated an empty restrict list as 'no restriction' (export everything). Once the views disagreed, a worker's empty slice exported the whole table -> duplication. Now NULL means whole table and a non-NULL list (even empty) restricts to exactly those groups, so an empty slice writes nothing. The worker always passes a non-NULL list. Also from the review: - validate EACH leaf partition's tupdesc in the dispatcher, not just the parent; - ship the oid-sorted leaf list from the dispatcher so a concurrent ATTACH cannot desynchronise the workers (they no longer re-derive it from the live catalog); - BgWorkerStart_ConsistentState so a read-only export runs on a hot standby; - PG_ENSURE_ERROR_CLEANUP so a dispatcher FATAL terminates the workers instead of orphaning them past the exported snapshot; - a per-partition memory context in the worker loop, so buffers do not accumulate. test/parallel_export_parquet.sh: add the in-transaction fixture (BEGIN; INSERT uncommitted; export; COMMIT) that asserts no duplication, no duplicate ids, and that uncommitted rows are absent -- the coverage the suite lacked. 30/30 pg18a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1jrWiQsJJA1t4pkmkb4T
Server-file functions had drifted: parallel_copy and file_split_offsets gated on pg_read_server_files while import_parquet/read_parquet/parquet_schema/the parquet FDW/import_arrow/export_parquet/export_arrow gated on superuser(), and the docs asserted superuser for all. parallel_export_parquet (jdatcmd#329) would add a third role-based writer. Two bars for the same capability, and the boundary test and docs covered only one. Resolution (issue jdatcmd#330, option 1): relax the seven superuser() checks to the core server-file roles -- pg_read_server_files for the readers, pg_write_server_files for the writers -- matching COPY ... FROM/TO 'file'. A superuser holds both roles, so this only widens access to role holders. This is a deliberate loosening. The read functions parse Parquet/Arrow files this project wrote, so a role short of superuser can now reach those parsers; the docs and CHANGELOG say so, and the mitigation is the parser fuzzing in jdatcmd#214 (Arrow still open). If the security posture should keep the untrusted-parser readers at superuser for now, that is a one-line-per-site change -- flag it and I will. - src/columnar_parquet.c, columnar_arrow.c, columnar_parquet_reader.c: the seven checks, with the pg_authid_d.h + utils/acl.h includes. - docs/administration.md: the blanket 'requires superuser' sentence replaced, the table completed with all ten entry points and their role, the untrusted-input note updated. - CHANGELOG.md: the deliberate-loosening entry. - test/server_file_privilege.sh: rebuilt over the full set. Per point it asserts a role-less caller is refused by name and a role holder gets past; plus a coverage check that a SQL function with a file-path argument missing from the list turns the gate red. 30/30 on pg18a. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1jrWiQsJJA1t4pkmkb4T
Review: the right resolution, and the durable half actually worksThis is option 1 from #330 and I think it is the correct call: the role gate is Verified myselfThe role mapping is right. I enumerated every gate in the branch: writers The coverage check works, proved by removal. I added an ungated function to the CREATE FUNCTION pgcolumnar.peek_server_file(path text) RETURNS bigint ...30 checks clean, 31 with the injection, and it names the offending function. That The anti-vacuity assertion on the discovery itself ("coverage check found the file The CHANGELOG says the quiet part. "This is a deliberate loosening", plus the One leftover, and it is now self-contradicting
/* ... The stat() is gated on superuser -- the same bar the scan
* enforces -- so a non-privileged planner cannot use the EXPLAIN estimate to
* probe whether a server-side path exists or how big it is. */
if (superuser() && path != NULL && stat(path, &st) == 0 && st.st_size > 0)The guard itself is good thinking: it stops the row estimate becoming a Consequence is not a security hole, it is the reverse: a user who legitimately It also escaped the new coverage check, which is fair, since that check scans the Note on scopeThis branch also carries #329's If you would rather land the privilege standardisation on its own, splitting it out |
…datcmd#330 review) jdatcmd's jdatcmd#331 review: pqfdwGetForeignRelSize gated its size-estimate stat() on superuser() with the comment 'the same bar the scan enforces'. This PR moved the scan to pg_read_server_files, so that comment became self-contradicting, and a role holding pg_read_server_files could read the file through the FDW yet got the flat 1000-row default estimate -- worse plans for no reason. Move the guard to the same role and update the comment. (My original grep was for !superuser(); this positive-form guard slipped past it.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1jrWiQsJJA1t4pkmkb4T
|
Fixed the FDW estimate gate in On the #329 items in your scope noteThe parent-versus-leaf tupdesc check, the leaf list re-derived from the live catalog, The one I did not change is partial output on failure: a failed export leaves its On splitting the privilege change from the featureI'd like to, and the coupling is only two spots: |
parallel_export_parquet (merged in #329) was in administration.md's role table but had no reference entry, feature note, or user-guide example, unlike its sibling parallel_copy. Add all three, including the consistency guarantee (one exported snapshot, so the files are the committed image at call time) and the partial-output cleanup on failure. Also correct six places that still said the server-file functions "require superuser". #330 and #331 moved them to the pg_read_server_files and pg_write_server_files roles, which superusers hold; administration.md was updated then, but these were missed: - sql-reference.md: the import/export intro and the read-in-place section - features.md: the interoperability note - user-guide.md: the read-in-place note - limitations.md: the type-coverage note and the read-in-place limits The remaining superuser mentions in configuration.md are GUC-set permissions, not server-file access, and are correct. STE and docs_style gates pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UX1jrWiQsJJA1t4pkmkb4T
…ence docs: document parallel_export_parquet; fix stale superuser claims after #331
Resolves #330. Standardizes every server-file function on the core roles (option 1 from the issue), the model core
COPY ... FROM/TO 'file'uses.Change
Relaxed the seven
superuser()checks tohas_privs_of_role(...):import_parquet,read_parquet,parquet_schema, parquet FDW scan,import_arrowsuperuser()pg_read_server_filesexport_parquet,export_arrowsuperuser()pg_write_server_filesparallel_copyandfile_split_offsetsalready usedpg_read_server_files;parallel_export_parquet(this stack) usespg_write_server_files. A superuser holds both roles, so this only widens access to role holders.Validation
server_file_privilege,parquet_export/parquet_import,arrow_export/arrow_import,native_read_parquet,native_parquet_fdw,parallel_copy,parallel_export_parquetall pass.server_file_privilege.shis 30/30.The security call I want you to make
This is a deliberate loosening, and the sharp edge is the read side:
import_parquet,read_parquet,parquet_schema, the FDW, andimport_arrowparse files this project wrote, so a role short of superuser can now reach those parsers. #216 kept them at superuser precisely until the parser fuzzing is done, and Arrow fuzzing (#214) is still open. I implemented option 1 because you leaned that way and it is the coherent model, and I documented the exposure indocs/administration.mdand the CHANGELOG. But if you'd rather keep the untrusted-parser readers at superuser for now and only role-gate the writers + the text-path readers (parallel_copy,file_split_offsets, which use core's trusted COPY parser), that is a one-line-per-site revert — say the word and I'll adjust.Stacked on #329 (
export_parquet's privilege is coupled to that PR's refactor +parallel_export_parquetlives there). This PR therefore shows #329's two commits as well as the #330 commit (0cf8fb4) until #329 merges, after which the diff reduces to #330 alone. Review the0cf8fb4commit for the #330 change; #329 you already verified. No sanitizer run: the change is privilege checks, and #329's code already went through pg18_san.