Skip to content

Commit cb55c49

Browse files
committed
Fix: unprivileged copy from (#125)
(cherry picked from commit 3d8fbb6)
1 parent 6014e11 commit cb55c49

File tree

3 files changed

+126
-6
lines changed

3 files changed

+126
-6
lines changed

src/parquet_copy_hook/copy_from.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ use crate::{
1818
},
1919
};
2020

21-
use super::copy_utils::{
22-
copy_from_stmt_match_by, copy_stmt_attribute_list, copy_stmt_create_namespace_item,
23-
copy_stmt_create_parse_state, create_filtered_tupledesc_for_relation,
21+
use super::{
22+
copy_utils::{
23+
copy_from_stmt_match_by, copy_stmt_attribute_list, copy_stmt_create_namespace_item,
24+
copy_stmt_create_parse_state, create_filtered_tupledesc_for_relation,
25+
},
26+
pg_compat::check_copy_table_permission,
2427
};
2528

2629
// stack to store parquet reader contexts for COPY FROM.
@@ -133,6 +136,8 @@ pub(crate) fn execute_copy_from(
133136
where_clause = copy_from_stmt_transform_where_clause(&p_state, &ns_item, where_clause);
134137
}
135138

139+
check_copy_table_permission(p_stmt, &p_state, &ns_item, &relation);
140+
136141
let attribute_list = copy_stmt_attribute_list(p_stmt);
137142

138143
let tupledesc = create_filtered_tupledesc_for_relation(p_stmt, &relation);

src/parquet_copy_hook/pg_compat.rs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
use std::ffi::{c_char, CStr};
22

3-
use pgrx::pg_sys::{AsPgCStr, List, Node, QueryEnvironment, RawStmt};
3+
use pgrx::{
4+
pg_sys::{
5+
bms_add_member, AsPgCStr, CopyGetAttnums, CopyStmt, FirstLowInvalidHeapAttributeNumber,
6+
List, Node, ParseNamespaceItem, ParseState, PlannedStmt, QueryEnvironment, RawStmt,
7+
ACL_INSERT, ACL_SELECT,
8+
},
9+
PgBox, PgList, PgRelation,
10+
};
411

512
pub(crate) fn pg_analyze_and_rewrite(
613
raw_stmt: *mut RawStmt,
@@ -65,3 +72,89 @@ pub(crate) fn MarkGUCPrefixReserved(guc_prefix: &str) {
6572
pgrx::pg_sys::MarkGUCPrefixReserved(guc_prefix.as_pg_cstr())
6673
}
6774
}
75+
76+
/// check_copy_table_permission checks if the user has permission to copy from/to the table.
77+
/// This is taken from the original PostgreSQL DoCopy function.
78+
#[cfg(any(feature = "pg16", feature = "pg17"))]
79+
pub(crate) fn check_copy_table_permission(
80+
p_stmt: &PgBox<PlannedStmt>,
81+
p_state: &PgBox<ParseState>,
82+
ns_item: &PgBox<ParseNamespaceItem>,
83+
relation: &PgRelation,
84+
) {
85+
let copy_stmt = unsafe { PgBox::<CopyStmt>::from_pg(p_stmt.utilityStmt as _) };
86+
87+
// init permissions
88+
let mut perminfo =
89+
unsafe { PgBox::<pgrx::pg_sys::RTEPermissionInfo>::from_pg(ns_item.p_perminfo) };
90+
91+
// set table access mode
92+
perminfo.requiredPerms = if copy_stmt.is_from {
93+
ACL_INSERT as _
94+
} else {
95+
ACL_SELECT as _
96+
};
97+
98+
// set column access modes
99+
let tup_desc = relation.tuple_desc();
100+
101+
let attnums =
102+
unsafe { CopyGetAttnums(tup_desc.as_ptr(), relation.as_ptr(), copy_stmt.attlist) };
103+
let attnums = unsafe { PgList::<i16>::from_pg(attnums) };
104+
105+
for attnum in attnums.iter_int() {
106+
let attno = attnum - FirstLowInvalidHeapAttributeNumber;
107+
108+
if copy_stmt.is_from {
109+
unsafe { perminfo.insertedCols = bms_add_member(perminfo.insertedCols, attno) };
110+
} else {
111+
unsafe { perminfo.selectedCols = bms_add_member(perminfo.selectedCols, attno) };
112+
}
113+
}
114+
115+
// check permissions
116+
let mut perm_infos = PgList::<pgrx::pg_sys::RTEPermissionInfo>::new();
117+
perm_infos.push(perminfo.as_ptr());
118+
119+
unsafe { pgrx::pg_sys::ExecCheckPermissions(p_state.p_rtable, perm_infos.as_ptr(), true) };
120+
}
121+
122+
#[cfg(any(feature = "pg14", feature = "pg15"))]
123+
pub(crate) fn check_copy_table_permission(
124+
p_stmt: &PgBox<PlannedStmt>,
125+
p_state: &PgBox<ParseState>,
126+
ns_item: &PgBox<ParseNamespaceItem>,
127+
relation: &PgRelation,
128+
) {
129+
let copy_stmt = unsafe { PgBox::<CopyStmt>::from_pg(p_stmt.utilityStmt as _) };
130+
131+
// init rte
132+
let mut rte = unsafe { PgBox::<pgrx::pg_sys::RangeTblEntry>::from_pg(ns_item.p_rte) };
133+
134+
// set table access mode
135+
rte.requiredPerms = if copy_stmt.is_from {
136+
ACL_INSERT as _
137+
} else {
138+
ACL_SELECT as _
139+
};
140+
141+
// set column access modes
142+
let tup_desc = relation.tuple_desc();
143+
144+
let attnums =
145+
unsafe { CopyGetAttnums(tup_desc.as_ptr(), relation.as_ptr(), copy_stmt.attlist) };
146+
let attnums = unsafe { PgList::<i16>::from_pg(attnums) };
147+
148+
for attnum in attnums.iter_int() {
149+
let attno = attnum - FirstLowInvalidHeapAttributeNumber;
150+
151+
if copy_stmt.is_from {
152+
unsafe { rte.insertedCols = bms_add_member(rte.insertedCols, attno) };
153+
} else {
154+
unsafe { rte.selectedCols = bms_add_member(rte.selectedCols, attno) };
155+
}
156+
}
157+
158+
// check permissions
159+
unsafe { pgrx::pg_sys::ExecCheckRTPerms(p_state.p_rtable, true) };
160+
}

src/pgrx_tests/copy_pg_rules.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ mod tests {
483483

484484
#[pg_test]
485485
#[should_panic(expected = "permission denied for table test_table")]
486-
fn test_with_no_table_privilege() {
486+
fn test_copy_to_with_no_table_privilege() {
487487
let create_table = "create table test_table(id int);";
488488
Spi::run(create_table).unwrap();
489489

@@ -496,7 +496,29 @@ mod tests {
496496
let set_role = "set role test_role;";
497497
Spi::run(set_role).unwrap();
498498

499-
let copy_from_parquet = format!("copy test_table to '{}';", LOCAL_TEST_FILE_PATH);
499+
let copy_to_parquet = format!("copy test_table to '{}';", LOCAL_TEST_FILE_PATH);
500+
Spi::run(&copy_to_parquet).unwrap();
501+
}
502+
503+
#[pg_test]
504+
#[should_panic(expected = "permission denied for table test_table")]
505+
fn test_copy_from_with_no_table_privilege() {
506+
let copy_to_parquet = format!("copy (select 1) to '{LOCAL_TEST_FILE_PATH}';");
507+
Spi::run(&copy_to_parquet).unwrap();
508+
509+
let create_table = "create table test_table(id int);";
510+
Spi::run(create_table).unwrap();
511+
512+
let create_role = "create role test_role;";
513+
Spi::run(create_role).unwrap();
514+
515+
let grant_role = "grant pg_read_server_files TO test_role;";
516+
Spi::run(grant_role).unwrap();
517+
518+
let set_role = "set role test_role;";
519+
Spi::run(set_role).unwrap();
520+
521+
let copy_from_parquet = format!("copy test_table from '{}';", LOCAL_TEST_FILE_PATH);
500522
Spi::run(&copy_from_parquet).unwrap();
501523
}
502524
}

0 commit comments

Comments
 (0)