diff --git a/doc/src/sgml/ref/copy.sgml b/doc/src/sgml/ref/copy.sgml
index 53b0ea8f5735..ecf0591a3c98 100644
--- a/doc/src/sgml/ref/copy.sgml
+++ b/doc/src/sgml/ref/copy.sgml
@@ -569,9 +569,11 @@ COPY count
If row-level security is enabled for the table, the relevant
SELECT policies will apply to COPY
table TO statements.
- Currently, COPY FROM is not supported for tables
- with row-level security. Use equivalent INSERT
- statements instead.
+ COPY FROM is supported for tables with row-level security.
+ However if any row-level security policy’s USING or
+ WITH CHECK expression contains a subquery, then
+ COPY FROM is not supported. In that case, Use equivalent
+ INSERT statements instead.
diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c
index 6454a39a01f0..ae91480beff1 100644
--- a/src/backend/commands/copy.c
+++ b/src/backend/commands/copy.c
@@ -218,7 +218,7 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
* If RLS is not enabled for this, then just fall through to the
* normal non-filtering relation handling.
*/
- if (check_enable_rls(relid, InvalidOid, false) == RLS_ENABLED)
+ if (!is_from && check_enable_rls(relid, InvalidOid, false) == RLS_ENABLED)
{
SelectStmt *select;
ColumnRef *cr;
@@ -226,12 +226,6 @@ DoCopy(ParseState *pstate, const CopyStmt *stmt,
RangeVar *from;
List *targetList = NIL;
- if (is_from)
- ereport(ERROR,
- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
- errmsg("COPY FROM not supported with row-level security"),
- errhint("Use INSERT statements instead.")));
-
/*
* Build target list
*
diff --git a/src/backend/commands/copyfrom.c b/src/backend/commands/copyfrom.c
index 2ae3d2ba86e7..4c0cb68be223 100644
--- a/src/backend/commands/copyfrom.c
+++ b/src/backend/commands/copyfrom.c
@@ -39,16 +39,20 @@
#include "foreign/fdwapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
+#include "nodes/makefuncs.h"
#include "nodes/miscnodes.h"
#include "optimizer/optimizer.h"
+#include "parser/parse_relation.h"
#include "pgstat.h"
#include "rewrite/rewriteHandler.h"
+#include "rewrite/rowsecurity.h"
#include "storage/fd.h"
#include "tcop/tcopprot.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/portal.h"
#include "utils/rel.h"
+#include "utils/rls.h"
#include "utils/snapmgr.h"
/*
@@ -957,6 +961,109 @@ CopyFrom(CopyFromState cstate)
Assert(resultRelInfo->ri_BatchSize >= 1);
+ if (check_enable_rls(RelationGetRelid(cstate->rel), InvalidOid, false) == RLS_ENABLED)
+ {
+ List *securityQuals = NIL;
+ List *withCheckOptions = NIL;
+ List *newWithCheckOptions = NIL;
+ List *wcoExprs = NIL;
+ char *refname;
+
+ Query *root = makeNode(Query);
+ ModifyTable *node = makeNode(ModifyTable);
+ RangeTblEntry *rte = makeNode(RangeTblEntry);
+ bool hasRowSecurity = false;
+ bool hasSubLinks = false;
+ RTEPermissionInfo *perminfo;
+
+ /*
+ * We use the Query and RTE nodes to retrieve the COPY FROM relation's
+ * security policies (get_row_security_policies) and transform them into
+ * WithCheckOption nodes. Later, we initialize these WCO node exprstate
+ * and pass these initialized WCOs to the resultRelInfo.
+ */
+ rte->alias = NULL;
+ refname = RelationGetRelationName(cstate->rel);
+ rte->eref = makeAlias(refname, NIL);
+ rte->rtekind = RTE_RELATION;
+ rte->relid = RelationGetRelid(cstate->rel);
+ rte->inh = false;
+ rte->relkind = cstate->rel->rd_rel->relkind;
+ rte->rellockmode = RowExclusiveLock;
+ rte->lateral = false;
+ rte->inFromCl = false;
+
+ perminfo = addRTEPermissionInfo(&root->rteperminfos, rte);
+ perminfo->requiredPerms = ACL_INSERT;
+ perminfo->checkAsUser = InvalidOid;
+
+ /* we already did the permission check on DoCopy */
+ foreach_int(cur, cstate->attnumlist)
+ {
+ int attno;
+ Bitmapset **bms;
+
+ attno = cur - FirstLowInvalidHeapAttributeNumber;
+ bms = &perminfo->insertedCols;
+
+ *bms = bms_add_member(*bms, attno);
+ }
+
+ root->resultRelation = 1;
+ root->rtable = list_make1(rte);
+ root->commandType = CMD_INSERT;
+ root->stmt_location = -1;
+
+ get_row_security_policies(root, rte, 1,
+ &securityQuals,
+ &withCheckOptions,
+ &hasRowSecurity,
+ &hasSubLinks);
+
+ /* policy contain subquery, maybe doable? */
+ if (hasSubLinks)
+ ereport(ERROR,
+ errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+ errmsg("COPY FROM not supported with row-level security policy contain subquery"),
+ errhint("Use INSERT statements instead."));
+
+ foreach_node(WithCheckOption, wco, withCheckOptions)
+ {
+ wco->qual = eval_const_expressions(NULL, wco->qual);
+ wco->qual = (Node *) canonicalize_qual((Expr *) wco->qual, false);
+ wco->qual = (Node *) make_ands_implicit((Expr *) wco->qual);
+
+ if (wco->qual != NULL)
+ newWithCheckOptions = lappend(newWithCheckOptions, wco);
+ }
+
+ foreach_node(WithCheckOption, wco, newWithCheckOptions)
+ {
+ ExprState *wcoExpr = ExecInitQual(castNode(List, wco->qual),
+ &mtstate->ps);
+
+ wcoExprs = lappend(wcoExprs, wcoExpr);
+ }
+
+ resultRelInfo->ri_WithCheckOptions = newWithCheckOptions;
+ resultRelInfo->ri_WithCheckOptionExprs = wcoExprs;
+
+ /* see make_modifytable */
+ node->operation = CMD_INSERT;
+
+ /*
+ * INSERT applies to a single relation only, so rootRelation is always 0
+ */
+ node->rootRelation = 0;
+ node->returningOldAlias = NULL;
+ node->returningNewAlias = NULL;
+ node->resultRelations = list_make1_int(1);
+ node->onConflictAction = ONCONFLICT_NONE;
+ node->withCheckOptionLists = list_make1(list_copy(newWithCheckOptions));
+
+ mtstate->ps.plan = (Plan *) node;
+ }
+
/* Prepare to catch AFTER triggers. */
AfterTriggerBeginQuery();
@@ -1349,6 +1456,13 @@ CopyFrom(CopyFromState cstate)
ExecComputeStoredGenerated(resultRelInfo, estate, myslot,
CMD_INSERT);
+ /* do row level security policy check */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo,
+ myslot,
+ estate);
+
/*
* If the target is a plain table, check the constraints of
* the tuple.
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index c958ef4d70a1..a315922a3a5f 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -615,6 +615,10 @@ EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dt
-- back from p1r for this because it sorts first
INSERT INTO document VALUES (100, 44, 1, 'regress_rls_dave', 'testing sorting of policies'); -- fail
ERROR: new row violates row-level security policy "p1r" for table "document"
+-- fail, COPY FROM, security policy with subquery not supported
+COPY document FROM STDIN;
+ERROR: COPY FROM not supported with row-level security policy contain subquery
+HINT: Use INSERT statements instead.
-- Just to see a p2r error
INSERT INTO document VALUES (100, 55, 1, 'regress_rls_dave', 'testing sorting of policies'); -- fail
ERROR: new row violates row-level security policy "p2r" for table "document"
@@ -1351,6 +1355,10 @@ EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-- pp1 ERROR
INSERT INTO part_document VALUES (100, 11, 5, 'regress_rls_dave', 'testing pp1'); -- fail
ERROR: new row violates row-level security policy for table "part_document"
+-- fail, COPY FROM, security policy with subquery not supported
+COPY part_document FROM STDIN;
+ERROR: COPY FROM not supported with row-level security policy contain subquery
+HINT: Use INSERT statements instead.
-- pp1r ERROR
INSERT INTO part_document VALUES (100, 99, 1, 'regress_rls_dave', 'testing pp1r'); -- fail
ERROR: new row violates row-level security policy "pp1r" for table "part_document"
@@ -1635,6 +1643,49 @@ EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualifie
Seq Scan on dependent
(1 row)
+--COPY FROM with RLS
+RESET SESSION AUTHORIZATION;
+CREATE TABLE pp (id int,val int) PARTITION BY RANGE (id);
+CREATE TABLE pp_1 (val int, id int);
+ALTER TABLE pp ATTACH PARTITION pp_1 FOR VALUES FROM (1) TO (10);
+CREATE TABLE pp_2 PARTITION OF pp FOR VALUES FROM (10) TO (20);
+ALTER TABLE pp ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pp_1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pp_2 ENABLE ROW LEVEL SECURITY;
+CREATE POLICY pp_1_p1 ON pp_1 FOR INSERT WITH CHECK (id = 6);
+CREATE POLICY p2_pp ON pp FOR ALL USING(id = 1 or id = 2);
+GRANT SELECT, INSERT ON pp TO regress_rls_alice;
+GRANT SELECT, INSERT ON pp_1 TO regress_rls_alice;
+GRANT SELECT, INSERT ON pp_2 TO regress_rls_alice;
+SET SESSION AUTHORIZATION regress_rls_alice;
+INSERT INTO pp_1 VALUES (13, 2); --error
+ERROR: new row violates row-level security policy for table "pp_1"
+INSERT INTO pp_1 VALUES (16, 6); --ok
+COPY pp_1 FROM STDIN WITH DELIMITER ','; --second record not ok
+ERROR: new row violates row-level security policy for table "pp_1"
+CONTEXT: COPY pp_1, line 2: "13,2"
+INSERT INTO pp VALUES (1,11), (2,12);
+INSERT INTO pp values (5,11); --error
+ERROR: new row violates row-level security policy for table "pp"
+INSERT INTO pp values (6,11); --error
+ERROR: new row violates row-level security policy for table "pp"
+COPY pp FROM STDIN WITH DELIMITER ','; --error, second record not ok
+ERROR: new row violates row-level security policy for table "pp"
+CONTEXT: COPY pp, line 2: "5,11"
+COPY pp FROM STDIN WITH DELIMITER ','; --error, second record not ok
+ERROR: new row violates row-level security policy for table "pp"
+CONTEXT: COPY pp, line 2: "6,11"
+RESET SESSION AUTHORIZATION;
+CREATE POLICY p1_pp ON pp FOR INSERT WITH CHECK(id > 4);
+SET SESSION AUTHORIZATION regress_rls_alice;
+INSERT INTO pp VALUES (5, 15), (6, 16); --ok
+INSERT INTO pp VALUES (4, 14); --error
+ERROR: new row violates row-level security policy for table "pp"
+COPY pp FROM STDIN WITH DELIMITER ','; --third record will result error
+ERROR: new row violates row-level security policy for table "pp"
+CONTEXT: COPY pp, line 3: "4,14"
+RESET SESSION AUTHORIZATION;
+DROP TABLE PP;
----- RECURSION ----
--
-- Simple recursion
@@ -4079,9 +4130,7 @@ SET row_security TO OFF;
COPY copy_t FROM STDIN; --fail - would be affected by RLS.
ERROR: query would be affected by row-level security policy for table "copy_t"
SET row_security TO ON;
-COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
-ERROR: COPY FROM not supported with row-level security
-HINT: Use INSERT statements instead.
+COPY copy_t FROM STDIN; --no error
-- Check COPY FROM as user with permissions and BYPASSRLS
SET SESSION AUTHORIZATION regress_rls_exempt_user;
SET row_security TO ON;
@@ -4597,6 +4646,7 @@ ALTER TABLE r1 ENABLE ROW LEVEL SECURITY;
ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
-- Works fine
INSERT INTO r1 VALUES (10), (20);
+COPY r1 FROM STDIN;
-- No error, but no rows
TABLE r1;
a
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 5d923c5ca3bc..85e8e8e33b66 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -283,6 +283,11 @@ EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dt
-- 44 would technically fail for both p2r and p1r, but we should get an error
-- back from p1r for this because it sorts first
INSERT INTO document VALUES (100, 44, 1, 'regress_rls_dave', 'testing sorting of policies'); -- fail
+
+-- fail, COPY FROM, security policy with subquery not supported
+COPY document FROM STDIN;
+\.
+
-- Just to see a p2r error
INSERT INTO document VALUES (100, 55, 1, 'regress_rls_dave', 'testing sorting of policies'); -- fail
@@ -518,6 +523,10 @@ EXPLAIN (COSTS OFF) SELECT * FROM part_document WHERE f_leak(dtitle);
-- pp1 ERROR
INSERT INTO part_document VALUES (100, 11, 5, 'regress_rls_dave', 'testing pp1'); -- fail
+-- fail, COPY FROM, security policy with subquery not supported
+COPY part_document FROM STDIN;
+\.
+
-- pp1r ERROR
INSERT INTO part_document VALUES (100, 99, 1, 'regress_rls_dave', 'testing pp1r'); -- fail
@@ -618,6 +627,56 @@ DROP TABLE dependee CASCADE;
EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualified
+--COPY FROM with RLS
+RESET SESSION AUTHORIZATION;
+CREATE TABLE pp (id int,val int) PARTITION BY RANGE (id);
+CREATE TABLE pp_1 (val int, id int);
+ALTER TABLE pp ATTACH PARTITION pp_1 FOR VALUES FROM (1) TO (10);
+CREATE TABLE pp_2 PARTITION OF pp FOR VALUES FROM (10) TO (20);
+ALTER TABLE pp ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pp_1 ENABLE ROW LEVEL SECURITY;
+ALTER TABLE pp_2 ENABLE ROW LEVEL SECURITY;
+CREATE POLICY pp_1_p1 ON pp_1 FOR INSERT WITH CHECK (id = 6);
+CREATE POLICY p2_pp ON pp FOR ALL USING(id = 1 or id = 2);
+GRANT SELECT, INSERT ON pp TO regress_rls_alice;
+GRANT SELECT, INSERT ON pp_1 TO regress_rls_alice;
+GRANT SELECT, INSERT ON pp_2 TO regress_rls_alice;
+
+SET SESSION AUTHORIZATION regress_rls_alice;
+INSERT INTO pp_1 VALUES (13, 2); --error
+INSERT INTO pp_1 VALUES (16, 6); --ok
+COPY pp_1 FROM STDIN WITH DELIMITER ','; --second record not ok
+16,6
+13,2
+\.
+
+INSERT INTO pp VALUES (1,11), (2,12);
+INSERT INTO pp values (5,11); --error
+INSERT INTO pp values (6,11); --error
+COPY pp FROM STDIN WITH DELIMITER ','; --error, second record not ok
+1,11
+5,11
+\.
+
+COPY pp FROM STDIN WITH DELIMITER ','; --error, second record not ok
+2,12
+6,11
+\.
+
+RESET SESSION AUTHORIZATION;
+CREATE POLICY p1_pp ON pp FOR INSERT WITH CHECK(id > 4);
+SET SESSION AUTHORIZATION regress_rls_alice;
+INSERT INTO pp VALUES (5, 15), (6, 16); --ok
+INSERT INTO pp VALUES (4, 14); --error
+COPY pp FROM STDIN WITH DELIMITER ','; --third record will result error
+5,15
+6,16
+4,14
+\.
+
+RESET SESSION AUTHORIZATION;
+DROP TABLE PP;
+
----- RECURSION ----
--
@@ -1795,8 +1854,10 @@ COPY copy_t FROM STDIN; --ok
SET SESSION AUTHORIZATION regress_rls_bob;
SET row_security TO OFF;
COPY copy_t FROM STDIN; --fail - would be affected by RLS.
+\.
SET row_security TO ON;
-COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS.
+COPY copy_t FROM STDIN; --no error
+\.
-- Check COPY FROM as user with permissions and BYPASSRLS
SET SESSION AUTHORIZATION regress_rls_exempt_user;
@@ -2154,6 +2215,9 @@ ALTER TABLE r1 FORCE ROW LEVEL SECURITY;
-- Works fine
INSERT INTO r1 VALUES (10), (20);
+COPY r1 FROM STDIN;
+10
+\.
-- No error, but no rows
TABLE r1;