diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml new file mode 100644 index 68f8434..34aa438 *** a/doc/src/sgml/catalogs.sgml --- b/doc/src/sgml/catalogs.sgml *************** *** 239,244 **** --- 239,249 ---- + pg_rowsecuritylevelsec + row-level security policy of relation + + + pg_seclabel security labels on database objects *************** *** 1936,1941 **** --- 1941,1955 ---- + relhasrowsecurity + bool + + True if table has (or once had) row-security policy; see + pg_rowsecurity catalog + + + + relhassubclass bool *************** *** 5328,5333 **** --- 5342,5413 ---- + + <structname>pg_rowsecurity</structname> + + + pg_rowsecurity + + + + The catalog pg_rowsecurity stores expression + tree of row-security policy to be performed on a particular relation. + + + + + <structname>pg_rowsecurity</structname> Columns + + + + + Name + Type + References + Description + + + + + + rsecpolname + NameData + + The name of the row-security policy + + + + rsecrelid + oid + pg_class.oid + The table to which the row-security policy is applied + + + + rseccmd + char + + The command to which the row-security policy is applied. + + + + rsecqual + pg_node_tree + + An expression tree that describes what is to be performed by the row-security policy + + + +
+ + + + pg_class.relhasrowsecurity + must be true if a table has row-security policy in this catalog. + + + +
<structname>pg_seclabel</structname> *************** SELECT * FROM pg_locks pl LEFT JOIN pg_p *** 9133,9138 **** --- 9213,9224 ---- pg_class.relhastriggers True if table has (or once had) triggers
+ + hasrowsecurity + boolean + pg_class.relhasrowsecurity + True if table has (or once had) row security policies + diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml new file mode 100644 index b685e16..7aa3128 *** a/doc/src/sgml/ref/allfiles.sgml --- b/doc/src/sgml/ref/allfiles.sgml *************** Complete list of usable sgml source file *** 25,30 **** --- 25,31 ---- + *************** Complete list of usable sgml source file *** 69,74 **** --- 70,76 ---- + *************** Complete list of usable sgml source file *** 110,115 **** --- 112,118 ---- + diff --git a/doc/src/sgml/ref/alter_policy.sgml b/doc/src/sgml/ref/alter_policy.sgml new file mode 100644 index ...04198d3 *** a/doc/src/sgml/ref/alter_policy.sgml --- b/doc/src/sgml/ref/alter_policy.sgml *************** *** 0 **** --- 1,110 ---- + + + + + ALTER POLICY + + + + ALTER POLICY + 7 + SQL - Language Statements + + + + ALTER POLICY + change the condition of a row-security policy + + + + + ALTER POLICY policy name ON table_name + FOR command + USING ( condition ) + + where command can be one of: + + ALL | SELECT | INSERT | UPDATE | DELETE + + + + + Description + + + ALTER POLICY changes the + condition of an existing row-security policy. + + + + To use ALTER POLICY, you must own the table that + the policy applies to. + + + + + Parameters + + + + policy_name + + + The name of an existing policy to alter. + + + + + + table_name + + + The name (optionally schema-qualified) of the table that the + policy applies to. + + + + + + command + + + The command that the policy applies to. + + + + + + condition + + + The new condition for the policy. + + + + + + + + Compatibility + + + ALTER POLICY is a + PostgreSQL language extension, as is the + entire query rewrite system. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml new file mode 100644 index bcd46d5..0471daa *** a/doc/src/sgml/ref/alter_role.sgml --- b/doc/src/sgml/ref/alter_role.sgml *************** ALTER ROLE connlimit | [ ENCRYPTED | UNENCRYPTED ] PASSWORD 'password' | VALID UNTIL 'timestamp' *************** ALTER ROLE { connlimit PASSWORD password ENCRYPTED diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml new file mode 100644 index ...f1e030d *** a/doc/src/sgml/ref/create_policy.sgml --- b/doc/src/sgml/ref/create_policy.sgml *************** *** 0 **** --- 1,124 ---- + + + + + CREATE POLICY + + + + CREATE POLICY + 7 + SQL - Language Statements + + + + CREATE POLICY + define a new row-security policy + + + + + CREATE POLICY policy_name ON table_name + FOR command + USING ( condition ) + + where command can be one of: + + ALL | SELECT | INSERT | UPDATE | DELETE + + + + + Description + + + CREATE POLICY defines a new row-security policy applying to a specified table for a specific command. + + + + The PostgreSQL row-security system allows + one to define per command row level security policies on a table. A policy + consists of a name, a table, a command and a qualifier expression. + + + + + + Parameters + + + + policy_name + + + The name of a policy to create. This must be distinct from the + name of any other policy for the same table and command. + + + + + + table_name + + + The name (optionally schema-qualified) of the table the + policy applies to. + + + + + + command + + + The command to which the policy applies. Valid commands are + ALL, SELECT, + INSERT, UPDATE, + or DELETE + + + + + + condition + + + Any SQL conditional expression (returning + boolean). The condition expression cannot contain + any aggregate or window functions. + + + + + + + + Notes + + + You must be the owner of a table to create or change policies for it. + + + + Compatibility + + + CREATE POLICY is a + PostgreSQL language extension, as is the + entire query rewrite system. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml new file mode 100644 index 641e350..d15c613 *** a/doc/src/sgml/ref/create_role.sgml --- b/doc/src/sgml/ref/create_role.sgml *************** CREATE ROLE connlimit | [ ENCRYPTED | UNENCRYPTED ] PASSWORD 'password' | VALID UNTIL 'timestamp' *************** CREATE ROLE connlimit diff --git a/doc/src/sgml/ref/drop_policy.sgml b/doc/src/sgml/ref/drop_policy.sgml new file mode 100644 index ...fbcad60 *** a/doc/src/sgml/ref/drop_policy.sgml --- b/doc/src/sgml/ref/drop_policy.sgml *************** *** 0 **** --- 1,118 ---- + + + + + DROP POLICY + + + + DROP POLICY + 7 + SQL - Language Statements + + + + DROP POLICY + remove a row-security policy + + + + + DROP POLICY [ IF EXISTS ] policy_name ON table_name + FOR command + + where command can be one of: + + ALL | SELECT | INSERT | UPDATE | DELETE + + + + + Description + + + DROP POLICY drops a row-security policy. + + + + + Parameters + + + + + IF EXISTS + + + Do not throw an error if the policy does not exist. A notice is issued + in this case. + + + + policy_name + + + The name of the policy to drop. + + + + + + table_name + + + The name (optionally schema-qualified) of the table that + the policy applies to. + + + + + + command + + + The command that the policy applies to. + + + + + + + + Examples + + + To drop the row-security policy called p1 on the + table named my_table for the ALL command: + + + DROP POLICY p1 ON my_table FOR ALL; + + + + + + Compatibility + + + DROP POLICY is a + PostgreSQL language extension, as is the + entire query rewrite system. + + + + + See Also + + + + + + + + diff --git a/doc/src/sgml/reference.sgml b/doc/src/sgml/reference.sgml new file mode 100644 index 6ec1263..10c9a6d *** a/doc/src/sgml/reference.sgml --- b/doc/src/sgml/reference.sgml *************** *** 53,58 **** --- 53,59 ---- &alterOperator; &alterOperatorClass; &alterOperatorFamily; + &alterPolicy; &alterRole; &alterRule; &alterSchema; *************** *** 97,102 **** --- 98,104 ---- &createOperator; &createOperatorClass; &createOperatorFamily; + &createPolicy; &createRole; &createRule; &createSchema; *************** *** 138,143 **** --- 140,146 ---- &dropOperatorClass; &dropOperatorFamily; &dropOwned; + &dropPolicy; &dropRole; &dropRule; &dropSchema; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile new file mode 100644 index a974bd5..b257b02 *** a/src/backend/catalog/Makefile --- b/src/backend/catalog/Makefile *************** POSTGRES_BKI_SRCS = $(addprefix $(top_sr *** 39,45 **** pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \ pg_ts_parser.h pg_ts_template.h pg_extension.h \ pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \ ! pg_foreign_table.h \ pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \ toasting.h indexing.h \ ) --- 39,45 ---- pg_ts_config.h pg_ts_config_map.h pg_ts_dict.h \ pg_ts_parser.h pg_ts_template.h pg_extension.h \ pg_foreign_data_wrapper.h pg_foreign_server.h pg_user_mapping.h \ ! pg_foreign_table.h pg_rowsecurity.h \ pg_default_acl.h pg_seclabel.h pg_shseclabel.h pg_collation.h pg_range.h \ toasting.h indexing.h \ ) diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c new file mode 100644 index d9745ca..d30612c *** a/src/backend/catalog/aclchk.c --- b/src/backend/catalog/aclchk.c *************** has_createrole_privilege(Oid roleid) *** 5080,5085 **** --- 5080,5104 ---- return result; } + bool + has_bypassrls_privilege(Oid roleid) + { + bool result = false; + HeapTuple utup; + + /* Superusers bypass all permission checking. */ + if (superuser_arg(roleid)) + return true; + + utup = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid)); + if (HeapTupleIsValid(utup)) + { + result = ((Form_pg_authid) GETSTRUCT(utup))->rolbypassrls; + ReleaseSysCache(utup); + } + return result; + } + /* * Fetch pg_default_acl entry for given role, namespace and object type * (object type must be given in pg_default_acl's encoding). diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c new file mode 100644 index d41ba49..256486c *** a/src/backend/catalog/dependency.c --- b/src/backend/catalog/dependency.c *************** *** 45,50 **** --- 45,51 ---- #include "catalog/pg_opfamily.h" #include "catalog/pg_proc.h" #include "catalog/pg_rewrite.h" + #include "catalog/pg_rowsecurity.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_ts_config.h" *************** *** 57,62 **** --- 58,64 ---- #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" + #include "commands/policy.h" #include "commands/proclang.h" #include "commands/schemacmds.h" #include "commands/seclabel.h" *************** doDeletion(const ObjectAddress *object, *** 1249,1254 **** --- 1251,1260 ---- RemoveEventTriggerById(object->objectId); break; + case OCLASS_ROWSECURITY: + RemovePolicyById(object->objectId); + break; + default: elog(ERROR, "unrecognized object class: %u", object->classId); *************** getObjectClass(const ObjectAddress *obje *** 2316,2321 **** --- 2322,2330 ---- case EventTriggerRelationId: return OCLASS_EVENT_TRIGGER; + + case RowSecurityRelationId: + return OCLASS_ROWSECURITY; } /* shouldn't get here */ diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c new file mode 100644 index 33eef9f..eb64b3d *** a/src/backend/catalog/heap.c --- b/src/backend/catalog/heap.c *************** InsertPgClassTuple(Relation pg_class_des *** 799,804 **** --- 799,805 ---- values[Anum_pg_class_relhaspkey - 1] = BoolGetDatum(rd_rel->relhaspkey); values[Anum_pg_class_relhasrules - 1] = BoolGetDatum(rd_rel->relhasrules); values[Anum_pg_class_relhastriggers - 1] = BoolGetDatum(rd_rel->relhastriggers); + values[Anum_pg_class_relhasrowsecurity - 1] = BoolGetDatum(rd_rel->relhasrowsecurity); values[Anum_pg_class_relhassubclass - 1] = BoolGetDatum(rd_rel->relhassubclass); values[Anum_pg_class_relispopulated - 1] = BoolGetDatum(rd_rel->relispopulated); values[Anum_pg_class_relreplident - 1] = CharGetDatum(rd_rel->relreplident); diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c new file mode 100644 index d143a44..b16293c *** a/src/backend/catalog/objectaddress.c --- b/src/backend/catalog/objectaddress.c *************** getObjectDescription(const ObjectAddress *** 2166,2171 **** --- 2166,2227 ---- break; } + case OCLASS_ROWSECURITY: + { + Relation rsec_rel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + Form_pg_rowsecurity form_rsec; + + rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock); + + ScanKeyInit(&skey, + ObjectIdAttributeNumber, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(object->objectId)); + + sscan = systable_beginscan(rsec_rel, RowSecurityOidIndexId, + true, NULL, 1, &skey); + + tuple = systable_getnext(sscan); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for row-security relation %u", + object->objectId); + + form_rsec = (Form_pg_rowsecurity) GETSTRUCT(tuple); + + appendStringInfo(&buffer, _("row-security of ")); + getRelationDescription(&buffer, form_rsec->rsecrelid); + + switch(form_rsec->rseccmd) + { + case ROWSECURITY_CMD_ALL: + appendStringInfo(&buffer, _(" FOR ALL")); + break; + case ROWSECURITY_CMD_SELECT: + appendStringInfo(&buffer, _(" FOR SELECT")); + break; + case ROWSECURITY_CMD_INSERT: + appendStringInfo(&buffer, _(" FOR INSERT")); + break; + case ROWSECURITY_CMD_UPDATE: + appendStringInfo(&buffer, _(" FOR UPDATE")); + break; + case ROWSECURITY_CMD_DELETE: + appendStringInfo(&buffer, _(" FOR DELETE")); + break; + default: + elog(ERROR, "unrecognized row-security command type: %c", + form_rsec->rseccmd); + } + + systable_endscan(sscan); + heap_close(rsec_rel, AccessShareLock); + break; + } + default: appendStringInfo(&buffer, "unrecognized object %u %u %d", object->classId, diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql new file mode 100644 index 1bde175..fb7284d *** a/src/backend/catalog/system_views.sql --- b/src/backend/catalog/system_views.sql *************** CREATE VIEW pg_roles AS *** 19,24 **** --- 19,25 ---- rolconnlimit, '********'::text as rolpassword, rolvaliduntil, + rolbypassrls, setconfig as rolconfig, pg_authid.oid FROM pg_authid LEFT JOIN pg_db_role_setting s *************** CREATE VIEW pg_tables AS *** 89,95 **** T.spcname AS tablespace, C.relhasindex AS hasindexes, C.relhasrules AS hasrules, ! C.relhastriggers AS hastriggers FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) LEFT JOIN pg_tablespace T ON (T.oid = C.reltablespace) WHERE C.relkind = 'r'; --- 90,97 ---- T.spcname AS tablespace, C.relhasindex AS hasindexes, C.relhasrules AS hasrules, ! C.relhastriggers AS hastriggers, ! c.relhasrowsecurity AS hasrowsecurity FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) LEFT JOIN pg_tablespace T ON (T.oid = C.reltablespace) WHERE C.relkind = 'r'; diff --git a/src/backend/commands/Makefile b/src/backend/commands/Makefile new file mode 100644 index 22f116b..b1ac704 *** a/src/backend/commands/Makefile --- b/src/backend/commands/Makefile *************** OBJS = aggregatecmds.o alter.o analyze.o *** 17,23 **** dbcommands.o define.o discard.o dropcmds.o \ event_trigger.o explain.o extension.o foreigncmds.o functioncmds.o \ indexcmds.o lockcmds.o matview.o operatorcmds.o opclasscmds.o \ ! portalcmds.o prepare.o proclang.o \ schemacmds.o seclabel.o sequence.o tablecmds.o tablespace.o trigger.o \ tsearchcmds.o typecmds.o user.o vacuum.o vacuumlazy.o \ variable.o view.o --- 17,23 ---- dbcommands.o define.o discard.o dropcmds.o \ event_trigger.o explain.o extension.o foreigncmds.o functioncmds.o \ indexcmds.o lockcmds.o matview.o operatorcmds.o opclasscmds.o \ ! policy.o portalcmds.o prepare.o proclang.o \ schemacmds.o seclabel.o sequence.o tablecmds.o tablespace.o trigger.o \ tsearchcmds.o typecmds.o user.o vacuum.o vacuumlazy.o \ variable.o view.o diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c new file mode 100644 index fbd7492..e609f1d *** a/src/backend/commands/copy.c --- b/src/backend/commands/copy.c *************** *** 37,43 **** --- 37,45 ---- #include "optimizer/clauses.h" #include "optimizer/planner.h" #include "parser/parse_relation.h" + #include "nodes/makefuncs.h" #include "rewrite/rewriteHandler.h" + #include "rewrite/rowsecurity.h" #include "storage/fd.h" #include "tcop/tcopprot.h" #include "utils/acl.h" *************** DoCopy(const CopyStmt *stmt, const char *** 784,789 **** --- 786,792 ---- bool pipe = (stmt->filename == NULL); Relation rel; Oid relid; + Node *query = NULL; /* Disallow COPY to/from file or program except to superusers. */ if (!pipe && !superuser()) *************** DoCopy(const CopyStmt *stmt, const char *** 816,847 **** rel = heap_openrv(stmt->relation, (is_from ? RowExclusiveLock : AccessShareLock)); ! relid = RelationGetRelid(rel); ! rte = makeNode(RangeTblEntry); ! rte->rtekind = RTE_RELATION; ! rte->relid = RelationGetRelid(rel); ! rte->relkind = rel->rd_rel->relkind; ! rte->requiredPerms = required_access; ! tupDesc = RelationGetDescr(rel); ! attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist); ! foreach(cur, attnums) { ! int attno = lfirst_int(cur) - ! FirstLowInvalidHeapAttributeNumber; ! if (is_from) ! rte->modifiedCols = bms_add_member(rte->modifiedCols, attno); ! else ! rte->selectedCols = bms_add_member(rte->selectedCols, attno); } - ExecCheckRTPerms(list_make1(rte), true); } else { Assert(stmt->query); relid = InvalidOid; rel = NULL; } --- 819,892 ---- rel = heap_openrv(stmt->relation, (is_from ? RowExclusiveLock : AccessShareLock)); ! /* ! * If the relation has a row security policy, then perform a "query" ! * copy. This will allow for the policies to be applied appropriately ! * to the relation. ! */ ! if (rel->rd_rel->relhasrowsecurity) ! { ! SelectStmt *select; ! ColumnRef *cr; ! ResTarget *target; ! RangeVar *from; ! /* Build target list */ ! cr = makeNode(ColumnRef); ! cr->fields = list_make1(makeNode(A_Star)); ! cr->location = 1; ! target = makeNode(ResTarget); ! target->name = NULL; ! target->indirection = NIL; ! target->val = (Node *) cr; ! target->location = 1; ! ! /* Build FROM clause */ ! from = makeRangeVar(NULL, RelationGetRelationName(rel), 1); ! ! /* Build query */ ! select = makeNode(SelectStmt); ! select->targetList = list_make1(target); ! select->fromClause = list_make1(from); ! ! query = (Node*) select; ! ! relid = InvalidOid; ! ! /* Close the handle to the relation as it is no longer needed. */ ! heap_close(rel, (is_from ? RowExclusiveLock : AccessShareLock)); ! rel = NULL; ! } ! else { ! relid = RelationGetRelid(rel); ! rte = makeNode(RangeTblEntry); ! rte->rtekind = RTE_RELATION; ! rte->relid = RelationGetRelid(rel); ! rte->relkind = rel->rd_rel->relkind; ! rte->requiredPerms = required_access; ! tupDesc = RelationGetDescr(rel); ! attnums = CopyGetAttnums(tupDesc, rel, stmt->attlist); ! foreach(cur, attnums) ! { ! int attno = lfirst_int(cur) - ! FirstLowInvalidHeapAttributeNumber; ! ! if (is_from) ! rte->modifiedCols = bms_add_member(rte->modifiedCols, attno); ! else ! rte->selectedCols = bms_add_member(rte->selectedCols, attno); ! } ! ExecCheckRTPerms(list_make1(rte), true); } } else { Assert(stmt->query); + query = stmt->query; relid = InvalidOid; rel = NULL; } *************** DoCopy(const CopyStmt *stmt, const char *** 861,867 **** } else { ! cstate = BeginCopyTo(rel, stmt->query, queryString, stmt->filename, stmt->is_program, stmt->attlist, stmt->options); *processed = DoCopyTo(cstate); /* copy from database to file */ --- 906,912 ---- } else { ! cstate = BeginCopyTo(rel, query, queryString, stmt->filename, stmt->is_program, stmt->attlist, stmt->options); *processed = DoCopyTo(cstate); /* copy from database to file */ diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c new file mode 100644 index 754264e..2584ef9 *** a/src/backend/commands/event_trigger.c --- b/src/backend/commands/event_trigger.c *************** EventTriggerSupportsObjectClass(ObjectCl *** 995,1000 **** --- 995,1001 ---- case OCLASS_USER_MAPPING: case OCLASS_DEFACL: case OCLASS_EXTENSION: + case OCLASS_ROWSECURITY: return true; case MAX_OCLASS: diff --git a/src/backend/commands/policy.c b/src/backend/commands/policy.c new file mode 100644 index ...9c149d1 *** a/src/backend/commands/policy.c --- b/src/backend/commands/policy.c *************** *** 0 **** --- 1,723 ---- + /*------------------------------------------------------------------------- + * + * policy.c + * Commands for manipulating policies. + * + * Portions Copyright (c) 1996-2014, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/commands/policy.c + * + *------------------------------------------------------------------------- + */ + #include "postgres.h" + + #include "access/genam.h" + #include "access/heapam.h" + #include "access/htup_details.h" + #include "access/sysattr.h" + #include "catalog/catalog.h" + #include "catalog/dependency.h" + #include "catalog/indexing.h" + #include "catalog/namespace.h" + #include "catalog/objectaddress.h" + #include "catalog/pg_class.h" + #include "catalog/pg_rowsecurity.h" + #include "catalog/pg_type.h" + #include "commands/policy.h" + #include "miscadmin.h" + #include "nodes/nodeFuncs.h" + #include "nodes/pg_list.h" + #include "optimizer/clauses.h" + #include "parser/parse_clause.h" + #include "parser/parse_node.h" + #include "parser/parse_relation.h" + #include "storage/lock.h" + #include "utils/acl.h" + #include "utils/builtins.h" + #include "utils/fmgroids.h" + #include "utils/inval.h" + #include "utils/rel.h" + #include "utils/syscache.h" + + static void RangeVarCallbackForCreatePolicy(const RangeVar *rv, + Oid relid, Oid oldrelid, void *arg); + static const char parse_row_security_command(const char *cmd_name); + static RowSecurityEntry* create_row_security_entry(Oid id, Expr *qual, + MemoryContext context); + + /* + * Callback to RangeVarGetRelidExtended(). + * + * Checks the following: + * - the relation specified is a table. + * - current user owns the table. + * - the table is not a system table. + * + * If any of these checks fails then an error is raised. + */ + static void + RangeVarCallbackForCreatePolicy(const RangeVar *rv, Oid relid, Oid oldrelid, + void *arg) + { + HeapTuple tuple; + Form_pg_class classform; + char relkind; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return; + classform = (Form_pg_class) GETSTRUCT(tuple); + relkind = classform->relkind; + + /* Must own relation. */ + if (!pg_class_ownercheck(relid, GetUserId())) + aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, rv->relname); + + /* No system table modifications unless explicitly allowed. */ + if (!allowSystemTableMods && IsSystemClass(relid, classform)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rv->relname))); + + /* Relation type MUST be a table. */ + if (relkind != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", rv->relname))); + + ReleaseSysCache(tuple); + } + + /* + * parse_row_security_command - + * helper function to convert full commands strings to their char + * representation. + * + * cmd_name - full string command name. Valid values are 'all', 'select', + * 'insert', 'update' and 'delete'. + * + */ + static const char + parse_row_security_command(const char *cmd_name) + { + char cmd; + + if (strcmp(cmd_name, "all") == 0) + cmd = ROWSECURITY_CMD_ALL; + else if (strcmp(cmd_name, "select") == 0) + cmd = ROWSECURITY_CMD_SELECT; + else if (strcmp(cmd_name, "insert") == 0) + cmd = ROWSECURITY_CMD_INSERT; + else if (strcmp(cmd_name, "update") == 0) + cmd = ROWSECURITY_CMD_UPDATE; + else if (strcmp(cmd_name, "delete") == 0) + cmd = ROWSECURITY_CMD_DELETE; + else + elog(ERROR, "Unregonized command."); + /* error unrecognized command */ + + return cmd; + } + + /* + * create_row_security_entry - + * helper function to create a RowSecurityEntry. + * + * id - the oid of the row-security policy. + * qual - the qualifier expression of the row-security policy. + * context - the memory context to which the entry belongs. + */ + static RowSecurityEntry * + create_row_security_entry(Oid id, Expr *qual, MemoryContext context) + { + RowSecurityEntry *entry; + + entry = MemoryContextAllocZero(context, sizeof(RowSecurityEntry)); + entry->rsecid = id; + entry->qual = copyObject(qual); + entry->hassublinks = contain_subplans((Node *) entry->qual); + + return entry; + } + + /* + * Load row-security policy from the catalog, and keep it in + * the relation cache. + */ + void + RelationBuildRowSecurity(Relation relation) + { + Relation catalog; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + MemoryContext oldcxt; + MemoryContext rscxt = NULL; + RowSecurityDesc *rsdesc = NULL; + + catalog = heap_open(RowSecurityRelationId, AccessShareLock); + + ScanKeyInit(&skey, + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(RelationGetRelid(relation))); + + sscan = systable_beginscan(catalog, RowSecurityRelidIndexId, true, + NULL, 1, &skey); + PG_TRY(); + { + /* + * Loop through the row-level security entries for this relation, if + * any. While we currently only support one command type for row-level + * security, eventually we will support multiple types and we will + * need to find the correct one (or possibly merge them?). + */ + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + Datum value_datum; + char cmd_value; + char *qual_value; + Expr *qual_expr; + char *policy_name_value; + Oid policy_id; + ListCell *item; + bool isnull; + RowSecurityPolicy *policy = NULL; + RowSecurityPolicy *temp_policy; + + /* + * Set up the memory context inside our loop to ensure we are only + * building it when we actually need it. + */ + if (!rsdesc) + { + rscxt = AllocSetContextCreate(CacheMemoryContext, + "Row-security descriptor", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + rsdesc = MemoryContextAllocZero(rscxt, sizeof(RowSecurityDesc)); + rsdesc->rscxt = rscxt; + } + + /* Get policy command */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd, + RelationGetDescr(catalog), &isnull); + Assert(!isnull); + cmd_value = DatumGetChar(value_datum); + + /* Get policy name */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecpolname, + RelationGetDescr(catalog), &isnull); + Assert(!isnull); + policy_name_value = DatumGetCString(value_datum); + + /* Get policy qual */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual, + RelationGetDescr(catalog), &isnull); + Assert(!isnull); + qual_value = TextDatumGetCString(value_datum); + qual_expr = (Expr *) stringToNode(qual_value); + + policy_id = HeapTupleGetOid(tuple); + + /* Find policy description for policy based on policy name.*/ + foreach(item, rsdesc->policies) + { + temp_policy = (RowSecurityPolicy *) lfirst(item); + + if (strcmp(temp_policy->policy_name, policy_name_value) == 0) + { + policy = temp_policy; + break; + } + } + + oldcxt = MemoryContextSwitchTo(rscxt); + + /* + * If no policy was found in the list, create a new one and add it + * to the list. + */ + if (!policy) + { + policy = MemoryContextAllocZero(rscxt, sizeof(RowSecurityPolicy)); + policy->policy_name = policy_name_value; + rsdesc->policies = lcons(policy, rsdesc->policies); + } + + /* Set policy information by command */ + switch (cmd_value) + { + case ROWSECURITY_CMD_ALL: + policy->rsall = create_row_security_entry(policy_id, + qual_expr, rscxt); + break; + case ROWSECURITY_CMD_SELECT: + policy->rsselect = create_row_security_entry(policy_id, + qual_expr, rscxt); + break; + case ROWSECURITY_CMD_INSERT: + policy->rsinsert = create_row_security_entry(policy_id, + qual_expr, rscxt); + break; + case ROWSECURITY_CMD_UPDATE: + policy->rsupdate = create_row_security_entry(policy_id, + qual_expr, rscxt); + break; + case ROWSECURITY_CMD_DELETE: + policy->rsdelete = create_row_security_entry(policy_id, + qual_expr, rscxt); + break; + default: + elog(ERROR, "Unregonized command for row-security policy"); + } + + MemoryContextSwitchTo(oldcxt); + + pfree(qual_expr); + } + } + PG_CATCH(); + { + if (rscxt != NULL) + MemoryContextDelete(rscxt); + PG_RE_THROW(); + } + PG_END_TRY(); + + systable_endscan(sscan); + heap_close(catalog, AccessShareLock); + + relation->rsdesc = rsdesc; + } + + /* + * RemovePolicyById - + * remove a row-security policy by its OID. If a policy does not exist with + * the provide oid, then an error is raised. + * + * policy_id - the oid of the row-security policy. + */ + void + RemovePolicyById(Oid policy_id) + { + Relation pg_rowsecurity_rel; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + Relation rel; + Oid relid; + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + ScanKeyInit(&skey, + ObjectIdAttributeNumber, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(policy_id)); + + sscan = systable_beginscan(pg_rowsecurity_rel, RowSecurityOidIndexId, true, + NULL, 1, &skey); + + tuple = systable_getnext(sscan); + + /* If the policy exists, then remote it, otherwise raise an error. */ + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "could not find tuple for row-security %u", policy_id); + + relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid; + + rel = heap_open(relid, AccessExclusiveLock); + + simple_heap_delete(pg_rowsecurity_rel, &tuple->t_self); + + CacheInvalidateRelcache(rel); + + /* Clean up */ + heap_close(rel, AccessExclusiveLock); + systable_endscan(sscan); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + } + + /* + * CreatePolicy - + * handles the execution of the CREATE POLICY command. + * + * stmt - the CreatePolicyStmt that describes the policy to create. + */ + Oid + CreatePolicy(CreatePolicyStmt *stmt) + { + Relation pg_rowsecurity_rel; + Oid rowsec_id; + Relation target_table; + Oid table_id; + char rseccmd; + ParseState *pstate; + RangeTblEntry *rte; + Node *qual; + ScanKeyData skeys[3]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + Datum values[Natts_pg_rowsecurity]; + bool isnull[Natts_pg_rowsecurity]; + ObjectAddress target; + ObjectAddress myself; + + /* Parse command */ + rseccmd = parse_row_security_command(stmt->cmd); + + /* Parse the supplied clause */ + pstate = make_parsestate(NULL); + + /* zero-clear */ + memset(values, 0, sizeof(values)); + memset(isnull, 0, sizeof(isnull)); + + /* Get id of table. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForCreatePolicy, + (void *) stmt); + + /* Open target_table to build qual. No lock is necessary.*/ + target_table = relation_open(table_id, NoLock); + + + rte = addRangeTableEntryForRelation(pstate, target_table, + NULL, false, false); + addRTEtoQuery(pstate, rte, false, true, true); + + qual = transformWhereClause(pstate, copyObject(stmt->qual), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + /* Open pg_rowsecurity catalog */ + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Set key - row security policy name. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + /* Set key - row security relation id. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security command. */ + ScanKeyInit(&skeys[2], + Anum_pg_rowsecurity_rseccmd, + BTEqualStrategyNumber, F_CHAREQ, + CharGetDatum(rseccmd)); + + sscan = systable_beginscan(pg_rowsecurity_rel, RowSecurityRelidIndexId, + true, NULL, 3, skeys); + + rsec_tuple = systable_getnext(sscan); + + /* + * If the policy does not already exist, then create it. Otherwise, raise + * an error notifying that the policy already exists. + */ + if (!HeapTupleIsValid(rsec_tuple)) + { + values[Anum_pg_rowsecurity_rsecrelid - 1] + = ObjectIdGetDatum(table_id); + values[Anum_pg_rowsecurity_rsecpolname - 1] + = CStringGetDatum(stmt->policy_name); + values[Anum_pg_rowsecurity_rseccmd - 1] + = CharGetDatum(rseccmd); + values[Anum_pg_rowsecurity_rsecqual -1] + = CStringGetTextDatum(nodeToString(qual)); + rsec_tuple = heap_form_tuple(RelationGetDescr(pg_rowsecurity_rel), + values, isnull); + rowsec_id = simple_heap_insert(pg_rowsecurity_rel, rsec_tuple); + } + else + { + elog(ERROR, "Table \"%s\" already has a policy named \"%s\"." + " Use a different name for the policy or to modify this policy" + " use ALTER POLICY %s ON %s USING (qual)", + RelationGetRelationName(target_table), stmt->policy_name, + RelationGetRelationName(target_table), stmt->policy_name); + } + + /* Update Indexes */ + CatalogUpdateIndexes(pg_rowsecurity_rel, rsec_tuple); + + /* Record Dependencies */ + target.classId = RelationRelationId; + target.objectId = table_id; + target.objectSubId = 0; + + myself.classId = RowSecurityRelationId; + myself.objectId = rowsec_id; + myself.objectSubId = 0; + + recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); + + recordDependencyOnExpr(&myself, qual, pstate->p_rtable, + DEPENDENCY_NORMAL); + + /* Turn on relhasrowsecurity on table. */ + if (!RelationGetForm(target_table)->relhasrowsecurity) + { + Relation class_rel = heap_open(RelationRelationId, RowExclusiveLock); + + HeapTuple tuple; + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(table_id)); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache look up failed for relation %u", table_id); + + ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true; + + simple_heap_update(class_rel, &tuple->t_self, tuple); + CatalogUpdateIndexes(class_rel, tuple); + + heap_freetuple(tuple); + heap_close(class_rel, RowExclusiveLock); + } + + /* Invalidate Relation Cache */ + CacheInvalidateRelcache(target_table); + + /* Clean up. */ + heap_freetuple(rsec_tuple); + free_parsestate(pstate); + systable_endscan(sscan); + relation_close(target_table, NoLock); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + + return rowsec_id; + } + + /* + * AlterPolicy - + * handles the execution of the ALTER POLICY command. + * + * stmt - the AlterPolicyStmt that describes the policy and how to alter it. + */ + Oid + AlterPolicy(AlterPolicyStmt *stmt) + { + Relation pg_rowsecurity_rel; + Oid rowsec_id; + Relation target_table; + Oid table_id; + char rseccmd; + ParseState *pstate; + RangeTblEntry *rte; + Node *qual; + ScanKeyData skeys[3]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + HeapTuple new_tuple; + Datum values[Natts_pg_rowsecurity]; + bool isnull[Natts_pg_rowsecurity]; + bool replaces[Natts_pg_rowsecurity]; + ObjectAddress target; + ObjectAddress myself; + + /* Parse command */ + rseccmd = parse_row_security_command(stmt->cmd); + + /* Get id of table. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForCreatePolicy, + (void *) stmt); + + target_table = relation_open(table_id, NoLock); + + /* Parse the row-security clause */ + pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(pstate, target_table, + NULL, false, false); + + addRTEtoQuery(pstate, rte, false, true, true); + + qual = transformWhereClause(pstate, copyObject(stmt->qual), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + /* zero-clear */ + memset(values, 0, sizeof(values)); + memset(replaces, 0, sizeof(replaces)); + memset(isnull, 0, sizeof(isnull)); + + /* Find policy to update. */ + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Set key - row security policy name. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + /* Set key - row security relation id. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security command. */ + ScanKeyInit(&skeys[2], + Anum_pg_rowsecurity_rseccmd, + BTEqualStrategyNumber, F_CHAREQ, + CharGetDatum(rseccmd)); + + sscan = systable_beginscan(pg_rowsecurity_rel, RowSecurityRelidIndexId, + true, NULL, 3, skeys); + + rsec_tuple = systable_getnext(sscan); + + /* If the policy exists, then alter it. Otherwise, raise an error. */ + if (HeapTupleIsValid(rsec_tuple)) + { + rowsec_id = HeapTupleGetOid(rsec_tuple); + + replaces[Anum_pg_rowsecurity_rsecqual - 1] = true; + + values[Anum_pg_rowsecurity_rsecqual -1] + = CStringGetTextDatum(nodeToString(qual)); + + new_tuple = heap_modify_tuple(rsec_tuple, + RelationGetDescr(pg_rowsecurity_rel), + values, isnull, replaces); + simple_heap_update(pg_rowsecurity_rel, &new_tuple->t_self, new_tuple); + + /* Update Catalog Indexes */ + CatalogUpdateIndexes(pg_rowsecurity_rel, new_tuple); + + /* Update Dependencies. */ + deleteDependencyRecordsFor(RowSecurityRelationId, rowsec_id, false); + + /* Record Dependencies */ + target.classId = RelationRelationId; + target.objectId = table_id; + target.objectSubId = 0; + + myself.classId = RowSecurityRelationId; + myself.objectId = rowsec_id; + myself.objectSubId = 0; + + + recordDependencyOn(&myself, &target, DEPENDENCY_AUTO); + + recordDependencyOnExpr(&myself, qual, pstate->p_rtable, + DEPENDENCY_NORMAL); + + heap_freetuple(new_tuple); + } else { + elog(ERROR, "policy %s for %s does not exist on table %s", + stmt->policy_name, stmt->cmd, + RelationGetRelationName(target_table)); + } + + /* Invalidate Relation Cache */ + CacheInvalidateRelcache(target_table); + + /* Clean up. */ + free_parsestate(pstate); + systable_endscan(sscan); + relation_close(target_table, NoLock); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + + return rowsec_id; + } + + /* + * DropPolicy - + * handle the execution of the DROP POLICY command. + * + * stmt - the DropPolicyStmt that describes the policy to drop. + */ + void + DropPolicy(DropPolicyStmt *stmt) + { + Relation pg_rowsecurity_rel; + Oid table_id; + char rseccmd; + ScanKeyData skeys[3]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + + /* Parse command */ + rseccmd = parse_row_security_command(stmt->cmd); + + /* Get id of target table. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForCreatePolicy, + (void *) stmt); + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Add key - row security policy name. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + /* Add key - row security relation id. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + ScanKeyInit(&skeys[2], + Anum_pg_rowsecurity_rseccmd, + BTEqualStrategyNumber, F_CHAREQ, + CharGetDatum(rseccmd)); + + sscan = systable_beginscan(pg_rowsecurity_rel, RowSecurityRelidIndexId, + true, NULL, 3, skeys); + + rsec_tuple = systable_getnext(sscan); + + /* + * If the policy exists, then remove it. If policy does not exists and + * the statment uses IF EXISTS, then raise a notice. If policy does not + * exist and the statment does not use IF EXISTS, then raise an error. + */ + if (HeapTupleIsValid(rsec_tuple)) + { + ObjectAddress address; + + address.classId = RowSecurityRelationId; + address.objectId = HeapTupleHeaderGetOid(rsec_tuple->t_data); + address.objectSubId = 0; + + performDeletion(&address, DROP_RESTRICT, 0); + } + else + { + if (!stmt->missing_ok) + { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("row-security policy \"%s\" does not exist on table" + " \"%s\" for \"%s\"", + stmt->policy_name, stmt->table->relname, stmt->cmd))); + } + else + { + ereport(NOTICE, + (errmsg("row-security policy \"%s\" does not exist on table" + " \"%s\" for \"%s\", skipping", + stmt->policy_name, stmt->table->relname, stmt->cmd))); + } + } + + /* Clean up. */ + systable_endscan(sscan); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + } diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c new file mode 100644 index d3a2044..1e3fe2d *** a/src/backend/commands/user.c --- b/src/backend/commands/user.c *************** CreateRole(CreateRoleStmt *stmt) *** 87,92 **** --- 87,93 ---- bool createdb = false; /* Can the user create databases? */ bool canlogin = false; /* Can this user login? */ bool isreplication = false; /* Is this a replication role? */ + bool bypassrls = false; /* Is this a row security enabled role? */ int connlimit = -1; /* maximum connections allowed */ List *addroleto = NIL; /* roles to make this a member of */ List *rolemembers = NIL; /* roles to be members of this role */ *************** CreateRole(CreateRoleStmt *stmt) *** 106,111 **** --- 107,113 ---- DefElem *drolemembers = NULL; DefElem *dadminmembers = NULL; DefElem *dvalidUntil = NULL; + DefElem *dbypassRLS = NULL; /* The defaults can vary depending on the original statement type */ switch (stmt->stmt_type) *************** CreateRole(CreateRoleStmt *stmt) *** 232,237 **** --- 234,247 ---- errmsg("conflicting or redundant options"))); dvalidUntil = defel; } + else if (strcmp(defel->defname, "bypassrls") == 0) + { + if (dbypassRLS) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + dbypassRLS = defel; + } else elog(ERROR, "option \"%s\" not recognized", defel->defname); *************** CreateRole(CreateRoleStmt *stmt) *** 267,272 **** --- 277,284 ---- adminmembers = (List *) dadminmembers->arg; if (dvalidUntil) validUntil = strVal(dvalidUntil->arg); + if (dbypassRLS) + bypassrls = intVal(dbypassRLS->arg) != 0; /* Check some permissions first */ if (issuper) *************** CreateRole(CreateRoleStmt *stmt) *** 283,288 **** --- 295,307 ---- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create replication users"))); } + else if (bypassrls) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to change row security attribute."))); + } else { if (!have_createrole_privilege()) *************** CreateRole(CreateRoleStmt *stmt) *** 375,380 **** --- 394,401 ---- new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum; new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; + new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls); + tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls); /* *************** AlterRole(AlterRoleStmt *stmt) *** 469,474 **** --- 490,496 ---- char *validUntil = NULL; /* time the login is valid until */ Datum validUntil_datum; /* same, as timestamptz Datum */ bool validUntil_null; + bool bypassrls = -1; DefElem *dpassword = NULL; DefElem *dissuper = NULL; DefElem *dinherit = NULL; *************** AlterRole(AlterRoleStmt *stmt) *** 479,484 **** --- 501,507 ---- DefElem *dconnlimit = NULL; DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; + DefElem *dbypassRLS = NULL; Oid roleid; /* Extract options from the statement node tree */ *************** AlterRole(AlterRoleStmt *stmt) *** 573,578 **** --- 596,609 ---- errmsg("conflicting or redundant options"))); dvalidUntil = defel; } + else if (strcmp(defel->defname, "bypassrls") == 0) + { + if (dbypassRLS) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("conflicting or redundant options"))); + dbypassRLS = defel; + } else elog(ERROR, "option \"%s\" not recognized", defel->defname); *************** AlterRole(AlterRoleStmt *stmt) *** 604,609 **** --- 635,642 ---- rolemembers = (List *) drolemembers->arg; if (dvalidUntil) validUntil = strVal(dvalidUntil->arg); + if (dbypassRLS) + bypassrls = intVal(dbypassRLS->arg); /* * Scan the pg_authid relation to be certain the user exists. *************** AlterRole(AlterRoleStmt *stmt) *** 637,642 **** --- 670,682 ---- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to alter replication users"))); } + else if (((Form_pg_authid) GETSTRUCT(tuple))->rolbypassrls || bypassrls >= 0) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("must be superuser to alter row security attribute"))); + } else if (!have_createrole_privilege()) { if (!(inherit < 0 && *************** AlterRole(AlterRoleStmt *stmt) *** 770,775 **** --- 810,821 ---- new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null; new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true; + if (bypassrls >= 0) + { + new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(bypassrls > 0); + new_record_repl[Anum_pg_authid_rolbypassrls - 1] = true; + } + new_tuple = heap_modify_tuple(tuple, pg_authid_dsc, new_record, new_record_nulls, new_record_repl); simple_heap_update(pg_authid_rel, &tuple->t_self, new_tuple); diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c new file mode 100644 index 3088578..ccbfdd8 *** a/src/backend/nodes/copyfuncs.c --- b/src/backend/nodes/copyfuncs.c *************** _copyAlterTSConfigurationStmt(const Alte *** 3849,3854 **** --- 3849,3892 ---- return newnode; } + static CreatePolicyStmt * + _copyCreatePolicyStmt(const CreatePolicyStmt *from) + { + CreatePolicyStmt *newnode = makeNode(CreatePolicyStmt); + + COPY_STRING_FIELD(policy_name); + COPY_NODE_FIELD(table); + COPY_SCALAR_FIELD(cmd); + COPY_NODE_FIELD(qual); + + return newnode; + } + + static AlterPolicyStmt * + _copyAlterPolicyStmt(const AlterPolicyStmt *from) + { + AlterPolicyStmt *newnode = makeNode(AlterPolicyStmt); + + COPY_STRING_FIELD(policy_name); + COPY_NODE_FIELD(table); + COPY_SCALAR_FIELD(cmd); + COPY_NODE_FIELD(qual); + + return newnode; + } + + static DropPolicyStmt * + _copyDropPolicyStmt(const DropPolicyStmt *from) + { + DropPolicyStmt *newnode = makeNode(DropPolicyStmt); + + COPY_STRING_FIELD(policy_name); + COPY_NODE_FIELD(table); + COPY_SCALAR_FIELD(cmd); + + return newnode; + } + /* **************************************************************** * pg_list.h copy functions * **************************************************************** *************** copyObject(const void *from) *** 4561,4566 **** --- 4599,4613 ---- case T_AlterTSConfigurationStmt: retval = _copyAlterTSConfigurationStmt(from); break; + case T_CreatePolicyStmt: + retval = _copyCreatePolicyStmt(from); + break; + case T_AlterPolicyStmt: + retval = _copyAlterPolicyStmt(from); + break; + case T_DropPolicyStmt: + retval = _copyDropPolicyStmt(from); + break; case T_A_Expr: retval = _copyAExpr(from); diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c new file mode 100644 index 1b07db6..e8b4226 *** a/src/backend/nodes/equalfuncs.c --- b/src/backend/nodes/equalfuncs.c *************** _equalAlterTSConfigurationStmt(const Alt *** 2008,2013 **** --- 2008,2045 ---- } static bool + _equalCreatePolicyStmt(const CreatePolicyStmt *a, const CreatePolicyStmt *b) + { + COMPARE_STRING_FIELD(policy_name); + COMPARE_NODE_FIELD(table); + COMPARE_SCALAR_FIELD(cmd); + COMPARE_NODE_FIELD(qual); + + return true; + } + + static bool + _equalAlterPolicyStmt(const AlterPolicyStmt *a, const AlterPolicyStmt *b) + { + COMPARE_STRING_FIELD(policy_name); + COMPARE_NODE_FIELD(table); + COMPARE_SCALAR_FIELD(cmd); + COMPARE_NODE_FIELD(qual); + + return true; + } + + static bool + _equalDropPolicyStmt(const DropPolicyStmt *a, const DropPolicyStmt *b) + { + COMPARE_STRING_FIELD(policy_name); + COMPARE_NODE_FIELD(table); + COMPARE_SCALAR_FIELD(cmd); + + return true; + } + + static bool _equalAExpr(const A_Expr *a, const A_Expr *b) { COMPARE_SCALAR_FIELD(kind); *************** equal(const void *a, const void *b) *** 3025,3030 **** --- 3057,3071 ---- case T_AlterTSConfigurationStmt: retval = _equalAlterTSConfigurationStmt(a, b); break; + case T_CreatePolicyStmt: + retval = _equalCreatePolicyStmt(a, b); + break; + case T_AlterPolicyStmt: + retval = _equalAlterPolicyStmt(a, b); + break; + case T_DropPolicyStmt: + retval = _equalDropPolicyStmt(a, b); + break; case T_A_Expr: retval = _equalAExpr(a, b); diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index a113809..311b658 *** a/src/backend/parser/gram.y --- b/src/backend/parser/gram.y *************** static Node *makeRecursiveViewSelect(cha *** 231,237 **** AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterSystemStmt AlterTableStmt AlterTblSpcStmt AlterExtensionStmt AlterExtensionContentsStmt AlterForeignTableStmt AlterCompositeTypeStmt AlterUserStmt AlterUserMappingStmt AlterUserSetStmt ! AlterRoleStmt AlterRoleSetStmt AlterDefaultPrivilegesStmt DefACLAction AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt --- 231,237 ---- AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterSystemStmt AlterTableStmt AlterTblSpcStmt AlterExtensionStmt AlterExtensionContentsStmt AlterForeignTableStmt AlterCompositeTypeStmt AlterUserStmt AlterUserMappingStmt AlterUserSetStmt ! AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt AlterDefaultPrivilegesStmt DefACLAction AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt *************** static Node *makeRecursiveViewSelect(cha *** 240,249 **** CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertStmt CreateTrigStmt CreateEventTrigStmt ! CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt ! DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt --- 240,249 ---- CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertStmt CreateTrigStmt CreateEventTrigStmt ! CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt ! DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt DropPolicyStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt *************** static Node *makeRecursiveViewSelect(cha *** 269,274 **** --- 269,275 ---- %type alter_table_cmd alter_type_cmd opt_collate_clause replica_identity %type alter_table_cmds alter_type_cmds + %type row_security_cmd %type opt_drop_behavior *************** static Node *makeRecursiveViewSelect(cha *** 319,325 **** %type all_Op MathOp ! %type iso_level opt_encoding %type grantee %type grantee_list %type privilege --- 320,326 ---- %type all_Op MathOp ! %type iso_level opt_encoding row_security_option %type grantee %type grantee_list %type privilege *************** static Node *makeRecursiveViewSelect(cha *** 588,594 **** OBJECT_P OF OFF OFFSET OIDS ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OUT_P OUTER_P OVER OVERLAPS OVERLAY OWNED OWNER ! PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM --- 589,595 ---- OBJECT_P OF OFF OFFSET OIDS ON ONLY OPERATOR OPTION OPTIONS OR ORDER ORDINALITY OUT_P OUTER_P OVER OVERLAPS OVERLAY OWNED OWNER ! PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM *************** stmt : *** 739,744 **** --- 740,746 ---- | AlterGroupStmt | AlterObjectSchemaStmt | AlterOwnerStmt + | AlterPolicyStmt | AlterSeqStmt | AlterSystemStmt | AlterTableStmt *************** stmt : *** 773,778 **** --- 775,781 ---- | CreateOpClassStmt | CreateOpFamilyStmt | AlterOpFamilyStmt + | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt | CreateSeqStmt *************** stmt : *** 798,803 **** --- 801,807 ---- | DropOpClassStmt | DropOpFamilyStmt | DropOwnedStmt + | DropPolicyStmt | DropPLangStmt | DropRuleStmt | DropStmt *************** AlterOptRoleElem: *** 956,961 **** --- 960,969 ---- $$ = makeDefElem("canlogin", (Node *)makeInteger(TRUE)); else if (strcmp($1, "nologin") == 0) $$ = makeDefElem("canlogin", (Node *)makeInteger(FALSE)); + else if (strcmp($1, "bypassrls") == 0) + $$ = makeDefElem("bypassrls", (Node *)makeInteger(TRUE)); + else if (strcmp($1, "nobypassrls") == 0) + $$ = makeDefElem("bypassrls", (Node *)makeInteger(FALSE)); else if (strcmp($1, "noinherit") == 0) { /* *************** set_rest_more: /* Generic SET syntaxes: *** 1472,1477 **** --- 1480,1493 ---- n->args = list_make1(makeStringConst($3, @3)); $$ = n; } + | ROW SECURITY row_security_option + { + VariableSetStmt *n = makeNode(VariableSetStmt); + n->kind = VAR_SET_VALUE; + n->name = "row_security"; + n->args = list_make1(makeStringConst($3, @3)); + $$ = n; + } ; var_name: ColId { $$ = $1; } *************** var_value: opt_boolean_or_string *** 1489,1494 **** --- 1505,1515 ---- { $$ = makeAConst($1, @1); } ; + row_security_option: + ON { $$ = "on"; } + | OFF { $$ = "off"; } + ; + iso_level: READ UNCOMMITTED { $$ = "read uncommitted"; } | READ COMMITTED { $$ = "read committed"; } | REPEATABLE READ { $$ = "repeatable read"; } *************** AlterUserMappingStmt: ALTER USER MAPPING *** 4397,4402 **** --- 4418,4485 ---- /***************************************************************************** * + * QUERIES: + * CREATE POLICY name ON table FOR cmd USING (qual) + * ALTER POLICY name ON table FOR cmd USING (qual) + * DROP POLICY name ON table FOR cmd + * + *****************************************************************************/ + + CreatePolicyStmt: + CREATE POLICY name ON qualified_name FOR row_security_cmd USING '(' a_expr ')' + { + CreatePolicyStmt *n = makeNode(CreatePolicyStmt); + n->policy_name = $3; + n->table = $5; + n->cmd = $7; + n->qual = $10; + $$ = (Node *) n; + } + ; + + AlterPolicyStmt: + ALTER POLICY name ON qualified_name FOR row_security_cmd USING '(' a_expr ')' + { + AlterPolicyStmt *n = makeNode(AlterPolicyStmt); + n->policy_name = $3; + n->table = $5; + n->cmd = $7; + n->qual = $10; + $$ = (Node *) n; + } + ; + + DropPolicyStmt: + DROP POLICY name ON qualified_name FOR row_security_cmd + { + DropPolicyStmt *n = makeNode(DropPolicyStmt); + n->policy_name = $3; + n->table = $5; + n->cmd = $7; + n->missing_ok = FALSE; + $$ = (Node *) n; + } + | DROP POLICY IF_P EXISTS name ON qualified_name FOR row_security_cmd + { + DropPolicyStmt *n = makeNode(DropPolicyStmt); + n->policy_name = $5; + n->table = $7; + n->cmd = $9; + n->missing_ok = TRUE; + $$ = (Node *) n; + } + ; + + row_security_cmd: + ALL { $$ = "all"; } + | SELECT { $$ = "select"; } + | INSERT { $$ = "insert"; } + | UPDATE { $$ = "update"; } + | DELETE_P { $$ = "delete"; } + ; + + /***************************************************************************** + * * QUERIES : * CREATE TRIGGER ... * DROP TRIGGER ... *************** unreserved_keyword: *** 13026,13031 **** --- 13109,13115 ---- | PASSING | PASSWORD | PLANS + | POLICY | PRECEDING | PREPARE | PREPARED diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c new file mode 100644 index c984b7d..bdd2a3d *** a/src/backend/parser/parse_agg.c --- b/src/backend/parser/parse_agg.c *************** transformAggregateCall(ParseState *pstat *** 333,338 **** --- 333,341 ---- case EXPR_KIND_TRIGGER_WHEN: err = _("aggregate functions are not allowed in trigger WHEN conditions"); break; + case EXPR_KIND_ROW_SECURITY: + err = _("aggregate functions are not allowed in row-security policy"); + break; /* * There is intentionally no default: case here, so that the *************** transformWindowFuncCall(ParseState *psta *** 662,667 **** --- 665,673 ---- case EXPR_KIND_TRIGGER_WHEN: err = _("window functions are not allowed in trigger WHEN conditions"); break; + case EXPR_KIND_ROW_SECURITY: + err = _("window functions are not allowed in row-security policy"); + break; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c new file mode 100644 index 4a8aaf6..7848e3b *** a/src/backend/parser/parse_expr.c --- b/src/backend/parser/parse_expr.c *************** transformSubLink(ParseState *pstate, Sub *** 1530,1535 **** --- 1530,1536 ---- case EXPR_KIND_OFFSET: case EXPR_KIND_RETURNING: case EXPR_KIND_VALUES: + case EXPR_KIND_ROW_SECURITY: /* okay */ break; case EXPR_KIND_CHECK_CONSTRAINT: *************** ParseExprKindName(ParseExprKind exprKind *** 2702,2707 **** --- 2703,2710 ---- return "EXECUTE"; case EXPR_KIND_TRIGGER_WHEN: return "WHEN"; + case EXPR_KIND_ROW_SECURITY: + return "ROW SECURITY"; /* * There is intentionally no default: case here, so that the diff --git a/src/backend/rewrite/Makefile b/src/backend/rewrite/Makefile new file mode 100644 index 9ff56c7..25423d3 *** a/src/backend/rewrite/Makefile --- b/src/backend/rewrite/Makefile *************** top_builddir = ../../.. *** 13,18 **** include $(top_builddir)/src/Makefile.global OBJS = rewriteRemove.o rewriteDefine.o \ ! rewriteHandler.o rewriteManip.o rewriteSupport.o include $(top_srcdir)/src/backend/common.mk --- 13,19 ---- include $(top_builddir)/src/Makefile.global OBJS = rewriteRemove.o rewriteDefine.o \ ! rewriteHandler.o rewriteManip.o rewriteSupport.o \ ! rowsecurity.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c new file mode 100644 index e6c5530..19cd36f *** a/src/backend/rewrite/rewriteHandler.c --- b/src/backend/rewrite/rewriteHandler.c *************** *** 25,30 **** --- 25,31 ---- #include "rewrite/rewriteDefine.h" #include "rewrite/rewriteHandler.h" #include "rewrite/rewriteManip.h" + #include "rewrite/rowsecurity.h" #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/rel.h" *************** fireRIRrules(Query *parsetree, List *act *** 1670,1715 **** * Collect the RIR rules that we must apply */ rules = rel->rd_rules; ! if (rules == NULL) ! { ! heap_close(rel, NoLock); ! continue; ! } ! locks = NIL; ! for (i = 0; i < rules->numLocks; i++) { ! rule = rules->rules[i]; ! if (rule->event != CMD_SELECT) ! continue; ! locks = lappend(locks, rule); ! } /* ! * If we found any, apply them --- but first check for recursion! */ ! if (locks != NIL) { ! ListCell *l; ! if (list_member_oid(activeRIRs, RelationGetRelid(rel))) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), ! errmsg("infinite recursion detected in rules for relation \"%s\"", ! RelationGetRelationName(rel)))); activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); ! foreach(l, locks) ! { ! rule = lfirst(l); ! ! parsetree = ApplyRetrieveRule(parsetree, ! rule, ! rt_index, ! rel, ! activeRIRs, ! forUpdatePushedDown); ! } activeRIRs = list_delete_first(activeRIRs); } --- 1671,1733 ---- * Collect the RIR rules that we must apply */ rules = rel->rd_rules; ! if (rules != NULL) { ! locks = NIL; ! for (i = 0; i < rules->numLocks; i++) ! { ! rule = rules->rules[i]; ! if (rule->event != CMD_SELECT) ! continue; ! locks = lappend(locks, rule); ! } ! ! /* ! * If we found any, apply them --- but first check for recursion! ! */ ! if (locks != NIL) ! { ! ListCell *l; ! ! if (list_member_oid(activeRIRs, RelationGetRelid(rel))) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), ! errmsg("infinite recursion detected in rules for relation \"%s\"", ! RelationGetRelationName(rel)))); ! activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); ! ! foreach(l, locks) ! { ! rule = lfirst(l); + parsetree = ApplyRetrieveRule(parsetree, + rule, + rt_index, + rel, + activeRIRs, + forUpdatePushedDown); + } + + activeRIRs = list_delete_first(activeRIRs); + } + } /* ! * If the RTE has row-security quals, apply them and recurse into the ! * securityQuals. */ ! if (prepend_row_security_quals(parsetree, rte, rt_index)) { ! // We applied security quals, check for infinite recursion and ! // then expand any nested queries. if (list_member_oid(activeRIRs, RelationGetRelid(rel))) ! ereport(ERROR, ! (errcode(ERRCODE_INVALID_OBJECT_DEFINITION), ! errmsg("infinite recursion detected in row-security policy for relation \"%s\"", ! RelationGetRelationName(rel)))); activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); ! expression_tree_walker( (Node*) rte->securityQuals, fireRIRonSubLink, (void*)activeRIRs ); activeRIRs = list_delete_first(activeRIRs); } diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c new file mode 100644 index ...b25af1d *** a/src/backend/rewrite/rowsecurity.c --- b/src/backend/rewrite/rowsecurity.c *************** *** 0 **** --- 1,193 ---- + /* + * rewrite/rowsecurity.c + * Routines to support row-security feature + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + */ + #include "postgres.h" + + #include "access/heapam.h" + #include "access/htup_details.h" + #include "access/sysattr.h" + #include "catalog/pg_class.h" + #include "catalog/pg_inherits_fn.h" + #include "catalog/pg_rowsecurity.h" + #include "catalog/pg_type.h" + #include "miscadmin.h" + #include "nodes/makefuncs.h" + #include "nodes/nodeFuncs.h" + #include "nodes/pg_list.h" + #include "nodes/plannodes.h" + #include "parser/parsetree.h" + #include "rewrite/rewriteHandler.h" + #include "rewrite/rewriteManip.h" + #include "rewrite/rowsecurity.h" + #include "utils/acl.h" + #include "utils/lsyscache.h" + #include "utils/rel.h" + #include "utils/syscache.h" + #include "tcop/utility.h" + + /* hook to allow extensions to apply their own security policy */ + row_security_policy_hook_type row_security_policy_hook = NULL; + + /* + * Check the given RTE to see whether it's already had row-security quals + * expanded and, if not, prepend any row-security rules from built-in or + * plug-in sources to the securityQuals. The security quals are rewritten (for + * view expansion, etc) before being added to the RTE. + * + * Returns true if any quals were added. Note that quals may have been found + * but not added if user rights make the user exempt from row security. + */ + bool + prepend_row_security_quals(Query* root, RangeTblEntry* rte, int rt_index) + { + List *rowsecquals; + Relation rel; + Oid userid; + int sec_context; + bool qualsAdded = false; + + GetUserIdAndSecContext(&userid, &sec_context); + + if (rte->relid >= FirstNormalObjectId + && rte->relkind == RELKIND_RELATION + && !(sec_context & SECURITY_ROW_LEVEL_DISABLED)) + { + /* + * Fetch the row-security qual and add it to the list of quals + * to be expanded by expand_security_quals. + */ + rel = heap_open(rte->relid, NoLock); + rowsecquals = pull_row_security_policy(root->commandType, rel); + if (rowsecquals) + { + /* + * Row security quals always have the target table as varno 1, as no + * joins are permitted in row security expressions. We must walk + * the expression, updating any references to varno 1 to the varno + * the table has in the outer query. + * + * We rewrite the expression in-place. + */ + qualsAdded = true; + ChangeVarNodes((Node *) rowsecquals, 1, rt_index, 0); + rte->securityQuals = list_concat(rowsecquals, rte->securityQuals); + } + heap_close(rel, NoLock); + } + + return qualsAdded; + } + + /* + * pull_row_security_policy + * + * Fetches the configured row-security policy of both built-in catalogs and any + * extensions. If any policy is found a list of qualifier expressions is + * returned, where each is treated as a securityQual. + * + * Vars must use varno 1 to refer to the table with row security. + * + * The returned expression trees will be modified in-place, so return copies if + * you're not generating the expression tree each time. + */ + List * + pull_row_security_policy(CmdType cmd, Relation relation) + { + List *quals = NIL; + + /* + * Pull the row-security policy configured with built-in features, + * if unprivileged users. Please note that superuser can bypass it. + */ + if (relation->rsdesc) + { + /* + * If Row Security is enabled, then it is applied to all queries on the + * relation. If Row Security is disabled, then we must check that the + * current user has the privilege to bypass. If the current user does + * not have the ability to bypass, then an error is thrown. + */ + if (is_rls_enabled()) + { + ListCell *item; + RowSecurityPolicy *policy; + + foreach(item, relation->rsdesc->policies) + { + policy = (RowSecurityPolicy *) lfirst(item); + + /* Always add ALL policy if exists. */ + if (policy->rsall != NULL) + quals = lcons(copyObject(policy->rsall->qual), quals); + + /* Build list of quals, merging when appropriate. */ + switch(cmd) + { + case CMD_SELECT: + if (policy->rsselect != NULL) + quals = lcons(copyObject(policy->rsselect->qual), quals); + break; + case CMD_INSERT: + if (policy->rsinsert != NULL) + quals = lcons(copyObject(policy->rsinsert->qual), quals); + break; + case CMD_UPDATE: + if (policy->rsselect != NULL) + quals = lcons(copyObject(policy->rsselect->qual), quals); + if (policy->rsupdate != NULL) + quals = lcons(copyObject(policy->rsupdate->qual), quals); + break; + case CMD_DELETE: + if (policy->rsselect != NULL) + quals = lcons(copyObject(policy->rsselect->qual), quals); + if (policy->rsdelete != NULL) + quals = lcons(copyObject(policy->rsdelete->qual), quals); + break; + default: + elog(ERROR, "unrecognized command type."); + break; + } + } + } + else if (!has_bypassrls_privilege(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("Insufficient privilege to bypass row security."))); + } + + + /* + * Also, ask extensions whether they want to apply their own + * row-security policy. If both built-in and extension has + * their own policy they're applied as nested qualifiers. + */ + if (row_security_policy_hook) + { + List *temp; + + temp = (*row_security_policy_hook)(cmd, relation); + if (temp != NIL) + quals = lcons(temp, quals); + } + + return quals; + } + + /* + * is_rls_enabled - + * determines if row-security is enabled by checking the value of the system + * configuration "row_security". + */ + bool + is_rls_enabled() + { + char const *rls_option; + + rls_option = GetConfigOption("row_security", true, false); + + return (strcmp(rls_option, "on") == 0); + } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c new file mode 100644 index 07e0b98..24a85bb *** a/src/backend/tcop/utility.c --- b/src/backend/tcop/utility.c *************** *** 39,44 **** --- 39,45 ---- #include "commands/extension.h" #include "commands/matview.h" #include "commands/lockcmds.h" + #include "commands/policy.h" #include "commands/portalcmds.h" #include "commands/prepare.h" #include "commands/proclang.h" *************** standard_ProcessUtility(Node *parsetree, *** 837,842 **** --- 838,853 ---- } break; + case T_CreatePolicyStmt: + CreatePolicy((CreatePolicyStmt *) parsetree); + break; + case T_AlterPolicyStmt: + AlterPolicy((AlterPolicyStmt *) parsetree); + break; + case T_DropPolicyStmt: + DropPolicy((DropPolicyStmt *) parsetree); + break; + default: /* All other statement types have event trigger support */ ProcessUtilitySlow(parsetree, queryString, *************** CreateCommandTag(Node *parsetree) *** 2438,2443 **** --- 2449,2464 ---- } break; + case T_CreatePolicyStmt: + tag = "CREATE POLICY"; + break; + case T_AlterPolicyStmt: + tag = "ALTER POLICY"; + break; + case T_DropPolicyStmt: + tag = "DROP POLICY"; + break; + default: elog(WARNING, "unrecognized node type: %d", (int) nodeTag(parsetree)); diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c new file mode 100644 index e4d7b2c..9d32e18 *** a/src/backend/utils/adt/ri_triggers.c --- b/src/backend/utils/adt/ri_triggers.c *************** ri_PlanCheck(const char *querystr, int n *** 2956,2961 **** --- 2956,2962 ---- Relation query_rel; Oid save_userid; int save_sec_context; + int temp_sec_context; /* * Use the query type code to determine whether the query is run against *************** ri_PlanCheck(const char *querystr, int n *** 2968,2975 **** /* Switch to proper UID to perform check as */ GetUserIdAndSecContext(&save_userid, &save_sec_context); SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, ! save_sec_context | SECURITY_LOCAL_USERID_CHANGE); /* Create the plan */ qplan = SPI_prepare(querystr, nargs, argtypes); --- 2969,2986 ---- /* Switch to proper UID to perform check as */ GetUserIdAndSecContext(&save_userid, &save_sec_context); + + /* + * Row-level security should be disabled in the case where a foreign-key + * relation is queried to check existence of tuples that references the + * primary-key being modified. + */ + temp_sec_context = save_sec_context | SECURITY_LOCAL_USERID_CHANGE; + if (qkey->constr_queryno != RI_PLAN_CHECK_LOOKUPPK) + temp_sec_context |= SECURITY_ROW_LEVEL_DISABLED; + SetUserIdAndSecContext(RelationGetForm(query_rel)->relowner, ! temp_sec_context); /* Create the plan */ qplan = SPI_prepare(querystr, nargs, argtypes); diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c new file mode 100644 index e4697f6..48bf9a8 *** a/src/backend/utils/cache/relcache.c --- b/src/backend/utils/cache/relcache.c *************** *** 50,60 **** --- 50,62 ---- #include "catalog/pg_opclass.h" #include "catalog/pg_proc.h" #include "catalog/pg_rewrite.h" + #include "catalog/pg_rowsecurity.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" #include "catalog/schemapg.h" #include "catalog/storage.h" + #include "commands/policy.h" #include "commands/trigger.h" #include "miscadmin.h" #include "optimizer/clauses.h" *************** RelationBuildDesc(Oid targetRelId, bool *** 966,971 **** --- 968,978 ---- else relation->trigdesc = NULL; + if (relation->rd_rel->relhasrowsecurity) + RelationBuildRowSecurity(relation); + else + relation->rsdesc = NULL; + /* * if it's an index, initialize index-related information */ *************** RelationDestroyRelation(Relation relatio *** 1936,1941 **** --- 1943,1950 ---- MemoryContextDelete(relation->rd_indexcxt); if (relation->rd_rulescxt) MemoryContextDelete(relation->rd_rulescxt); + if (relation->rsdesc) + MemoryContextDelete(relation->rsdesc->rscxt); if (relation->rd_fdwroutine) pfree(relation->rd_fdwroutine); pfree(relation); *************** RelationCacheInitializePhase3(void) *** 3334,3339 **** --- 3343,3356 ---- restart = true; } + if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL) + { + RelationBuildRowSecurity(relation); + if (relation->rsdesc == NULL) + relation->rd_rel->relhasrowsecurity = false; + restart = true; + } + /* Release hold on the relation */ RelationDecrementReferenceCount(relation); *************** load_relcache_init_file(bool shared) *** 4702,4707 **** --- 4719,4725 ---- rel->rd_rules = NULL; rel->rd_rulescxt = NULL; rel->trigdesc = NULL; + rel->rsdesc = NULL; rel->rd_indexprs = NIL; rel->rd_indpred = NIL; rel->rd_exclops = NULL; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c new file mode 100644 index 60e4354..67deb32 *** a/src/backend/utils/misc/guc.c --- b/src/backend/utils/misc/guc.c *************** *** 31,36 **** --- 31,37 ---- #include "access/twophase.h" #include "access/xact.h" #include "catalog/namespace.h" + #include "catalog/pg_rowsecurity.h" #include "commands/async.h" #include "commands/prepare.h" #include "commands/vacuum.h" *************** static const struct config_enum_entry hu *** 401,406 **** --- 402,423 ---- }; /* + * Although only "on" and"off" are documented, we accept all likely variants of + * "on" and "off". + */ + static const struct config_enum_entry row_security_options[] = { + {"off", ROW_SECURITY_OFF, false}, + {"on", ROW_SECURITY_ON, false}, + {"true", ROW_SECURITY_ON, true}, + {"false", ROW_SECURITY_OFF, true}, + {"yes", ROW_SECURITY_ON, true}, + {"no", ROW_SECURITY_OFF, true}, + {"1", ROW_SECURITY_ON, true}, + {"0", ROW_SECURITY_OFF, true}, + {NULL, 0, false} + }; + + /* * Options for enum values stored in other modules */ extern const struct config_enum_entry wal_level_options[]; *************** int tcp_keepalives_idle; *** 456,461 **** --- 473,480 ---- int tcp_keepalives_interval; int tcp_keepalives_count; + int row_security; + /* * This really belongs in pg_shmem.c, but is defined here so that it doesn't * need to be duplicated in all the different implementations of pg_shmem.c. *************** static struct config_enum ConfigureNames *** 3384,3390 **** LOGSTMT_NONE, log_statement_options, NULL, NULL, NULL }, ! { {"syslog_facility", PGC_SIGHUP, LOGGING_WHERE, gettext_noop("Sets the syslog \"facility\" to be used when syslog enabled."), --- 3403,3419 ---- LOGSTMT_NONE, log_statement_options, NULL, NULL, NULL }, ! { ! {"row_security", PGC_USERSET, CONN_AUTH_SECURITY, ! gettext_noop("Enable row security."), ! gettext_noop("When enabled, row security will be applied to all " ! "users except those with superuser privileges." ! ) ! }, ! &row_security, ! ROW_SECURITY_ON, row_security_options, ! NULL, NULL, NULL ! }, { {"syslog_facility", PGC_SIGHUP, LOGGING_WHERE, gettext_noop("Sets the syslog \"facility\" to be used when syslog enabled."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample new file mode 100644 index 3f3e706..3c47fd2 *** a/src/backend/utils/misc/postgresql.conf.sample --- b/src/backend/utils/misc/postgresql.conf.sample *************** *** 90,95 **** --- 90,96 ---- #ssl_crl_file = '' # (change requires restart) #password_encryption = on #db_user_namespace = off + #row_security = on # GSSAPI using Kerberos #krb_server_keyfile = '' diff --git a/src/bin/pg_dump/common.c b/src/bin/pg_dump/common.c new file mode 100644 index 94e9147..2f855cf *** a/src/bin/pg_dump/common.c --- b/src/bin/pg_dump/common.c *************** getSchemaData(Archive *fout, int *numTab *** 244,249 **** --- 244,253 ---- write_msg(NULL, "reading rewrite rules\n"); getRules(fout, &numRules); + if (g_verbose) + write_msg(NULL, "reading row-security policies\n"); + getRowSecurity(fout, tblinfo, numTables); + *numTablesPtr = numTables; return tblinfo; } diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c new file mode 100644 index 3aebac8..427e3d9 *** a/src/bin/pg_dump/pg_backup_archiver.c --- b/src/bin/pg_dump/pg_backup_archiver.c *************** _printTocEntry(ArchiveHandle *AH, TocEnt *** 3228,3233 **** --- 3228,3234 ---- strcmp(te->desc, "INDEX") == 0 || strcmp(te->desc, "RULE") == 0 || strcmp(te->desc, "TRIGGER") == 0 || + strcmp(te->desc, "ROW SECURITY") == 0 || strcmp(te->desc, "USER MAPPING") == 0) { /* these object types don't have separate owners */ diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c new file mode 100644 index 5c0f95f..3b42caa *** a/src/bin/pg_dump/pg_dump.c --- b/src/bin/pg_dump/pg_dump.c *************** static int no_security_labels = 0; *** 137,142 **** --- 137,143 ---- static int no_synchronized_snapshots = 0; static int no_unlogged_table_data = 0; static int serializable_deferrable = 0; + static int allow_row_security = 0; static void help(const char *progname); *************** static char *myFormatType(const char *ty *** 247,252 **** --- 248,254 ---- static void getBlobs(Archive *fout); static void dumpBlob(Archive *fout, BlobInfo *binfo); static int dumpBlobs(Archive *fout, void *arg); + static void dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo); static void dumpDatabase(Archive *AH); static void dumpEncoding(Archive *AH); static void dumpStdStrings(Archive *AH); *************** main(int argc, char **argv) *** 340,345 **** --- 342,348 ---- /* * the following options don't have an equivalent short option letter */ + {"allow-row-security", no_argument, &allow_row_security, 1}, {"attribute-inserts", no_argument, &column_inserts, 1}, {"binary-upgrade", no_argument, &binary_upgrade, 1}, {"column-inserts", no_argument, &column_inserts, 1}, *************** help(const char *progname) *** 893,898 **** --- 896,902 ---- printf(_(" -t, --table=TABLE dump the named table(s) only\n")); printf(_(" -T, --exclude-table=TABLE do NOT dump the named table(s)\n")); printf(_(" -x, --no-privileges do not dump privileges (grant/revoke)\n")); + printf(_(" --allow-row-security allow execution of row level security\n")); printf(_(" --binary-upgrade for use by upgrade utilities only\n")); printf(_(" --column-inserts dump data as INSERT commands with column names\n")); printf(_(" --disable-dollar-quoting disable dollar quoting, use SQL standard quoting\n")); *************** setup_connection(Archive *AH, const char *** 1050,1055 **** --- 1054,1064 ---- else AH->sync_snapshot_id = get_synchronized_snapshot(AH); } + + if (allow_row_security) + ExecuteSqlStatement(AH, "SET ROW SECURITY ON"); + else + ExecuteSqlStatement(AH, "SET ROW SECURITY OFF"); } static void *************** dumpBlobs(Archive *fout, void *arg) *** 2756,2761 **** --- 2765,2902 ---- return 1; } + /* + * getRowSecurity + * get information about every row-security policy on a dumpable table. + */ + void + getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables) + { + PQExpBuffer query = createPQExpBuffer(); + PGresult *res; + RowSecurityInfo *rsinfo; + int i_oid; + int i_tableoid; + int i_rsecpolname; + int i_rseccmd; + int i_rsecqual; + int i, j, ntups; + + if (fout->remoteVersion < 90500) + return; + + for (i = 0; i < numTables; i++) + { + TableInfo *tbinfo = &tblinfo[i]; + + if (!tbinfo->hasrowsec || !tbinfo->dobj.dump) + continue; + + if (g_verbose) + write_msg(NULL, "reading row-security policy for table \"%s\"\n", + tbinfo->dobj.name); + + /* + * select table schema to ensure regproc name is qualified if needed + */ + selectSourceSchema(fout, tbinfo->dobj.namespace->dobj.name); + + resetPQExpBuffer(query); + + appendPQExpBuffer(query, + "SELECT oid, tableoid, s.rsecpolname, s.rseccmd, " + "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual " + "FROM pg_catalog.pg_rowsecurity s " + "WHERE rsecrelid = '%u'", + tbinfo->dobj.catId.oid); + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + i_oid = PQfnumber(res, "oid"); + i_tableoid = PQfnumber(res, "tableoid"); + i_rsecpolname = PQfnumber(res, "rsecpolname"); + i_rseccmd = PQfnumber(res, "rseccmd"); + i_rsecqual = PQfnumber(res, "rsecqual"); + + rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo)); + + for (j = 0; j < ntups; j++) + { + char namebuf[NAMEDATALEN + 1]; + + rsinfo[j].dobj.objType = DO_ROW_SECURITY; + rsinfo[j].dobj.catId.tableoid = + atooid(PQgetvalue(res, j, i_tableoid)); + rsinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid)); + AssignDumpId(&rsinfo[j].dobj); + snprintf(namebuf, sizeof(namebuf), "row-security of %s", + tbinfo->rolname); + rsinfo[j].dobj.name = namebuf; + rsinfo[j].dobj.namespace = tbinfo->dobj.namespace; + rsinfo[j].rstable = tbinfo; + rsinfo[j].rsecpolname = pg_strdup(PQgetvalue(res, j, i_rsecpolname)); + rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd)); + rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual)); + } + PQclear(res); + } + destroyPQExpBuffer(query); + } + + /* + * dumpRowSecurity + * dump the definition of the given row-security policy + */ + static void + dumpRowSecurity(Archive *fout, RowSecurityInfo *rsinfo) + { + TableInfo *tbinfo = rsinfo->rstable; + PQExpBuffer query; + PQExpBuffer delqry; + const char *cmd; + + if (dataOnly || !tbinfo->hasrowsec) + return; + + if (strcmp(rsinfo->rseccmd, "a") == 0) + cmd = "ALL"; + else if (strcmp(rsinfo->rseccmd, "s") == 0) + cmd = "SELECT"; + else if (strcmp(rsinfo->rseccmd, "i") == 0) + cmd = "INSERT"; + else if (strcmp(rsinfo->rseccmd, "u") == 0) + cmd = "UPDATE"; + else if (strcmp(rsinfo->rseccmd, "d") == 0) + cmd = "DELETE"; + else + { + write_msg(NULL, "unexpected command type: '%s'\n", rsinfo->rseccmd); + exit_nicely(1); + } + + query = createPQExpBuffer(); + delqry = createPQExpBuffer(); + + appendPQExpBuffer(query, "CREATE POLICY %s ON %s FOR %s USING %s;\n", + rsinfo->rsecpolname, fmtId(tbinfo->dobj.name), + cmd, rsinfo->rsecqual); + appendPQExpBuffer(delqry, "DROP POLICY %s ON %s FOR %s;\n", + rsinfo->rsecpolname, fmtId(tbinfo->dobj.name), cmd); + + ArchiveEntry(fout, rsinfo->dobj.catId, rsinfo->dobj.dumpId, + rsinfo->dobj.name, + rsinfo->dobj.namespace->dobj.name, + NULL, + tbinfo->rolname, false, + "ROW SECURITY", SECTION_POST_DATA, + query->data, delqry->data, NULL, + NULL, 0, + NULL, NULL); + + destroyPQExpBuffer(query); + } + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, *************** getTables(Archive *fout, int *numTables) *** 4286,4291 **** --- 4427,4433 ---- int i_relhastriggers; int i_relhasindex; int i_relhasrules; + int i_relhasrowsec; int i_relhasoids; int i_relfrozenxid; int i_relminmxid; *************** getTables(Archive *fout, int *numTables) *** 4327,4333 **** * we cannot correctly identify inherited columns, owned sequences, etc. */ ! if (fout->remoteVersion >= 90400) { /* * Left join to pick up dependency info linking sequences to their --- 4469,4475 ---- * we cannot correctly identify inherited columns, owned sequences, etc. */ ! if (fout->remoteVersion >= 90500) { /* * Left join to pick up dependency info linking sequences to their *************** getTables(Archive *fout, int *numTables) *** 4339,4344 **** --- 4481,4528 ---- "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "c.relhasrowsecurity, " + "c.relfrozenxid, c.relminmxid, tc.oid AS toid, " + "tc.relfrozenxid AS tfrozenxid, " + "tc.relminmxid AS tminmxid, " + "c.relpersistence, c.relispopulated, " + "c.relreplident, c.relpages, " + "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, " + "d.refobjid AS owning_tab, " + "d.refobjsubid AS owning_col, " + "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " + "array_to_string(array_remove(array_remove(c.reloptions,'check_option=local'),'check_option=cascaded'), ', ') AS reloptions, " + "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " + "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions " + "FROM pg_class c " + "LEFT JOIN pg_depend d ON " + "(c.relkind = '%c' AND " + "d.classid = c.tableoid AND d.objid = c.oid AND " + "d.objsubid = 0 AND " + "d.refclassid = c.tableoid AND d.deptype = 'a') " + "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) " + "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') " + "ORDER BY c.oid", + username_subquery, + RELKIND_SEQUENCE, + RELKIND_RELATION, RELKIND_SEQUENCE, + RELKIND_VIEW, RELKIND_COMPOSITE_TYPE, + RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE); + } + else if (fout->remoteVersion >= 90400) + { + /* + * Left join to pick up dependency info linking sequences to their + * owning column, if any (note this dependency is AUTO as of 8.2) + */ + appendPQExpBuffer(query, + "SELECT c.tableoid, c.oid, c.relname, " + "c.relacl, c.relkind, c.relnamespace, " + "(%s c.relowner) AS rolname, " + "c.relchecks, c.relhastriggers, " + "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, c.relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "tc.relminmxid AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4379,4384 **** --- 4563,4569 ---- "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, c.relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "tc.relminmxid AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4419,4424 **** --- 4604,4610 ---- "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "0 AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4457,4462 **** --- 4643,4649 ---- "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "0 AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4494,4499 **** --- 4681,4687 ---- "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "0 AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4531,4536 **** --- 4719,4725 ---- "(%s c.relowner) AS rolname, " "c.relchecks, (c.reltriggers <> 0) AS relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, 0 AS relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "0 AS tminmxid, " *************** getTables(Archive *fout, int *numTables) *** 4568,4573 **** --- 4757,4763 ---- "(%s relowner) AS rolname, " "relchecks, (reltriggers <> 0) AS relhastriggers, " "relhasindex, relhasrules, relhasoids, " + "'f'::bool AS relhasrowsecurity, " "0 AS relfrozenxid, 0 AS relminmxid," "0 AS toid, " "0 AS tfrozenxid, 0 AS tminmxid," *************** getTables(Archive *fout, int *numTables) *** 4604,4609 **** --- 4794,4800 ---- "(%s relowner) AS rolname, " "relchecks, (reltriggers <> 0) AS relhastriggers, " "relhasindex, relhasrules, relhasoids, " + "'f'::bool AS relhasrowsecurity, " "0 AS relfrozenxid, 0 AS relminmxid," "0 AS toid, " "0 AS tfrozenxid, 0 AS tminmxid," *************** getTables(Archive *fout, int *numTables) *** 4636,4641 **** --- 4827,4833 ---- "(%s relowner) AS rolname, " "relchecks, (reltriggers <> 0) AS relhastriggers, " "relhasindex, relhasrules, relhasoids, " + "'f'::bool AS relhasrowsecurity, " "0 AS relfrozenxid, 0 AS relminmxid," "0 AS toid, " "0 AS tfrozenxid, 0 AS tminmxid," *************** getTables(Archive *fout, int *numTables) *** 4663,4668 **** --- 4855,4861 ---- "relchecks, (reltriggers <> 0) AS relhastriggers, " "relhasindex, relhasrules, " "'t'::bool AS relhasoids, " + "'f'::bool AS relhasrowsecurity, " "0 AS relfrozenxid, 0 AS relminmxid," "0 AS toid, " "0 AS tfrozenxid, 0 AS tminmxid," *************** getTables(Archive *fout, int *numTables) *** 4700,4705 **** --- 4893,4899 ---- "relchecks, (reltriggers <> 0) AS relhastriggers, " "relhasindex, relhasrules, " "'t'::bool AS relhasoids, " + "'f'::bool AS relhasrowsecurity, " "0 AS relfrozenxid, 0 AS relminmxid," "0 AS toid, " "0 AS tfrozenxid, 0 AS tminmxid," *************** getTables(Archive *fout, int *numTables) *** 4747,4752 **** --- 4941,4947 ---- i_relhastriggers = PQfnumber(res, "relhastriggers"); i_relhasindex = PQfnumber(res, "relhasindex"); i_relhasrules = PQfnumber(res, "relhasrules"); + i_relhasrowsec = PQfnumber(res, "relhasrowsecurity"); i_relhasoids = PQfnumber(res, "relhasoids"); i_relfrozenxid = PQfnumber(res, "relfrozenxid"); i_relminmxid = PQfnumber(res, "relminmxid"); *************** getTables(Archive *fout, int *numTables) *** 4798,4803 **** --- 4993,4999 ---- tblinfo[i].hasindex = (strcmp(PQgetvalue(res, i, i_relhasindex), "t") == 0); tblinfo[i].hasrules = (strcmp(PQgetvalue(res, i, i_relhasrules), "t") == 0); tblinfo[i].hastriggers = (strcmp(PQgetvalue(res, i, i_relhastriggers), "t") == 0); + tblinfo[i].hasrowsec = (strcmp(PQgetvalue(res, i, i_relhasrowsec), "t") == 0); tblinfo[i].hasoids = (strcmp(PQgetvalue(res, i, i_relhasoids), "t") == 0); tblinfo[i].relispopulated = (strcmp(PQgetvalue(res, i, i_relispopulated), "t") == 0); tblinfo[i].relreplident = *(PQgetvalue(res, i, i_relreplident)); *************** dumpDumpableObject(Archive *fout, Dumpab *** 7923,7928 **** --- 8119,8127 ---- NULL, 0, dumpBlobs, NULL); break; + case DO_ROW_SECURITY: + dumpRowSecurity(fout, (RowSecurityInfo *) dobj); + break; case DO_PRE_DATA_BOUNDARY: case DO_POST_DATA_BOUNDARY: /* never dumped, nothing to do */ *************** addBoundaryDependencies(DumpableObject * *** 15325,15330 **** --- 15524,15530 ---- case DO_TRIGGER: case DO_EVENT_TRIGGER: case DO_DEFAULT_ACL: + case DO_ROW_SECURITY: /* Post-data objects: must come after the post-data boundary */ addObjectDependency(dobj, postDataBound->dumpId); break; diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h new file mode 100644 index d184187..80f9f0c *** a/src/bin/pg_dump/pg_dump.h --- b/src/bin/pg_dump/pg_dump.h *************** typedef enum *** 111,117 **** DO_PRE_DATA_BOUNDARY, DO_POST_DATA_BOUNDARY, DO_EVENT_TRIGGER, ! DO_REFRESH_MATVIEW } DumpableObjectType; typedef struct _dumpableObject --- 111,118 ---- DO_PRE_DATA_BOUNDARY, DO_POST_DATA_BOUNDARY, DO_EVENT_TRIGGER, ! DO_REFRESH_MATVIEW, ! DO_ROW_SECURITY } DumpableObjectType; typedef struct _dumpableObject *************** typedef struct _tableInfo *** 245,250 **** --- 246,252 ---- bool hasindex; /* does it have any indexes? */ bool hasrules; /* does it have any rules? */ bool hastriggers; /* does it have any triggers? */ + bool hasrowsec; /* does it have any row-security policy? */ bool hasoids; /* does it have OIDs? */ uint32 frozenxid; /* for restore frozen xid */ uint32 minmxid; /* for restore min multi xid */ *************** typedef struct _blobInfo *** 486,491 **** --- 488,502 ---- char *blobacl; } BlobInfo; + typedef struct _rowSecurityInfo + { + DumpableObject dobj; + TableInfo *rstable; + char *rsecpolname; + char *rseccmd; + char *rsecqual; + } RowSecurityInfo; + /* global decls */ extern bool force_quotes; /* double-quotes for identifiers flag */ extern bool g_verbose; /* verbose flag */ *************** extern DefaultACLInfo *getDefaultACLs(Ar *** 577,581 **** --- 588,593 ---- extern void getExtensionMembership(Archive *fout, ExtensionInfo extinfo[], int numExtensions); extern EventTriggerInfo *getEventTriggers(Archive *fout, int *numEventTriggers); + extern void getRowSecurity(Archive *fout, TableInfo tblinfo[], int numTables); #endif /* PG_DUMP_H */ diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c new file mode 100644 index f0caa6b..c0dcae6 *** a/src/bin/pg_dump/pg_dump_sort.c --- b/src/bin/pg_dump/pg_dump_sort.c *************** describeDumpableObject(DumpableObject *o *** 1434,1439 **** --- 1434,1444 ---- "BLOB DATA (ID %d)", obj->dumpId); return; + case DO_ROW_SECURITY: + snprintf(buf, bufsize, + "ROW-SECURITY POLICY (ID %d OID %u)", + obj->dumpId, obj->catId.oid); + return; case DO_PRE_DATA_BOUNDARY: snprintf(buf, bufsize, "PRE-DATA BOUNDARY (ID %d)", diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c new file mode 100644 index 4050091..304da8e *** a/src/bin/pg_dump/pg_dumpall.c --- b/src/bin/pg_dump/pg_dumpall.c *************** dumpRoles(PGconn *conn) *** 663,679 **** i_rolpassword, i_rolvaliduntil, i_rolreplication, i_rolcomment, i_is_current_user; int i; /* note: rolconfig is dumped later */ ! if (server_version >= 90100) printfPQExpBuffer(buf, "SELECT oid, rolname, rolsuper, rolinherit, " "rolcreaterole, rolcreatedb, " "rolcanlogin, rolconnlimit, rolpassword, " "rolvaliduntil, rolreplication, " "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, " "rolname = current_user AS is_current_user " "FROM pg_authid " --- 663,691 ---- i_rolpassword, i_rolvaliduntil, i_rolreplication, + i_rolbypassrls, i_rolcomment, i_is_current_user; int i; /* note: rolconfig is dumped later */ ! if (server_version >= 90500) ! printfPQExpBuffer(buf, ! "SELECT oid, rolname, rolsuper, rolinherit, " ! "rolcreaterole, rolcreatedb, " ! "rolcanlogin, rolconnlimit, rolpassword, " ! "rolvaliduntil, rolreplication, rolbypassrls" ! "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, " ! "rolname = current_user AS is_current_user " ! "FROM pg_authid " ! "ORDER BY 2"); ! else if (server_version >= 90100) printfPQExpBuffer(buf, "SELECT oid, rolname, rolsuper, rolinherit, " "rolcreaterole, rolcreatedb, " "rolcanlogin, rolconnlimit, rolpassword, " "rolvaliduntil, rolreplication, " + "rolsuper as rolbypassrls" "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, " "rolname = current_user AS is_current_user " "FROM pg_authid " *************** dumpRoles(PGconn *conn) *** 684,689 **** --- 696,702 ---- "rolcreaterole, rolcreatedb, " "rolcanlogin, rolconnlimit, rolpassword, " "rolvaliduntil, false as rolreplication, " + "rolsuper as rolbypassrls" "pg_catalog.shobj_description(oid, 'pg_authid') as rolcomment, " "rolname = current_user AS is_current_user " "FROM pg_authid " *************** dumpRoles(PGconn *conn) *** 694,699 **** --- 707,713 ---- "rolcreaterole, rolcreatedb, " "rolcanlogin, rolconnlimit, rolpassword, " "rolvaliduntil, false as rolreplication, " + "rolsuper as rolbypassrls" "null as rolcomment, " "rolname = current_user AS is_current_user " "FROM pg_authid " *************** dumpRoles(PGconn *conn) *** 724,729 **** --- 738,744 ---- "null::text as rolpassword, " "null::abstime as rolvaliduntil, " "false as rolreplication, " + "rolsuper as rolbypassrls, " "null as rolcomment, false " "FROM pg_group " "WHERE NOT EXISTS (SELECT 1 FROM pg_shadow " *************** dumpRoles(PGconn *conn) *** 743,748 **** --- 758,764 ---- i_rolpassword = PQfnumber(res, "rolpassword"); i_rolvaliduntil = PQfnumber(res, "rolvaliduntil"); i_rolreplication = PQfnumber(res, "rolreplication"); + i_rolbypassrls = PQfnumber(res, "rolbypassrls"); i_rolcomment = PQfnumber(res, "rolcomment"); i_is_current_user = PQfnumber(res, "is_current_user"); *************** dumpRoles(PGconn *conn) *** 810,815 **** --- 826,836 ---- else appendPQExpBufferStr(buf, " NOREPLICATION"); + if (strcmp(PQgetvalue(res, i, i_rolbypassrls), "t") == 0) + appendPQExpBufferStr(buf, " BYPASSRLS"); + else + appendPQExpBufferStr(buf, " NOBYPASSRLS"); + if (strcmp(PQgetvalue(res, i, i_rolconnlimit), "-1") != 0) appendPQExpBuffer(buf, " CONNECTION LIMIT %s", PQgetvalue(res, i, i_rolconnlimit)); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c new file mode 100644 index 282cd43..7fad59c *** a/src/bin/psql/describe.c --- b/src/bin/psql/describe.c *************** describeRoles(const char *pattern, bool *** 2529,2534 **** --- 2529,2539 ---- appendPQExpBufferStr(&buf, "\n, r.rolreplication"); } + if (pset.sversion >= 90500) + { + appendPQExpBufferStr(&buf, "\n, r.rolbypassrls"); + } + appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_roles r\n"); processSQLNamePattern(pset.db, &buf, pattern, false, false, *************** listTables(const char *tabtypes, const c *** 2796,2801 **** --- 2801,2811 ---- appendPQExpBuffer(&buf, ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", gettext_noop("Description")); + + if (pset.sversion >= 90500) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"", + gettext_noop("Row-security")); } appendPQExpBufferStr(&buf, *************** listTables(const char *tabtypes, const c *** 2805,2810 **** --- 2815,2823 ---- appendPQExpBufferStr(&buf, "\n LEFT JOIN pg_catalog.pg_index i ON i.indexrelid = c.oid" "\n LEFT JOIN pg_catalog.pg_class c2 ON i.indrelid = c2.oid"); + if (verbose && pset.sversion >= 90500) + appendPQExpBuffer(&buf, + "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid"); appendPQExpBufferStr(&buf, "\nWHERE c.relkind IN ("); if (showTables) diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h new file mode 100644 index 8ed2592..6a4913a *** a/src/include/catalog/dependency.h --- b/src/include/catalog/dependency.h *************** typedef enum ObjectClass *** 147,152 **** --- 147,153 ---- OCLASS_DEFACL, /* pg_default_acl */ OCLASS_EXTENSION, /* pg_extension */ OCLASS_EVENT_TRIGGER, /* pg_event_trigger */ + OCLASS_ROWSECURITY, /* pg_rowsecurity */ MAX_OCLASS /* MUST BE LAST */ } ObjectClass; diff --git a/src/include/catalog/indexing.h b/src/include/catalog/indexing.h new file mode 100644 index 59576fd..1120380 *** a/src/include/catalog/indexing.h --- b/src/include/catalog/indexing.h *************** DECLARE_UNIQUE_INDEX(pg_extension_name_i *** 299,304 **** --- 299,310 ---- DECLARE_UNIQUE_INDEX(pg_range_rngtypid_index, 3542, on pg_range using btree(rngtypid oid_ops)); #define RangeTypidIndexId 3542 + DECLARE_UNIQUE_INDEX(pg_rowsecurity_oid_index, 5001, on pg_rowsecurity using btree(oid oid_ops)); + #define RowSecurityOidIndexId 5001 + + DECLARE_UNIQUE_INDEX(pg_rowsecurity_relid_index, 5002, on pg_rowsecurity using btree(rsecpolname name_ops, rsecrelid oid_ops, rseccmd char_ops)); + #define RowSecurityRelidIndexId 5002 + /* last step of initialization script: build the indexes declared above */ BUILD_INDICES diff --git a/src/include/catalog/pg_authid.h b/src/include/catalog/pg_authid.h new file mode 100644 index e7c32c9..3b63d2b *** a/src/include/catalog/pg_authid.h --- b/src/include/catalog/pg_authid.h *************** CATALOG(pg_authid,1260) BKI_SHARED_RELAT *** 52,57 **** --- 52,58 ---- bool rolcatupdate; /* allowed to alter catalogs manually? */ bool rolcanlogin; /* allowed to log in as session user? */ bool rolreplication; /* role used for streaming replication */ + bool rolbypassrls; /* allowed to bypass row level security? */ int32 rolconnlimit; /* max connections allowed (-1=no limit) */ /* remaining fields may be null; use heap_getattr to read them! */ *************** typedef FormData_pg_authid *Form_pg_auth *** 73,79 **** * compiler constants for pg_authid * ---------------- */ ! #define Natts_pg_authid 11 #define Anum_pg_authid_rolname 1 #define Anum_pg_authid_rolsuper 2 #define Anum_pg_authid_rolinherit 3 --- 74,80 ---- * compiler constants for pg_authid * ---------------- */ ! #define Natts_pg_authid 12 #define Anum_pg_authid_rolname 1 #define Anum_pg_authid_rolsuper 2 #define Anum_pg_authid_rolinherit 3 *************** typedef FormData_pg_authid *Form_pg_auth *** 82,90 **** #define Anum_pg_authid_rolcatupdate 6 #define Anum_pg_authid_rolcanlogin 7 #define Anum_pg_authid_rolreplication 8 ! #define Anum_pg_authid_rolconnlimit 9 ! #define Anum_pg_authid_rolpassword 10 ! #define Anum_pg_authid_rolvaliduntil 11 /* ---------------- * initial contents of pg_authid --- 83,92 ---- #define Anum_pg_authid_rolcatupdate 6 #define Anum_pg_authid_rolcanlogin 7 #define Anum_pg_authid_rolreplication 8 ! #define Anum_pg_authid_rolbypassrls 9 ! #define Anum_pg_authid_rolconnlimit 10 ! #define Anum_pg_authid_rolpassword 11 ! #define Anum_pg_authid_rolvaliduntil 12 /* ---------------- * initial contents of pg_authid *************** typedef FormData_pg_authid *Form_pg_auth *** 93,99 **** * user choices. * ---------------- */ ! DATA(insert OID = 10 ( "POSTGRES" t t t t t t t -1 _null_ _null_ )); #define BOOTSTRAP_SUPERUSERID 10 --- 95,101 ---- * user choices. * ---------------- */ ! DATA(insert OID = 10 ( "POSTGRES" t t t t t t t t -1 _null_ _null_)); #define BOOTSTRAP_SUPERUSERID 10 diff --git a/src/include/catalog/pg_class.h b/src/include/catalog/pg_class.h new file mode 100644 index f2fb317..f635351 *** a/src/include/catalog/pg_class.h --- b/src/include/catalog/pg_class.h *************** CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI *** 65,70 **** --- 65,71 ---- bool relhasrules; /* has (or has had) any rules */ bool relhastriggers; /* has (or has had) any TRIGGERs */ bool relhassubclass; /* has (or has had) derived classes */ + bool relhasrowsecurity; /* has (or has had) row-security policy */ bool relispopulated; /* matview currently holds query results */ char relreplident; /* see REPLICA_IDENTITY_xxx constants */ TransactionId relfrozenxid; /* all Xids < this are frozen in this rel */ *************** typedef FormData_pg_class *Form_pg_class *** 94,100 **** * ---------------- */ ! #define Natts_pg_class 29 #define Anum_pg_class_relname 1 #define Anum_pg_class_relnamespace 2 #define Anum_pg_class_reltype 3 --- 95,101 ---- * ---------------- */ ! #define Natts_pg_class 30 #define Anum_pg_class_relname 1 #define Anum_pg_class_relnamespace 2 #define Anum_pg_class_reltype 3 *************** typedef FormData_pg_class *Form_pg_class *** 118,129 **** #define Anum_pg_class_relhasrules 21 #define Anum_pg_class_relhastriggers 22 #define Anum_pg_class_relhassubclass 23 ! #define Anum_pg_class_relispopulated 24 ! #define Anum_pg_class_relreplident 25 ! #define Anum_pg_class_relfrozenxid 26 ! #define Anum_pg_class_relminmxid 27 ! #define Anum_pg_class_relacl 28 ! #define Anum_pg_class_reloptions 29 /* ---------------- * initial contents of pg_class --- 119,131 ---- #define Anum_pg_class_relhasrules 21 #define Anum_pg_class_relhastriggers 22 #define Anum_pg_class_relhassubclass 23 ! #define Anum_pg_class_relhasrowsecurity 24 ! #define Anum_pg_class_relispopulated 25 ! #define Anum_pg_class_relreplident 26 ! #define Anum_pg_class_relfrozenxid 27 ! #define Anum_pg_class_relminmxid 28 ! #define Anum_pg_class_relacl 29 ! #define Anum_pg_class_reloptions 30 /* ---------------- * initial contents of pg_class *************** typedef FormData_pg_class *Form_pg_class *** 138,150 **** * Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId; * similarly, "1" in relminmxid stands for FirstMultiXactId */ ! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 29 0 t f f f f t n 3 1 _null_ _null_ )); DESCR(""); --- 140,152 ---- * Note: "3" in the relfrozenxid column stands for FirstNormalTransactionId; * similarly, "1" in relminmxid stands for FirstMultiXactId */ ! DATA(insert OID = 1247 ( pg_type PGNSP 71 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1249 ( pg_attribute PGNSP 75 0 PGUID 0 0 0 0 0 0 0 f f p r 21 0 f f f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1255 ( pg_proc PGNSP 81 0 PGUID 0 0 0 0 0 0 0 f f p r 27 0 t f f f f f t n 3 1 _null_ _null_ )); DESCR(""); ! DATA(insert OID = 1259 ( pg_class PGNSP 83 0 PGUID 0 0 0 0 0 0 0 f f p r 30 0 t f f f f f t n 3 1 _null_ _null_ )); DESCR(""); diff --git a/src/include/catalog/pg_rowsecurity.h b/src/include/catalog/pg_rowsecurity.h new file mode 100644 index ...66f993c *** a/src/include/catalog/pg_rowsecurity.h --- b/src/include/catalog/pg_rowsecurity.h *************** *** 0 **** --- 1,91 ---- + /* + * pg_rowsecurity.h + * definition of the system catalog for row-security policy (pg_rowsecurity) + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + */ + #ifndef PG_ROWSECURITY_H + #define PG_ROWSECURITY_H + + #include "catalog/genbki.h" + #include "nodes/primnodes.h" + #include "utils/memutils.h" + #include "utils/relcache.h" + + /* ---------------- + * pg_rowsecurity definition. cpp turns this into + * typedef struct FormData_pg_rowsecurity + * ---------------- + */ + #define RowSecurityRelationId 5000 + + CATALOG(pg_rowsecurity,5000) + { + NameData rsecpolname; /* Policy name. */ + Oid rsecrelid; /* Oid of the relation with policy. */ + char rseccmd; /* One of ROWSECURITY_CMD_* below */ + + #ifdef CATALOG_VARLEN + pg_node_tree rsecqual; /* Policy quals. */ + #endif + } FormData_pg_rowsecurity; + + /* ---------------- + * Form_pg_rowsecurity corresponds to a pointer to a row with + * the format of pg_rowsecurity relation. + * ---------------- + */ + typedef FormData_pg_rowsecurity *Form_pg_rowsecurity; + + /* ---------------- + * compiler constants for pg_rowsecurity + * ---------------- + */ + #define Natts_pg_rowsecurity 4 + #define Anum_pg_rowsecurity_rsecpolname 1 + #define Anum_pg_rowsecurity_rsecrelid 2 + #define Anum_pg_rowsecurity_rseccmd 3 + #define Anum_pg_rowsecurity_rsecqual 4 + + #define ROWSECURITY_CMD_ALL 'a' + #define ROWSECURITY_CMD_SELECT 's' + #define ROWSECURITY_CMD_INSERT 'i' + #define ROWSECURITY_CMD_UPDATE 'u' + #define ROWSECURITY_CMD_DELETE 'd' + + typedef struct + { + Oid rsecid; + Expr *qual; + bool hassublinks; + } RowSecurityEntry; + + typedef struct + { + char *policy_name; + RowSecurityEntry *rsall; /* row-security policy for ALL */ + RowSecurityEntry *rsselect; /* row-security policy for SELECT */ + RowSecurityEntry *rsinsert; /* row-security policy for INSERT */ + RowSecurityEntry *rsupdate; /* row-security policy for UPDATE */ + RowSecurityEntry *rsdelete; /* row-security policy for DELETE */ + } RowSecurityPolicy; + + typedef struct + { + MemoryContext rscxt; /* row-security memory context */ + List *policies; /* list of row-security policies */ + } RowSecurityDesc; + + /* GUC variable */ + extern int row_security; + + /* Possible values for row_security GUC */ + typedef enum + { + ROW_SECURITY_OFF, + ROW_SECURITY_ON + } RowSecurityType; + + #endif /* PG_ROWSECURITY_H */ diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h new file mode 100644 index ...692d608 *** a/src/include/commands/policy.h --- b/src/include/commands/policy.h *************** *** 0 **** --- 1,28 ---- + /*------------------------------------------------------------------------- + * + * policy.h + * prototypes for policy.c. + * + * + * Portions Copyright (c) 1996-2014, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/include/commands/policy.h + * + *------------------------------------------------------------------------- + */ + + #ifndef POLICY_H + #define POLICY_H + + #include "nodes/parsenodes.h" + + extern void RelationBuildRowSecurity(Relation relation); + + extern void RemovePolicyById(Oid policy_id); + + extern Oid CreatePolicy(CreatePolicyStmt *stmt); + extern Oid AlterPolicy(AlterPolicyStmt *stmt); + extern void DropPolicy(DropPolicyStmt *stmt); + + #endif /* POLICY_H */ diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h new file mode 100644 index c2b786e..802c7f9 *** a/src/include/miscadmin.h --- b/src/include/miscadmin.h *************** extern int trace_recovery(int trace_leve *** 272,277 **** --- 272,278 ---- /* flags to be OR'd to form sec_context */ #define SECURITY_LOCAL_USERID_CHANGE 0x0001 #define SECURITY_RESTRICTED_OPERATION 0x0002 + #define SECURITY_ROW_LEVEL_DISABLED 0x0004 extern char *DatabasePath; diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h new file mode 100644 index 067c768..85ac489 *** a/src/include/nodes/nodes.h --- b/src/include/nodes/nodes.h *************** typedef enum NodeTag *** 366,371 **** --- 366,374 ---- T_RefreshMatViewStmt, T_ReplicaIdentityStmt, T_AlterSystemStmt, + T_CreatePolicyStmt, + T_AlterPolicyStmt, + T_DropPolicyStmt, /* * TAGS FOR PARSE TREE NODES (parsenodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h new file mode 100644 index 8364bef..53fa13b *** a/src/include/nodes/parsenodes.h --- b/src/include/nodes/parsenodes.h *************** typedef struct ImportForeignSchemaStmt *** 1854,1859 **** --- 1854,1898 ---- List *options; /* list of options to pass to FDW */ } ImportForeignSchemaStmt; + /*---------------------- + * Create POLICY Statement + *---------------------- + */ + typedef struct CreatePolicyStmt + { + NodeTag type; + char *policy_name; /* Policy's name */ + RangeVar *table; /* the table name the policy applies to */ + char *cmd; /* the command name the policy applies to */ + Node *qual; /* the policy's condition */ + } CreatePolicyStmt; + + /*---------------------- + * Alter POLICY Statement + *---------------------- + */ + typedef struct AlterPolicyStmt + { + NodeTag type; + char *policy_name; /* Policy's name */ + RangeVar *table; /* the table name the policy applies to */ + char *cmd; /* the command name the policy applies to */ + Node *qual; /* the policy's condition */ + } AlterPolicyStmt; + + /*---------------------- + * Drop POLICY Statement + *---------------------- + */ + typedef struct DropPolicyStmt + { + NodeTag type; + char *policy_name; /* Policy's name */ + RangeVar *table; /* the table name the policy applies to */ + char *cmd; /* the command name the policy applies to */ + bool missing_ok; /* skip error if policy is missing. */ + } DropPolicyStmt; + /* ---------------------- * Create TRIGGER Statement * ---------------------- diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h new file mode 100644 index b52e507..4ecd763 *** a/src/include/parser/kwlist.h --- b/src/include/parser/kwlist.h *************** PG_KEYWORD("passing", PASSING, UNRESERVE *** 283,288 **** --- 283,289 ---- PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD) PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD) PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD) + PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD) PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD) PG_KEYWORD("preceding", PRECEDING, UNRESERVED_KEYWORD) PG_KEYWORD("precision", PRECISION, COL_NAME_KEYWORD) diff --git a/src/include/parser/parse_node.h b/src/include/parser/parse_node.h new file mode 100644 index b32ddf7..c6c53b7 *** a/src/include/parser/parse_node.h --- b/src/include/parser/parse_node.h *************** typedef enum ParseExprKind *** 63,69 **** EXPR_KIND_INDEX_PREDICATE, /* index predicate */ EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */ EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */ ! EXPR_KIND_TRIGGER_WHEN /* WHEN condition in CREATE TRIGGER */ } ParseExprKind; --- 63,70 ---- EXPR_KIND_INDEX_PREDICATE, /* index predicate */ EXPR_KIND_ALTER_COL_TRANSFORM, /* transform expr in ALTER COLUMN TYPE */ EXPR_KIND_EXECUTE_PARAMETER, /* parameter value in EXECUTE */ ! EXPR_KIND_TRIGGER_WHEN, /* WHEN condition in CREATE TRIGGER */ ! EXPR_KIND_ROW_SECURITY /* ROW SECURITY policy for a table */ } ParseExprKind; diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h new file mode 100644 index ...96d474f *** a/src/include/rewrite/rowsecurity.h --- b/src/include/rewrite/rowsecurity.h *************** *** 0 **** --- 1,31 ---- + /* ------------------------------------------------------------------------- + * + * rowsecurity.h + * prototypes for optimizer/rowsecurity.c + * + * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * ------------------------------------------------------------------------- + */ + #ifndef ROWSECURITY_H + #define ROWSECURITY_H + + #include "nodes/execnodes.h" + #include "nodes/parsenodes.h" + #include "nodes/relation.h" + #include "utils/rel.h" + + typedef List *(*row_security_policy_hook_type)(CmdType cmdtype, + Relation relation); + + extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook; + + extern List *pull_row_security_policy(CmdType cmd, Relation relation); + + extern bool prepend_row_security_quals(Query* root, RangeTblEntry* rte, + int rt_index); + + extern bool is_rls_enabled(void); + + #endif /* ROWSECURITY_H */ \ No newline at end of file diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h new file mode 100644 index 9430baa..45c1dc7 *** a/src/include/utils/acl.h --- b/src/include/utils/acl.h *************** extern bool pg_foreign_server_ownercheck *** 326,330 **** --- 326,331 ---- extern bool pg_event_trigger_ownercheck(Oid et_oid, Oid roleid); extern bool pg_extension_ownercheck(Oid ext_oid, Oid roleid); extern bool has_createrole_privilege(Oid roleid); + extern bool has_bypassrls_privilege(Oid roleid); #endif /* ACL_H */ diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h new file mode 100644 index 37b6cbb..8b96218 *** a/src/include/utils/rel.h --- b/src/include/utils/rel.h *************** *** 18,23 **** --- 18,24 ---- #include "catalog/pg_am.h" #include "catalog/pg_class.h" #include "catalog/pg_index.h" + #include "catalog/pg_rowsecurity.h" #include "fmgr.h" #include "nodes/bitmapset.h" #include "rewrite/prs2lock.h" *************** typedef struct RelationData *** 105,110 **** --- 106,112 ---- RuleLock *rd_rules; /* rewrite rules */ MemoryContext rd_rulescxt; /* private memory cxt for rd_rules, if any */ TriggerDesc *trigdesc; /* Trigger info, or NULL if rel has none */ + RowSecurityDesc *rsdesc; /* Row-security policy, or NULL */ /* data managed by RelationGetIndexList: */ List *rd_indexlist; /* list of OIDs of indexes on relation */ diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out new file mode 100644 index ...f419a75 *** a/src/test/regress/expected/rowsecurity.out --- b/src/test/regress/expected/rowsecurity.out *************** *** 0 **** --- 1,1116 ---- + -- + -- Test of Row-level security feature + -- + -- Clean up in case a prior regression run failed + -- Suppress NOTICE messages when users/groups don't exist + SET client_min_messages TO 'warning'; + DROP USER IF EXISTS rls_regress_user0; + DROP USER IF EXISTS rls_regress_user1; + DROP USER IF EXISTS rls_regress_user2; + DROP USER IF EXISTS rls_regress_exempt_user; + DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; + RESET client_min_messages; + -- initial setup + CREATE USER rls_regress_user0; + CREATE USER rls_regress_user1; + CREATE USER rls_regress_user2; + CREATE USER rls_regress_exempt_user BYPASSRLS; + CREATE SCHEMA rls_regress_schema; + GRANT ALL ON SCHEMA rls_regress_schema to public; + SET search_path = rls_regress_schema; + -- setup of malicious function + CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; + GRANT EXECUTE ON FUNCTION f_leak(text) TO public; + -- BASIC Row-Level Security Scenario + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE uaccount ( + pguser name primary key, + seclv int + ); + GRANT SELECT ON uaccount TO public; + INSERT INTO uaccount VALUES + ('rls_regress_user0', 99), + ('rls_regress_user1', 1), + ('rls_regress_user2', 2), + ('rls_regress_user3', 3); + CREATE TABLE category ( + cid int primary key, + cname text + ); + GRANT ALL ON category TO public; + INSERT INTO category VALUES + (11, 'novel'), + (22, 'science fiction'), + (33, 'technology'), + (44, 'manga'); + CREATE TABLE document ( + did int primary key, + cid int references category(cid), + dlevel int not null, + dauthor name, + dtitle text + ); + GRANT ALL ON document TO public; + INSERT INTO document VALUES + ( 1, 11, 1, 'rls_regress_user1', 'my first novel'), + ( 2, 11, 2, 'rls_regress_user1', 'my second novel'), + ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'), + ( 4, 44, 1, 'rls_regress_user1', 'my first manga'), + ( 5, 44, 2, 'rls_regress_user1', 'my second manga'), + ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'), + ( 7, 33, 2, 'rls_regress_user2', 'great technology book'), + ( 8, 44, 1, 'rls_regress_user2', 'great manga'); + -- user's security level must be higher that or equal to document's + CREATE POLICY p1 ON document FOR ALL + USING (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); + -- viewpoint from rls_regress_user1 + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my first manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 4 | 44 | 1 | rls_regress_user1 | my first manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 8 | 44 | 1 | rls_regress_user2 | great manga + (4 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my first manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + (4 rows) + + -- viewpoint from rls_regress_user2 + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (8 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 11 | 2 | 2 | rls_regress_user1 | my second novel | novel + 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + 44 | 5 | 2 | rls_regress_user1 | my second manga | manga + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 33 | 7 | 2 | rls_regress_user2 | great technology book | technology + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + (8 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) + (7 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------------------------- + Hash Join + Hash Cond: (category.cid = document.cid) + -> Seq Scan on category + -> Hash + -> Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dlevel <= $0) + InitPlan 1 (returns $0) + -> Index Scan using uaccount_pkey on uaccount + Index Cond: (pguser = "current_user"()) + (11 rows) + + -- only owner can change row-level security + ALTER POLICY p1 ON document FOR ALL USING (true); --fail + ERROR: must be owner of relation document + DROP POLICY p1 ON document FOR ALL; --fail + ERROR: must be owner of relation document + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON document FOR ALL USING (dauthor = current_user); + -- viewpoint from rls_regress_user1 again + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+-------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + (5 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; + NOTICE: f_leak => my first novel + NOTICE: f_leak => my second novel + NOTICE: f_leak => my science fiction + NOTICE: f_leak => my first manga + NOTICE: f_leak => my second manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+--------------------+----------------- + 11 | 1 | 1 | rls_regress_user1 | my first novel | novel + 11 | 2 | 2 | rls_regress_user1 | my second novel | novel + 22 | 3 | 2 | rls_regress_user1 | my science fiction | science fiction + 44 | 4 | 1 | rls_regress_user1 | my first manga | manga + 44 | 5 | 2 | rls_regress_user1 | my second manga | manga + (5 rows) + + -- viewpoint from rls_regres_user2 again + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (3 rows) + + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; + NOTICE: f_leak => great science fiction + NOTICE: f_leak => great technology book + NOTICE: f_leak => great manga + cid | did | dlevel | dauthor | dtitle | cname + -----+-----+--------+-------------------+-----------------------+----------------- + 22 | 6 | 1 | rls_regress_user2 | great science fiction | science fiction + 33 | 7 | 2 | rls_regress_user2 | great technology book | technology + 44 | 8 | 1 | rls_regress_user2 | great manga | manga + (3 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------- + Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dauthor = "current_user"()) + (4 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + QUERY PLAN + ---------------------------------------------------- + Nested Loop + -> Subquery Scan on document + Filter: f_leak(document.dtitle) + -> Seq Scan on document document_1 + Filter: (dauthor = "current_user"()) + -> Index Scan using category_pkey on category + Index Cond: (cid = document.cid) + (7 rows) + + -- interaction of FK/PK constraints + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE POLICY p2 ON category FOR ALL + USING (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33) + WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44) + ELSE false END); + -- cannot delete PK referenced by invisible FK + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; + did | cid | dlevel | dauthor | dtitle | cid | cname + -----+-----+--------+-------------------+--------------------+-----+------------ + 2 | 11 | 2 | rls_regress_user1 | my second novel | 11 | novel + 1 | 11 | 1 | rls_regress_user1 | my first novel | 11 | novel + | | | | | 33 | technology + 5 | 44 | 2 | rls_regress_user1 | my second manga | | + 4 | 44 | 1 | rls_regress_user1 | my first manga | | + 3 | 22 | 2 | rls_regress_user1 | my science fiction | | + (6 rows) + + DELETE FROM category WHERE cid = 33; -- fails with FK violation + ERROR: update or delete on table "category" violates foreign key constraint "document_cid_fkey" on table "document" + DETAIL: Key (cid)=(33) is still referenced from table "document". + -- cannot insert FK referencing invisible PK + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; + did | cid | dlevel | dauthor | dtitle | cid | cname + -----+-----+--------+-------------------+-----------------------+-----+----------------- + 6 | 22 | 1 | rls_regress_user2 | great science fiction | 22 | science fiction + 8 | 44 | 1 | rls_regress_user2 | great manga | 44 | manga + 7 | 33 | 2 | rls_regress_user2 | great technology book | | + (3 rows) + + INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- fail wil FK violation + ERROR: insert or update on table "document" violates foreign key constraint "document_cid_fkey" + DETAIL: Key (cid)=(33) is not present in table "category". + -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row + SET SESSION AUTHORIZATION rls_regress_user1; + INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see + ERROR: duplicate key value violates unique constraint "document_pkey" + DETAIL: Key (did)=(8) already exists. + SELECT * FROM document WHERE did = 8; -- and confirm we can't see it + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+---------+-------- + (0 rows) + + -- database superuser cannot bypass RLS policy when enabled + RESET SESSION AUTHORIZATION; + SET row_security TO ON; + SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+---------+-------- + (0 rows) + + SELECT * FROM category; + cid | cname + -----+------- + (0 rows) + + -- database superuser can bypass RLS policy when disabled + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (8 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- database non-superuser with bypass privilege can bypass RLS policy when disabled + SET SESSION AUTHORIZATION rls_regress_exempt_user; + SET row_security TO OFF; + SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+-------------------+----------------------- + 1 | 11 | 1 | rls_regress_user1 | my first novel + 2 | 11 | 2 | rls_regress_user1 | my second novel + 3 | 22 | 2 | rls_regress_user1 | my science fiction + 4 | 44 | 1 | rls_regress_user1 | my first manga + 5 | 44 | 2 | rls_regress_user1 | my second manga + 6 | 22 | 1 | rls_regress_user2 | great science fiction + 7 | 33 | 2 | rls_regress_user2 | great technology book + 8 | 44 | 1 | rls_regress_user2 | great manga + (8 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- + -- Table inheritance and RLS policy + -- + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + CREATE TABLE t1 (a int, junk1 text, b text) WITH OIDS; + ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor + GRANT ALL ON t1 TO public; + COPY t1 FROM stdin WITH (oids); + CREATE TABLE t2 (c float) INHERITS (t1); + COPY t2 FROM stdin WITH (oids); + CREATE TABLE t3 (c text, b text, a int) WITH OIDS; + ALTER TABLE t3 INHERIT t1; + COPY t3(a,b,c) FROM stdin WITH (oids); + CREATE POLICY p1 ON t1 FOR ALL USING (a % 2 = 0); -- be even number + CREATE POLICY p2 ON t2 FOR ALL USING (a % 2 = 1); -- be odd number + SELECT * FROM t1; + a | b + ---+----- + 2 | bbb + 4 | ddd + 2 | bcd + 4 | def + 2 | yyy + (5 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN + ------------------------------- + Append + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (7 rows) + + SELECT * FROM t1 WHERE f_leak(b); + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + NOTICE: f_leak => bcd + NOTICE: f_leak => def + NOTICE: f_leak => yyy + a | b + ---+----- + 2 | bbb + 4 | ddd + 2 | bcd + 4 | def + 2 | yyy + (5 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Subquery Scan on t1 + Filter: f_leak(t1.b) + -> Append + -> Seq Scan on t1 t1_1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (9 rows) + + -- reference to system column + SELECT oid, * FROM t1; + oid | a | b + -----+---+----- + 102 | 2 | bbb + 104 | 4 | ddd + 202 | 2 | bcd + 204 | 4 | def + 302 | 2 | yyy + (5 rows) + + EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; + QUERY PLAN + ------------------------------- + Append + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (7 rows) + + -- reference to whole-row reference + SELECT *, t1 FROM t1; + a | b | t1 + ---+-----+--------- + 2 | bbb | (2,bbb) + 4 | ddd | (4,ddd) + 2 | bcd | (2,bcd) + 4 | def | (4,def) + 2 | yyy | (2,yyy) + (5 rows) + + EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; + QUERY PLAN + ------------------------------- + Append + -> Seq Scan on t1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (7 rows) + + -- for share/update lock + SELECT * FROM t1 FOR SHARE; + a | b + ---+----- + 2 | bbb + 4 | ddd + 2 | bcd + 4 | def + 2 | yyy + (5 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 FOR SHARE; + QUERY PLAN + ------------------------------------------------------- + LockRows + -> Subquery Scan on t1 + -> LockRows + -> Result + -> Append + -> Seq Scan on t1 t1_1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (11 rows) + + SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + NOTICE: f_leak => bcd + NOTICE: f_leak => def + NOTICE: f_leak => yyy + a | b + ---+----- + 2 | bbb + 4 | ddd + 2 | bcd + 4 | def + 2 | yyy + (5 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; + QUERY PLAN + ------------------------------------------------------- + LockRows + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> LockRows + -> Result + -> Append + -> Seq Scan on t1 t1_1 + Filter: ((a % 2) = 0) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (12 rows) + + -- superuser is allowed to bypass RLS checks + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + NOTICE: f_leak => aaa + NOTICE: f_leak => bbb + NOTICE: f_leak => ccc + NOTICE: f_leak => ddd + NOTICE: f_leak => abc + NOTICE: f_leak => bcd + NOTICE: f_leak => cde + NOTICE: f_leak => def + NOTICE: f_leak => xxx + NOTICE: f_leak => yyy + NOTICE: f_leak => zzz + a | b + ---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd + 1 | abc + 2 | bcd + 3 | cde + 4 | def + 1 | xxx + 2 | yyy + 3 | zzz + (11 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + QUERY PLAN + --------------------------- + Append + -> Seq Scan on t1 + Filter: f_leak(b) + -> Seq Scan on t2 + Filter: f_leak(b) + -> Seq Scan on t3 + Filter: f_leak(b) + (7 rows) + + -- non-superuser with bypass privilege can bypass RLS policy when disabled + SET SESSION AUTHORIZATION rls_regress_exempt_user; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + NOTICE: f_leak => aaa + NOTICE: f_leak => bbb + NOTICE: f_leak => ccc + NOTICE: f_leak => ddd + NOTICE: f_leak => abc + NOTICE: f_leak => bcd + NOTICE: f_leak => cde + NOTICE: f_leak => def + NOTICE: f_leak => xxx + NOTICE: f_leak => yyy + NOTICE: f_leak => zzz + a | b + ---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd + 1 | abc + 2 | bcd + 3 | cde + 4 | def + 1 | xxx + 2 | yyy + 3 | zzz + (11 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + QUERY PLAN + --------------------------- + Append + -> Seq Scan on t1 + Filter: f_leak(b) + -> Seq Scan on t2 + Filter: f_leak(b) + -> Seq Scan on t3 + Filter: f_leak(b) + (7 rows) + + ----- Dependencies ----- + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + CREATE TABLE dependee (x integer, y integer); + CREATE TABLE dependent (x integer, y integer); + CREATE POLICY d1 ON dependent FOR ALL USING (x = (SELECT d.x FROM dependee d WHERE d.y = y)); + DROP TABLE dependee; -- Should fail without CASCADE due to dependency on row-security qual? + ERROR: cannot drop table dependee because other objects depend on it + DETAIL: row-security of table dependent FOR ALL depends on table dependee + HINT: Use DROP ... CASCADE to drop the dependent objects too. + DROP TABLE dependee CASCADE; + NOTICE: drop cascades to row-security of table dependent FOR ALL + EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualified + QUERY PLAN + ----------------------- + Seq Scan on dependent + (1 row) + + ----- RECURSION ---- + -- + -- Simple recursion + -- + CREATE TABLE rec1 (x integer, y integer); + CREATE POLICY r1 ON rec1 FOR ALL + USING (x = (SELECT r.x FROM rec1 r WHERE y = r.y)); + SELECT * FROM rec1; -- fail, direct recursion + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- Mutual recursion + -- + CREATE TABLE rec2 (a integer, b integer); + ALTER POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2 WHERE b = y)); + CREATE POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1 WHERE y = b)); + SELECT * FROM rec1; -- fail, mutual recursion + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- Mutual recursion via views + -- + CREATE VIEW rec1v AS SELECT * FROM rec1; + CREATE VIEW rec2v AS SELECT * FROM rec2; + ALTER POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2v WHERE b = y)); + ALTER POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1v WHERE y = b)); + SELECT * FROM rec1; -- fail, mutual recursion via views + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- Mutual recursion via .s.b views + -- + DROP VIEW rec1v, rec2v CASCADE; + NOTICE: drop cascades to 2 other objects + DETAIL: drop cascades to row-security of table rec1 FOR ALL + drop cascades to row-security of table rec2 FOR ALL + CREATE VIEW rec1v WITH (security_barrier) AS SELECT * FROM rec1; + CREATE VIEW rec2v WITH (security_barrier) AS SELECT * FROM rec2; + CREATE POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2v WHERE b = y)); + CREATE POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1v WHERE y = b)); + SELECT * FROM rec1; -- fail, mutual recursion via s.b. views + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- recursive RLS and VIEWs in policy + -- + CREATE TABLE s1 (a int, b text); + INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); + CREATE TABLE s2 (x int, y text); + INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x); + CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; + CREATE POLICY p1 ON s1 FOR ALL + USING (a in (select x from s2 where y like '%2f%')); + CREATE POLICY p2 ON s2 FOR ALL + USING (x in (select a from s1 where b like '%22%')); + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) + ERROR: infinite recursion detected in row-security policy for relation "s1" + ALTER POLICY p2 ON s2 FOR ALL USING (x % 2 = 0); + SELECT * FROM s1 WHERE f_leak(b); -- OK + NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c + NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c + a | b + ---+---------------------------------- + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + (2 rows) + + EXPLAIN SELECT * FROM only s1 WHERE f_leak(b); + QUERY PLAN + --------------------------------------------------------------------------------------- + Subquery Scan on s1 (cost=28.55..61.67 rows=205 width=36) + Filter: f_leak(s1.b) + -> Hash Join (cost=28.55..55.52 rows=615 width=36) + Hash Cond: (s1_1.a = s2.x) + -> Seq Scan on s1 s1_1 (cost=0.00..22.30 rows=1230 width=36) + -> Hash (cost=28.54..28.54 rows=1 width=4) + -> HashAggregate (cost=28.53..28.54 rows=1 width=4) + Group Key: s2.x + -> Subquery Scan on s2 (cost=0.00..28.52 rows=1 width=4) + Filter: (s2.y ~~ '%2f%'::text) + -> Seq Scan on s2 s2_1 (cost=0.00..28.45 rows=6 width=36) + Filter: ((x % 2) = 0) + (12 rows) + + ALTER POLICY p1 ON s1 FOR ALL + USING (a in (select x from v2)); -- using VIEW in RLS policy + SELECT * FROM s1 WHERE f_leak(b); -- OK + NOTICE: f_leak => 0267aaf632e87a63288a08331f22c7c3 + NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc + a | b + ----+---------------------------------- + -4 | 0267aaf632e87a63288a08331f22c7c3 + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + (2 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b); + QUERY PLAN + ---------------------------------------------------------- + Subquery Scan on s1 + Filter: f_leak(s1.b) + -> Hash Join + Hash Cond: (s1_1.a = s2.x) + -> Seq Scan on s1 s1_1 + -> Hash + -> HashAggregate + Group Key: s2.x + -> Subquery Scan on s2 + Filter: (s2.y ~~ '%af%'::text) + -> Seq Scan on s2 s2_1 + Filter: ((x % 2) = 0) + (12 rows) + + SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + xx | x | y + ----+----+---------------------------------- + -6 | -6 | 596a3d04481816330f07e4f97510c28f + -4 | -4 | 0267aaf632e87a63288a08331f22c7c3 + 2 | 2 | c81e728d9d4c2f636f067f89cc14862c + (3 rows) + + EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + QUERY PLAN + -------------------------------------------------------------------- + Subquery Scan on s2 + Filter: (s2.y ~~ '%28%'::text) + -> Seq Scan on s2 s2_1 + Filter: ((x % 2) = 0) + SubPlan 1 + -> Limit + -> Subquery Scan on s1 + -> Nested Loop Semi Join + Join Filter: (s1_1.a = s2_2.x) + -> Seq Scan on s1 s1_1 + -> Materialize + -> Subquery Scan on s2_2 + Filter: (s2_2.y ~~ '%af%'::text) + -> Seq Scan on s2 s2_3 + Filter: ((x % 2) = 0) + (15 rows) + + ALTER POLICY p2 ON s2 FOR ALL + USING (x in (select a from s1 where b like '%d2%')); + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view) + ERROR: infinite recursion detected in row-security policy for relation "s1" + -- prepared statement with rls_regress_user0 privilege + PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1; + EXECUTE p1(2); + a | b + ---+----- + 2 | bbb + 2 | bcd + 2 | yyy + (3 rows) + + EXPLAIN (COSTS OFF) EXECUTE p1(2); + QUERY PLAN + ---------------------------------------------- + Append + -> Seq Scan on t1 + Filter: ((a <= 2) AND ((a % 2) = 0)) + -> Seq Scan on t2 + Filter: ((a <= 2) AND ((a % 2) = 0)) + -> Seq Scan on t3 + Filter: ((a <= 2) AND ((a % 2) = 0)) + (7 rows) + + -- superuser is allowed to bypass RLS checks + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + NOTICE: f_leak => aaa + NOTICE: f_leak => bbb + NOTICE: f_leak => ccc + NOTICE: f_leak => ddd + NOTICE: f_leak => abc + NOTICE: f_leak => bcd + NOTICE: f_leak => cde + NOTICE: f_leak => def + NOTICE: f_leak => xxx + NOTICE: f_leak => yyy + NOTICE: f_leak => zzz + a | b + ---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd + 1 | abc + 2 | bcd + 3 | cde + 4 | def + 1 | xxx + 2 | yyy + 3 | zzz + (11 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + QUERY PLAN + --------------------------- + Append + -> Seq Scan on t1 + Filter: f_leak(b) + -> Seq Scan on t2 + Filter: f_leak(b) + -> Seq Scan on t3 + Filter: f_leak(b) + (7 rows) + + -- plan cache should be invalidated + EXECUTE p1(2); + a | b + ---+----- + 1 | aaa + 2 | bbb + 1 | abc + 2 | bcd + 1 | xxx + 2 | yyy + (6 rows) + + EXPLAIN (COSTS OFF) EXECUTE p1(2); + QUERY PLAN + -------------------------- + Append + -> Seq Scan on t1 + Filter: (a <= 2) + -> Seq Scan on t2 + Filter: (a <= 2) + -> Seq Scan on t3 + Filter: (a <= 2) + (7 rows) + + PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1; + EXECUTE p2(2); + a | b + ---+----- + 2 | bbb + 2 | bcd + 2 | yyy + (3 rows) + + EXPLAIN (COSTS OFF) EXECUTE p2(2); + QUERY PLAN + ------------------------- + Append + -> Seq Scan on t1 + Filter: (a = 2) + -> Seq Scan on t2 + Filter: (a = 2) + -> Seq Scan on t3 + Filter: (a = 2) + (7 rows) + + -- also, case when privilege switch from superuser + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + EXECUTE p2(2); + a | b + ---+----- + 2 | bbb + 2 | bcd + 2 | yyy + (3 rows) + + EXPLAIN (COSTS OFF) EXECUTE p2(2); + QUERY PLAN + --------------------------------------------- + Append + -> Seq Scan on t1 + Filter: ((a = 2) AND ((a % 2) = 0)) + -> Seq Scan on t2 + Filter: ((a = 2) AND ((a % 2) = 0)) + -> Seq Scan on t3 + Filter: ((a = 2) AND ((a % 2) = 0)) + (7 rows) + + -- + -- UPDATE / DELETE and Row-level security + -- + SET SESSION AUTHORIZATION rls_regress_user0; + EXPLAIN (COSTS OFF) UPDATE t1 SET b = b || b WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Update on t1 t1_3 + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> Seq Scan on t1 t1_4 + Filter: ((a % 2) = 0) + -> Subquery Scan on t1_1 + Filter: f_leak(t1_1.b) + -> Seq Scan on t2 + Filter: ((a % 2) = 0) + -> Subquery Scan on t1_2 + Filter: f_leak(t1_2.b) + -> Seq Scan on t3 + Filter: ((a % 2) = 0) + (13 rows) + + UPDATE t1 SET b = b || b WHERE f_leak(b); + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + NOTICE: f_leak => abc + NOTICE: f_leak => cde + NOTICE: f_leak => xxx + NOTICE: f_leak => yyy + NOTICE: f_leak => zzz + EXPLAIN (COSTS OFF) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Update on t1 t1_1 + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> Seq Scan on t1 t1_2 + Filter: ((a % 2) = 0) + (5 rows) + + UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); + NOTICE: f_leak => bbbbbb + NOTICE: f_leak => dddddd + -- returning clause with system column + UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; + NOTICE: f_leak => bbbbbb_updt + NOTICE: f_leak => dddddd_updt + oid | a | b | t1 + -----+---+-------------+----------------- + 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) + 104 | 4 | dddddd_updt | (4,dddddd_updt) + (2 rows) + + UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *; + NOTICE: f_leak => bbbbbb_updt + NOTICE: f_leak => dddddd_updt + NOTICE: f_leak => abcabc + NOTICE: f_leak => cdecde + NOTICE: f_leak => xxxxxx + NOTICE: f_leak => yyyyyy + NOTICE: f_leak => zzzzzz + a | b + ---+------------- + 2 | bbbbbb_updt + 4 | dddddd_updt + 1 | abcabc + 3 | cdecde + 1 | xxxxxx + 2 | yyyyyy + 3 | zzzzzz + (7 rows) + + UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; + NOTICE: f_leak => bbbbbb_updt + NOTICE: f_leak => dddddd_updt + NOTICE: f_leak => abcabc + NOTICE: f_leak => cdecde + NOTICE: f_leak => xxxxxx + NOTICE: f_leak => yyyyyy + NOTICE: f_leak => zzzzzz + oid | a | b | t1 + -----+---+-------------+----------------- + 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) + 104 | 4 | dddddd_updt | (4,dddddd_updt) + 201 | 1 | abcabc | (1,abcabc) + 203 | 3 | cdecde | (3,cdecde) + 301 | 1 | xxxxxx | (1,xxxxxx) + 302 | 2 | yyyyyy | (2,yyyyyy) + 303 | 3 | zzzzzz | (3,zzzzzz) + (7 rows) + + RESET SESSION AUTHORIZATION; + SELECT * FROM t1; + a | b + ---+------------- + 1 | aaa + 3 | ccc + 2 | bbbbbb_updt + 4 | dddddd_updt + 2 | bcd + 4 | def + 1 | abcabc + 3 | cdecde + 1 | xxxxxx + 2 | yyyyyy + 3 | zzzzzz + (11 rows) + + SET SESSION AUTHORIZATION rls_regress_user0; + EXPLAIN (costs off) DELETE FROM only t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Delete on t1 + -> Subquery Scan on t1_1 + Filter: f_leak(t1_1.b) + -> Seq Scan on t1 t1_2 + Filter: ((a % 2) = 0) + (5 rows) + + EXPLAIN (costs off) DELETE FROM t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Delete on t1 + -> Subquery Scan on t1_1 + Filter: f_leak(t1_1.b) + -> Seq Scan on t1 t1_2 + Filter: ((a % 2) = 0) + -> Subquery Scan on t2 + Filter: f_leak(t2.b) + -> Seq Scan on t2 t2_1 + Filter: ((a % 2) = 1) + -> Seq Scan on t3 + Filter: f_leak(b) + (11 rows) + + DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1; + NOTICE: f_leak => bbbbbb_updt + NOTICE: f_leak => dddddd_updt + oid | a | b | t1 + -----+---+-------------+----------------- + 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) + 104 | 4 | dddddd_updt | (4,dddddd_updt) + (2 rows) + + DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1; + NOTICE: f_leak => abcabc + NOTICE: f_leak => cdecde + NOTICE: f_leak => xxxxxx + NOTICE: f_leak => yyyyyy + NOTICE: f_leak => zzzzzz + oid | a | b | t1 + -----+---+--------+------------ + 201 | 1 | abcabc | (1,abcabc) + 203 | 3 | cdecde | (3,cdecde) + 301 | 1 | xxxxxx | (1,xxxxxx) + 302 | 2 | yyyyyy | (2,yyyyyy) + 303 | 3 | zzzzzz | (3,zzzzzz) + (5 rows) + + -- + -- Test psql \dt+ command + -- + DROP POLICY p2 ON category FOR ALL; -- too long qual + \dt+ + List of relations + Schema | Name | Type | Owner | Size | Description | Row-security + --------------------+-----------+-------+-------------------+------------+-------------+---------------------------------- + rls_regress_schema | category | table | rls_regress_user0 | 16 kB | | + rls_regress_schema | dependent | table | rls_regress_user0 | 0 bytes | | + rls_regress_schema | document | table | rls_regress_user0 | 16 kB | | (dauthor = "current_user"()) + rls_regress_schema | rec1 | table | rls_regress_user0 | 0 bytes | | (x = ( SELECT rec2v.a + + | | | | | | FROM rec2v + + | | | | | | WHERE (rec2v.b = rec1.y))) + rls_regress_schema | rec2 | table | rls_regress_user0 | 0 bytes | | (a = ( SELECT rec1v.x + + | | | | | | FROM rec1v + + | | | | | | WHERE (rec1v.y = rec2.b))) + rls_regress_schema | s1 | table | rls_regress_user0 | 16 kB | | (a IN ( SELECT v2.x + + | | | | | | FROM v2)) + rls_regress_schema | s2 | table | rls_regress_user0 | 16 kB | | (x IN ( SELECT s1.a + + | | | | | | FROM s1 + + | | | | | | WHERE (s1.b ~~ '%d2%'::text))) + rls_regress_schema | t1 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 0) + rls_regress_schema | t2 | table | rls_regress_user0 | 16 kB | | ((a % 2) = 1) + rls_regress_schema | t3 | table | rls_regress_user0 | 16 kB | | + rls_regress_schema | uaccount | table | rls_regress_user0 | 8192 bytes | | + (11 rows) + + -- + -- Clean up objects + -- + RESET SESSION AUTHORIZATION; + DROP SCHEMA rls_regress_schema CASCADE; + NOTICE: drop cascades to 15 other objects + DETAIL: drop cascades to function f_leak(text) + drop cascades to table uaccount + drop cascades to table category + drop cascades to table document + drop cascades to table t1 + drop cascades to table t2 + drop cascades to table t3 + drop cascades to table dependent + drop cascades to table rec1 + drop cascades to table rec2 + drop cascades to view rec1v + drop cascades to view rec2v + drop cascades to table s1 + drop cascades to table s2 + drop cascades to view v2 + DROP USER rls_regress_user0; + DROP USER rls_regress_user1; + DROP USER rls_regress_user2; diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out new file mode 100644 index ca56b47..5c20fd6 *** a/src/test/regress/expected/rules.out --- b/src/test/regress/expected/rules.out *************** pg_roles| SELECT pg_authid.rolname, *** 1389,1394 **** --- 1389,1395 ---- pg_authid.rolconnlimit, '********'::text AS rolpassword, pg_authid.rolvaliduntil, + pg_authid.rolbypassrls, s.setconfig AS rolconfig, pg_authid.oid FROM (pg_authid *************** pg_tables| SELECT n.nspname AS schemanam *** 2018,2024 **** t.spcname AS tablespace, c.relhasindex AS hasindexes, c.relhasrules AS hasrules, ! c.relhastriggers AS hastriggers FROM ((pg_class c LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace))) --- 2019,2026 ---- t.spcname AS tablespace, c.relhasindex AS hasindexes, c.relhasrules AS hasrules, ! c.relhastriggers AS hastriggers, ! c.relhasrowsecurity AS hasrowsecurity FROM ((pg_class c LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace))) diff --git a/src/test/regress/expected/sanity_check.out b/src/test/regress/expected/sanity_check.out new file mode 100644 index 111d24c..2c8ec11 *** a/src/test/regress/expected/sanity_check.out --- b/src/test/regress/expected/sanity_check.out *************** pg_pltemplate|t *** 121,126 **** --- 121,127 ---- pg_proc|t pg_range|t pg_rewrite|t + pg_rowsecurity|t pg_seclabel|t pg_shdepend|t pg_shdescription|t diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule new file mode 100644 index c0416f4..ab6c4e2 *** a/src/test/regress/parallel_schedule --- b/src/test/regress/parallel_schedule *************** test: select_into select_distinct select *** 83,89 **** # ---------- # Another group of parallel tests # ---------- ! test: privileges security_label collate matview lock replica_identity # ---------- # Another group of parallel tests --- 83,89 ---- # ---------- # Another group of parallel tests # ---------- ! test: privileges security_label collate matview lock replica_identity rowsecurity # ---------- # Another group of parallel tests diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql new file mode 100644 index ...9efcfea *** a/src/test/regress/sql/rowsecurity.sql --- b/src/test/regress/sql/rowsecurity.sql *************** *** 0 **** --- 1,382 ---- + -- + -- Test of Row-level security feature + -- + + -- Clean up in case a prior regression run failed + + -- Suppress NOTICE messages when users/groups don't exist + SET client_min_messages TO 'warning'; + + DROP USER IF EXISTS rls_regress_user0; + DROP USER IF EXISTS rls_regress_user1; + DROP USER IF EXISTS rls_regress_user2; + DROP USER IF EXISTS rls_regress_exempt_user; + + DROP SCHEMA IF EXISTS rls_regress_schema CASCADE; + + RESET client_min_messages; + + -- initial setup + CREATE USER rls_regress_user0; + CREATE USER rls_regress_user1; + CREATE USER rls_regress_user2; + CREATE USER rls_regress_exempt_user BYPASSRLS; + + CREATE SCHEMA rls_regress_schema; + GRANT ALL ON SCHEMA rls_regress_schema to public; + SET search_path = rls_regress_schema; + + -- setup of malicious function + CREATE OR REPLACE FUNCTION f_leak(text) RETURNS bool + COST 0.0000001 LANGUAGE plpgsql + AS 'BEGIN RAISE NOTICE ''f_leak => %'', $1; RETURN true; END'; + GRANT EXECUTE ON FUNCTION f_leak(text) TO public; + + -- BASIC Row-Level Security Scenario + + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE uaccount ( + pguser name primary key, + seclv int + ); + GRANT SELECT ON uaccount TO public; + INSERT INTO uaccount VALUES + ('rls_regress_user0', 99), + ('rls_regress_user1', 1), + ('rls_regress_user2', 2), + ('rls_regress_user3', 3); + + CREATE TABLE category ( + cid int primary key, + cname text + ); + GRANT ALL ON category TO public; + INSERT INTO category VALUES + (11, 'novel'), + (22, 'science fiction'), + (33, 'technology'), + (44, 'manga'); + + CREATE TABLE document ( + did int primary key, + cid int references category(cid), + dlevel int not null, + dauthor name, + dtitle text + ); + GRANT ALL ON document TO public; + INSERT INTO document VALUES + ( 1, 11, 1, 'rls_regress_user1', 'my first novel'), + ( 2, 11, 2, 'rls_regress_user1', 'my second novel'), + ( 3, 22, 2, 'rls_regress_user1', 'my science fiction'), + ( 4, 44, 1, 'rls_regress_user1', 'my first manga'), + ( 5, 44, 2, 'rls_regress_user1', 'my second manga'), + ( 6, 22, 1, 'rls_regress_user2', 'great science fiction'), + ( 7, 33, 2, 'rls_regress_user2', 'great technology book'), + ( 8, 44, 1, 'rls_regress_user2', 'great manga'); + + -- user's security level must be higher that or equal to document's + CREATE POLICY p1 ON document FOR ALL + USING (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); + + -- viewpoint from rls_regress_user1 + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; + + -- viewpoint from rls_regress_user2 + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER BY did; + + EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); + EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- only owner can change row-level security + ALTER POLICY p1 ON document FOR ALL USING (true); --fail + DROP POLICY p1 ON document FOR ALL; --fail + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON document FOR ALL USING (dauthor = current_user); + + -- viewpoint from rls_regress_user1 again + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; + + -- viewpoint from rls_regres_user2 again + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document WHERE f_leak(dtitle) ORDER BY did; + SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle) ORDER by did; + + EXPLAIN (COSTS OFF) SELECT * FROM document WHERE f_leak(dtitle); + EXPLAIN (COSTS OFF) SELECT * FROM document NATURAL JOIN category WHERE f_leak(dtitle); + + -- interaction of FK/PK constraints + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE POLICY p2 ON category FOR ALL + USING (CASE WHEN current_user = 'rls_regress_user1' THEN cid IN (11, 33) + WHEN current_user = 'rls_regress_user2' THEN cid IN (22, 44) + ELSE false END); + + -- cannot delete PK referenced by invisible FK + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; + DELETE FROM category WHERE cid = 33; -- fails with FK violation + + -- cannot insert FK referencing invisible PK + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM document d FULL OUTER JOIN category c on d.cid = c.cid; + INSERT INTO document VALUES (10, 33, 1, current_user, 'hoge'); -- fail wil FK violation + + -- UNIQUE or PRIMARY KEY constraint violation DOES reveal presence of row + SET SESSION AUTHORIZATION rls_regress_user1; + INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see + SELECT * FROM document WHERE did = 8; -- and confirm we can't see it + + -- database superuser cannot bypass RLS policy when enabled + RESET SESSION AUTHORIZATION; + SET row_security TO ON; + SELECT * FROM document; + SELECT * FROM category; + + -- database superuser can bypass RLS policy when disabled + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM document; + SELECT * FROM category; + + -- database non-superuser with bypass privilege can bypass RLS policy when disabled + SET SESSION AUTHORIZATION rls_regress_exempt_user; + SET row_security TO OFF; + SELECT * FROM document; + SELECT * FROM category; + + -- + -- Table inheritance and RLS policy + -- + SET SESSION AUTHORIZATION rls_regress_user0; + + SET row_security TO ON; + + CREATE TABLE t1 (a int, b text) WITH OIDS; + -- ALTER TABLE t1 DROP COLUMN junk1; -- just a disturbing factor + GRANT ALL ON t1 TO public; + + COPY t1 FROM stdin WITH (oids); + 101 1 aaa + 102 2 bbb + 103 3 ccc + 104 4 ddd + \. + + CREATE TABLE t2 (c float) INHERITS (t1); + COPY t2 FROM stdin WITH (oids); + 201 1 abc 1.1 + 202 2 bcd 2.2 + 203 3 cde 3.3 + 204 4 def 4.4 + \. + + CREATE TABLE t3 (c text, b text, a int) WITH OIDS; + ALTER TABLE t3 INHERIT t1; + COPY t3(a,b,c) FROM stdin WITH (oids); + 301 1 xxx X + 302 2 yyy Y + 303 3 zzz Z + \. + + CREATE POLICY p1 ON t1 FOR ALL USING (a % 2 = 0); -- be even number + CREATE POLICY p2 ON t2 FOR ALL USING (a % 2 = 1); -- be odd number + + SELECT * FROM t1; + EXPLAIN (COSTS OFF) SELECT * FROM t1; + + SELECT * FROM t1 WHERE f_leak(b); + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + + -- reference to system column + SELECT oid, * FROM t1; + EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; + + -- reference to whole-row reference + SELECT *, t1 FROM t1; + EXPLAIN (COSTS OFF) SELECT *, t1 FROM t1; + + -- for share/update lock + SELECT * FROM t1 FOR SHARE; + EXPLAIN (COSTS OFF) SELECT * FROM t1 FOR SHARE; + + SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE; + + -- superuser is allowed to bypass RLS checks + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + + -- non-superuser with bypass privilege can bypass RLS policy when disabled + SET SESSION AUTHORIZATION rls_regress_exempt_user; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + + ----- Dependencies ----- + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + + CREATE TABLE dependee (x integer, y integer); + + CREATE TABLE dependent (x integer, y integer); + CREATE POLICY d1 ON dependent FOR ALL USING (x = (SELECT d.x FROM dependee d WHERE d.y = y)); + + DROP TABLE dependee; -- Should fail without CASCADE due to dependency on row-security qual? + + DROP TABLE dependee CASCADE; + + EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualified + + ----- RECURSION ---- + + -- + -- Simple recursion + -- + CREATE TABLE rec1 (x integer, y integer); + CREATE POLICY r1 ON rec1 FOR ALL + USING (x = (SELECT r.x FROM rec1 r WHERE y = r.y)); + + SELECT * FROM rec1; -- fail, direct recursion + + -- + -- Mutual recursion + -- + CREATE TABLE rec2 (a integer, b integer); + ALTER POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2 WHERE b = y)); + CREATE POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1 WHERE y = b)); + + SELECT * FROM rec1; -- fail, mutual recursion + + -- + -- Mutual recursion via views + -- + CREATE VIEW rec1v AS SELECT * FROM rec1; + CREATE VIEW rec2v AS SELECT * FROM rec2; + ALTER POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2v WHERE b = y)); + ALTER POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1v WHERE y = b)); + + SELECT * FROM rec1; -- fail, mutual recursion via views + + -- + -- Mutual recursion via .s.b views + -- + + DROP VIEW rec1v, rec2v CASCADE; + CREATE VIEW rec1v WITH (security_barrier) AS SELECT * FROM rec1; + CREATE VIEW rec2v WITH (security_barrier) AS SELECT * FROM rec2; + CREATE POLICY r1 ON rec1 FOR ALL USING (x = (SELECT a FROM rec2v WHERE b = y)); + CREATE POLICY r2 ON rec2 FOR ALL USING (a = (SELECT x FROM rec1v WHERE y = b)); + + SELECT * FROM rec1; -- fail, mutual recursion via s.b. views + + -- + -- recursive RLS and VIEWs in policy + -- + CREATE TABLE s1 (a int, b text); + INSERT INTO s1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x); + + CREATE TABLE s2 (x int, y text); + INSERT INTO s2 (SELECT x, md5(x::text) FROM generate_series(-6,6) x); + CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; + + CREATE POLICY p1 ON s1 FOR ALL + USING (a in (select x from s2 where y like '%2f%')); + + CREATE POLICY p2 ON s2 FOR ALL + USING (x in (select a from s1 where b like '%22%')); + + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) + + ALTER POLICY p2 ON s2 FOR ALL USING (x % 2 = 0); + + SELECT * FROM s1 WHERE f_leak(b); -- OK + EXPLAIN SELECT * FROM only s1 WHERE f_leak(b); + + ALTER POLICY p1 ON s1 FOR ALL + USING (a in (select x from v2)); -- using VIEW in RLS policy + SELECT * FROM s1 WHERE f_leak(b); -- OK + EXPLAIN (COSTS OFF) SELECT * FROM s1 WHERE f_leak(b); + + SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + EXPLAIN (COSTS OFF) SELECT (SELECT x FROM s1 LIMIT 1) xx, * FROM s2 WHERE y like '%28%'; + + ALTER POLICY p2 ON s2 FOR ALL + USING (x in (select a from s1 where b like '%d2%')); + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion via view) + + -- prepared statement with rls_regress_user0 privilege + PREPARE p1(int) AS SELECT * FROM t1 WHERE a <= $1; + EXECUTE p1(2); + EXPLAIN (COSTS OFF) EXECUTE p1(2); + + -- superuser is allowed to bypass RLS checks + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM t1 WHERE f_leak(b); + EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b); + + -- plan cache should be invalidated + EXECUTE p1(2); + EXPLAIN (COSTS OFF) EXECUTE p1(2); + + PREPARE p2(int) AS SELECT * FROM t1 WHERE a = $1; + EXECUTE p2(2); + EXPLAIN (COSTS OFF) EXECUTE p2(2); + + -- also, case when privilege switch from superuser + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + EXECUTE p2(2); + EXPLAIN (COSTS OFF) EXECUTE p2(2); + + -- + -- UPDATE / DELETE and Row-level security + -- + SET SESSION AUTHORIZATION rls_regress_user0; + EXPLAIN (COSTS OFF) UPDATE t1 SET b = b || b WHERE f_leak(b); + UPDATE t1 SET b = b || b WHERE f_leak(b); + + EXPLAIN (COSTS OFF) UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); + UPDATE only t1 SET b = b || '_updt' WHERE f_leak(b); + + -- returning clause with system column + UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; + UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *; + UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1; + + RESET SESSION AUTHORIZATION; + SELECT * FROM t1; + + SET SESSION AUTHORIZATION rls_regress_user0; + EXPLAIN (COSTS OFF) DELETE FROM only t1 WHERE f_leak(b); + EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b); + + DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1; + DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1; + + -- + -- Test psql \dt+ command + -- + DROP POLICY p2 ON category FOR ALL; -- too long qual + \dt+ + + -- + -- Clean up objects + -- + RESET SESSION AUTHORIZATION; + + DROP SCHEMA rls_regress_schema CASCADE; + + DROP USER rls_regress_user0; + DROP USER rls_regress_user1; + DROP USER rls_regress_user2;