diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml new file mode 100644 index 68f8434..76d6405 *** a/doc/src/sgml/catalogs.sgml --- b/doc/src/sgml/catalogs.sgml *************** *** 239,244 **** --- 239,249 ---- + pg_rowsecurity + table row-level security policies + + + pg_seclabel security labels on database objects *************** *** 1936,1941 **** --- 1941,1955 ---- + relhasrowsecurity + bool + + True if table has row-security enabled; see + pg_rowsecurity catalog + + + + relhassubclass bool *************** *** 5328,5333 **** --- 5342,5427 ---- + + <structname>pg_rowsecurity</structname> + + + pg_rowsecurity + + + + The catalog pg_rowsecurity stores row-level + security policies for each table. A policy includes the kind of + command which it applies to (or all commands), the roles which it + applies to, the expression to be added as a security-barrier + qualification to queries which include the table and the expression + to be added as a with-check option for queries which attempt to add + new records to the table. + + + + + <structname>pg_rowsecurity</structname> Columns + + + + + Name + Type + References + Description + + + + + + rsecpolname + name + + The name of the row-security policy + + + + rsecrelid + oid + pg_class.oid + The table to which the row-security policy belongs + + + + rseccmd + char + + The command type to which the row-security policy is applied. + + + + rsecqual + pg_node_tree + + The expression tree to be added to the security barrier qualifications for queries which use the table. + + + + rsecwithcheck + pg_node_tree + + The expression tree to be added to the with check qualifications for queries which attempt to add rows to the table. + + + + +
+ + + + pg_class.relhasrowsecurity + True if the table has row-security enabled. + Must be true if the table has a row-security policy in this catalog. + + + +
<structname>pg_seclabel</structname> *************** SELECT * FROM pg_locks pl LEFT JOIN pg_p *** 9133,9138 **** --- 9227,9238 ---- pg_class.relhastriggers True if table has (or once had) triggers
+ + hasrowsecurity + boolean + pg_class.relhasrowsecurity + True if table has row security enabled + diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml new file mode 100644 index 49547ee..cca7ca0 *** a/doc/src/sgml/config.sgml --- b/doc/src/sgml/config.sgml *************** COPY postgres_log FROM '/full/path/to/lo *** 5297,5302 **** --- 5297,5342 ---- + + row_security (enum) + + row_security configuration parameter + + + + + This variable controls if row security policies are to be applied + to queries which are run. The default is 'on'. When set to 'on', + all users, except superusers and the owner of the table, will have + the row policies for the table applied to their queries. The + table owner and superuser can request that row policies be applied + to their queries by setting this to 'force'. Lastly, this can also + be set to 'off' which will bypass row policies for the table, if + possible, and error if not. + + + + For a user who is not a superuser and not the table owner to bypass + row policies for the table, they must have the BYPASSRLS role attribute. + If this is set to 'off' and the user queries a table which has row + policies enabled and the user does not have the right to bypass + row policies then a permission denied error will be returned. + + + + The allowed values of row_security are + on (apply normally- not to superuser or table owner), + off (fail if row security would be applied), and + force (apply always- even to superuser and table owner). + + + + For more information on row security policies, + see . + + + + default_tablespace (string) diff --git a/doc/src/sgml/keywords.sgml b/doc/src/sgml/keywords.sgml new file mode 100644 index 1c93b7c..b0dfd5f *** a/doc/src/sgml/keywords.sgml --- b/doc/src/sgml/keywords.sgml *************** *** 3423,3428 **** --- 3423,3435 ---- non-reserved + POLICY + non-reserved + + + + + PORTION reserved 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 ...37615fc *** a/doc/src/sgml/ref/alter_policy.sgml --- b/doc/src/sgml/ref/alter_policy.sgml *************** *** 0 **** --- 1,135 ---- + + + + + ALTER POLICY + + + + ALTER POLICY + 7 + SQL - Language Statements + + + + ALTER POLICY + change the definition of a row-security policy + + + + + ALTER POLICY name ON table_name + [ RENAME TO new_name ] + [ TO { role_name | PUBLIC } [, ...] ] + [ USING ( expression ) ] + [ WITH CHECK ( check_expression ) ] + + + + + Description + + + ALTER POLICY changes the + definition of an existing row-security policy. + + + + To use ALTER POLICY, you must own the table that + the policy applies to. + + + + + Parameters + + + + name + + + The name of an existing policy to alter. + + + + + + table_name + + + The name (optionally schema-qualified) of the table that the + policy is on. + + + + + + new_name + + + The new name for the policy. + + + + + + role_name + + + The role to which the policy applies. Multiple roles can be specified at one time. + To apply the policy to all roles, use PUBLIC, which is also + the default. + + + + + + expression + + + The USING expression for the policy. This expression will be added as a + security-barrier qualification to queries which use the table + automatically. If multiple policies are being applied for a given + table then they are all combined and added using OR. The USING + expression applies to records which are being retrived from the table. + + + + + + check_expression + + + The with-check expression for the policy. This expression will be + added as a WITH CHECK OPTION qualification to queries which use the + table automatically. If multiple policies are being applied for a + given table then they are all combined and added using OR. The WITH + CHECK expression applies to records which are being added to the table. + + + + + + + + + Compatibility + + + ALTER POLICY is a PostgreSQL extension. + + + + + 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 | [ 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 ...a3c2f22 *** a/doc/src/sgml/ref/create_policy.sgml --- b/doc/src/sgml/ref/create_policy.sgml *************** *** 0 **** --- 1,284 ---- + + + + + CREATE POLICY + + + + CREATE POLICY + 7 + SQL - Language Statements + + + + CREATE POLICY + define a new row-security policy for a table + + + + + CREATE POLICY name ON table_name + [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] + [ TO { role_name | PUBLIC } [, ...] ] + [ USING ( expression ) ] + [ WITH CHECK ( check_expression ) ] + + + + + Description + + + The CREATE POLICY command defines a new row-security policy for a table. + + + + A row-security policy is an expression which is added to the security-barrier + qualifications of queries which are run against the table the policy is on, or + an expression which is added to the with-check options for a table and which + is applied to rows which would be added to the table. + The security-barrier qualifications will always be evaluated prior to any + user-defined functions or user-provided WHERE clauses, while the with-check + expression will be evaluated against the rows which are going to be added to + the table. By adding policies to a table, a user can limit the rows which a + given user can select, insert, update, or delete. This capability is also + known as Row-Level Security or RLS. + + + + Policy names are per-table, therefore one policy name can be used for many + different tables and have a definition for each table which is appropriate to + that table. + + + + Policies can be applied for specific commands or for specific roles. The + default for newly created policies is that they apply for all commands and + roles, unless otherwise specified. If multiple policies apply to a given + query, they will be combined using OR. + + + + + + Parameters + + + + name + + + The name of the policy to be created. This must be distinct from the + name of any other policy for the table. + + + + + + table_name + + + The name (optionally schema-qualified) of the table the + policy applies to. + + + + + + command + + + The command to which the policy applies. Valid options are + ALL, SELECT, + INSERT, UPDATE, + and DELETE. + ALL is the default. + See below for specifics regarding how these are applied. + + + + + + role_name + + + The roles to which the policy is to be applied. The default is + PUBLIC, which will apply the policy to all roles. + + + + + + expression + + + Any SQL conditional expression (returning + boolean). The conditional expression cannot contain + any aggregate or window functions. This expression will be added + to queries to filter out the records which are visible to the query. + + + + + + check_expression + + + Any SQL conditional expression (returning + boolean). The condition expression cannot contain + any aggregate or window functions. This expression will be added + to queries which are attempting to add records to the table as + with-check options, and an error will be thrown if this condition + returns false for any records being added. + + + + + + + + + Per-Command policies + + + + + ALL + + + Using ALL for a policy means that it will apply + to all commands, regardless of the type of command. If an + ALL policy exists and more specific policies + exist, then both the ALL policy and the more + specific policy (or policies) will be combined using + OR, as usual for overlapping policies. + Additionally, ALL policies will be applied to + both the selection side of a query and the modification side, using + the USING policy for both if only a USING policy has been defined. + + As an example, if an UPDATE is issued, then the + ALL policy will be applicable to both what the + UPDATE will be able to select out as rows to be + updated (with the USING expression being applied), and it will be + applied to rows which result from the UPDATE + statement, to check if they are permitted to be added to the table + (using the WITH CHECK expression, if defined, and the USING expression + otherwise). If an INSERT or UPDATE command attempts to add rows to + the table which do not pass the ALL WITH CHECK + (or USING, if no WITH CHECK expression is defined) expression, the + command will error. + + + + + + SELECT + + + Using SELECT for a policy means that it will apply + to SELECT commands. The result is that only those + records from the relation which pass the SELECT + policy will be returned, even if other records exist in the relation. + The SELECT policy only accepts the USING expression + as it only ever applies in cases where records are being retrived from + the relation. + + + + + + INSERT + + + Using INSERT for a policy means that it will apply + to INSERT commands. Rows being inserted which do + not pass this policy will result in a policy violation ERROR and the + entire INSERT command will be aborted. The + INSERT policy only accepts the WITH CHECK expression + as it only ever applies in cases where records are being added to the + relation. + + + + + + DELETE + + + Using UPDATE for a policy means that it will apply + to UPDATE commands. As UPDATE + involves pulling an existing record and then making changes to some + portion (but possibly not all) of the record, the + UPDATE policy accepts both a USING expression and + a WITH CHECK expression. The USING expression will be used to + determine which records the UPDATE command will + see to operate against, while the WITH CHECK + expression defines what rows are allowed to be added back into the + relation (similar to the INSERT policy). + Any rows whose resulting values do not pass the + WITH CHECK expression will cause an ERROR and the + entire command will be aborted. + + + + + + DELETE + + + Using DELETE for a policy means that it will apply + to DELETE commands. Only rows which pass this + policy will be seen by a DELETE command. Rows may + be visible through a SELECT which are not seen by a + DELETE, as they do not pass the USING expression + for the DELETE, and rows which are not visible + through the SELECT policy may be deleted if they + pass the DELETE USING policy. The + DELETE policy only accept the USING expression as + it only ever applies in cases where records are being extracted from + the relation for deletion. + + + + + + + + + Notes + + + You must be the owner of a table to create or change policies for it. + + + + In order to maintain referential integrity between + two related tables, row-security policies are not applied when the system + performs checks on foreign key constraints. + + + + + + Compatibility + + + CREATE POLICY is a PostgreSQL + extension. + + + + + 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..ea26027 *** 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 ...7b85e05 *** a/doc/src/sgml/ref/drop_policy.sgml --- b/doc/src/sgml/ref/drop_policy.sgml *************** *** 0 **** --- 1,103 ---- + + + + + DROP POLICY + + + + DROP POLICY + 7 + SQL - Language Statements + + + + DROP POLICY + remove a row-security policy from a table + + + + + DROP POLICY [ IF EXISTS ] name ON table_name + + + + + Description + + + DROP POLICY removes the specified row-security policy from the table. + + + + + Parameters + + + + + IF EXISTS + + + Do not throw an error if the policy does not exist. A notice is issued + in this case. + + + + + + name + + + The name of the policy to drop. + + + + + + table_name + + + The name (optionally schema-qualified) of the table that + the policy is on. + + + + + + + + + Examples + + + To drop the row-security policy called p1 on the + table named my_table: + + + DROP POLICY p1 ON my_table; + + + + + + Compatibility + + + DROP POLICY is a PostgreSQL extension. + + + + + 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 c346eda..8d9eeb9 *** 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..bd09fbd *** a/src/backend/catalog/objectaddress.c --- b/src/backend/catalog/objectaddress.c *************** *** 42,47 **** --- 42,48 ---- #include "catalog/pg_opfamily.h" #include "catalog/pg_operator.h" #include "catalog/pg_proc.h" + #include "catalog/pg_rowsecurity.h" #include "catalog/pg_rewrite.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" *************** *** 55,60 **** --- 56,62 ---- #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/extension.h" + #include "commands/policy.h" #include "commands/proclang.h" #include "commands/tablespace.h" #include "commands/trigger.h" *************** *** 67,72 **** --- 69,75 ---- #include "parser/parse_oper.h" #include "parser/parse_type.h" #include "rewrite/rewriteSupport.h" + #include "rewrite/rowsecurity.h" #include "storage/lmgr.h" #include "storage/sinval.h" #include "utils/builtins.h" *************** get_object_address(ObjectType objtype, L *** 517,522 **** --- 520,526 ---- case OBJECT_RULE: case OBJECT_TRIGGER: case OBJECT_CONSTRAINT: + case OBJECT_POLICY: address = get_object_address_relobject(objtype, objname, &relation, missing_ok); break; *************** get_object_address_relobject(ObjectType *** 982,987 **** --- 986,998 ---- InvalidOid; address.objectSubId = 0; break; + case OBJECT_POLICY: + address.classId = RowSecurityRelationId; + address.objectId = relation ? + get_relation_policy_oid(reloid, depname, missing_ok) : + InvalidOid; + address.objectSubId = 0; + break; default: elog(ERROR, "unrecognized objtype: %d", (int) objtype); /* placate compiler, which doesn't know elog won't return */ *************** getObjectDescription(const ObjectAddress *** 2166,2171 **** --- 2177,2217 ---- 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, _("policy %s on "), + NameStr(form_rsec->rsecpolname)); + getRelationDescription(&buffer, form_rsec->rsecrelid); + + 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..ae96b55 *** 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_user AS *** 62,67 **** --- 63,96 ---- useconfig FROM pg_shadow; + CREATE VIEW pg_policies AS + SELECT + rs.rsecpolname AS policyname, + (SELECT relname FROM pg_catalog.pg_class WHERE oid = rs.rsecrelid) AS tablename, + CASE + WHEN rs.rsecroles = '{0}' THEN + string_to_array('public', '') + ELSE + ARRAY + ( + SELECT rolname + FROM pg_catalog.pg_authid + WHERE oid = ANY (rs.rsecroles) ORDER BY 1 + ) + END AS roles, + CASE WHEN rs.rseccmd IS NULL THEN 'ALL' ELSE + CASE rs.rseccmd + WHEN 'r' THEN 'SELECT' + WHEN 'a' THEN 'INSERT' + WHEN 'u' THEN 'UPDATE' + WHEN 'd' THEN 'DELETE' + END + END AS cmd, + pg_catalog.pg_get_expr(rs.rsecqual, rs.rsecrelid) AS qual, + pg_catalog.pg_get_expr(rs.rsecwithcheck, rs.rsecrelid) AS with_check + FROM pg_catalog.pg_rowsecurity rs + ORDER BY 1; + CREATE VIEW pg_rules AS SELECT N.nspname AS schemaname, *************** 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'; --- 118,125 ---- 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/alter.c b/src/backend/commands/alter.c new file mode 100644 index 80c9743..c9a9baf *** a/src/backend/commands/alter.c --- b/src/backend/commands/alter.c *************** *** 43,48 **** --- 43,49 ---- #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/tablecmds.h" *************** ExecRenameStmt(RenameStmt *stmt) *** 338,343 **** --- 339,347 ---- case OBJECT_TRIGGER: return renametrig(stmt); + case OBJECT_POLICY: + return rename_policy(stmt); + case OBJECT_DOMAIN: case OBJECT_TYPE: return RenameType(stmt); diff --git a/src/backend/commands/copy.c b/src/backend/commands/copy.c new file mode 100644 index fbd7492..1a70683 *** 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 *** 837,847 **** --- 840,929 ---- rte->selectedCols = bms_add_member(rte->selectedCols, attno); } ExecCheckRTPerms(list_make1(rte), true); + + /* + * Permission check for row security. + * + * If row_security is set to off and the user is not the owner and + * does not have the bypassrls privilege, then throw a permission + * denied error. Note that the superuser always has the bypassrls + * privilege. + */ + if (rel->rd_rel->relhasrowsecurity + && row_security == ROW_SECURITY_OFF + && rel->rd_rel->relowner != GetUserId() + && !has_bypassrls_privilege(GetUserId())) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("insufficient privilege to bypass row security."))); + + /* + * If the relation has a row security policy and we are to apply it + * (row_security is 'force', or 'on' but the user is not the owner or a + * superuser), then perform a "query" copy. This will allow for the + * policies to be applied appropriately to the relation. + * + * Permission checks are done above, so it's fine that if anything + * in the below conditional results in 'false' that we just go ahead + * and allow the COPY directly against the table without RLS being + * applied to it. This would include an owner running with row_security + * set to 'off', for example. + */ + if (rel->rd_rel->relhasrowsecurity + && (row_security == ROW_SECURITY_FORCE + || (row_security == ROW_SECURITY_ON + && rel->rd_rel->relowner != GetUserId() + && !superuser()))) + { + SelectStmt *select; + ColumnRef *cr; + ResTarget *target; + RangeVar *from; + + if (is_from) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("COPY FROM not supported with row security."), + errhint("Use direct INSERT statements instead."))); + + /* Build target list */ + cr = makeNode(ColumnRef); + + if (!stmt->attlist) + cr->fields = list_make1(makeNode(A_Star)); + else + cr->fields = stmt->attlist; + + 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 { 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 */ --- 943,949 ---- } 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..1b8c94b *** a/src/backend/commands/event_trigger.c --- b/src/backend/commands/event_trigger.c *************** static event_trigger_support_data event_ *** 85,90 **** --- 85,91 ---- {"OPERATOR", true}, {"OPERATOR CLASS", true}, {"OPERATOR FAMILY", true}, + {"POLICY", true}, {"ROLE", false}, {"RULE", true}, {"SCHEMA", true}, *************** EventTriggerSupportsObjectType(ObjectTyp *** 936,941 **** --- 937,943 ---- case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_POLICY: case OBJECT_RULE: case OBJECT_SCHEMA: case OBJECT_SEQUENCE: *************** EventTriggerSupportsObjectClass(ObjectCl *** 995,1000 **** --- 997,1003 ---- 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 ...6c464c8 *** a/src/backend/commands/policy.c --- b/src/backend/commands/policy.c *************** *** 0 **** --- 1,1030 ---- + /*------------------------------------------------------------------------- + * + * 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.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/objectaccess.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/array.h" + #include "utils/builtins.h" + #include "utils/fmgroids.h" + #include "utils/inval.h" + #include "utils/lsyscache.h" + #include "utils/memutils.h" + #include "utils/rel.h" + #include "utils/syscache.h" + + static void RangeVarCallbackForPolicy(const RangeVar *rv, + Oid relid, Oid oldrelid, void *arg); + static const char parse_row_security_command(const char *cmd_name); + static ArrayType* parse_role_ids(List *roles); + + /* + * 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 + RangeVarCallbackForPolicy(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 command 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 (!cmd_name) + elog(ERROR, "Unregonized command."); + + if (strcmp(cmd_name, "all") == 0) + cmd = 0; + else if (strcmp(cmd_name, "select") == 0) + cmd = ACL_SELECT_CHR; + else if (strcmp(cmd_name, "insert") == 0) + cmd = ACL_INSERT_CHR; + else if (strcmp(cmd_name, "update") == 0) + cmd = ACL_UPDATE_CHR; + else if (strcmp(cmd_name, "delete") == 0) + cmd = ACL_DELETE_CHR; + else + elog(ERROR, "Unregonized command."); + /* error unrecognized command */ + + return cmd; + } + + /* + * parse_role_ids + * helper function to convert a list of role names in to an array of + * role ids. + * + * Note: If PUBLIC is provided as a role name, then ACL_PUBLIC_ID is + * used as the role id. + * + * roles - the list of role names to convert. + */ + static ArrayType * + parse_role_ids(List *roles) + { + ArrayType *role_ids; + Datum *temp_array; + ListCell *cell; + int num_roles; + int i = 0; + + /* Handle no roles being passed in as being for public */ + if (roles == NIL) + { + temp_array = (Datum *) palloc(sizeof(Datum)); + temp_array[0] = ObjectIdGetDatum(ACL_ID_PUBLIC); + + role_ids = construct_array(temp_array, 1, OIDOID, sizeof(Oid), true, + 'i'); + return role_ids; + } + + num_roles = list_length(roles); + temp_array = (Datum *) palloc(num_roles * sizeof(Datum)); + + foreach(cell, roles) + { + Oid roleid = get_role_oid_or_public(strVal(lfirst(cell))); + + /* + * PUBLIC covers all roles, so it only makes sense alone. + */ + if (roleid == ACL_ID_PUBLIC) + { + if (num_roles != 1) + ereport(WARNING, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("ignoring roles specified other than public"), + errhint("All roles are members of the public role."))); + + temp_array[0] = ObjectIdGetDatum(roleid); + num_roles = 1; + break; + } + else + temp_array[i++] = ObjectIdGetDatum(roleid); + } + + role_ids = construct_array(temp_array, num_roles, OIDOID, sizeof(Oid), true, + 'i'); + + return role_ids; + } + + /* + * 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, RowSecurityRelidPolnameIndexId, true, + NULL, 1, &skey); + PG_TRY(); + { + /* + * Loop through the row-level security entries for this relation, if + * any. + */ + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + Datum value_datum; + char cmd_value; + ArrayType *roles; + char *qual_value; + Expr *qual_expr; + char *with_check_value; + Expr *with_check_qual; + char *policy_name_value; + Oid policy_id; + bool isnull; + RowSecurityPolicy *policy = NULL; + + /* + * 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; + } + + oldcxt = MemoryContextSwitchTo(rscxt); + + /* Get policy command */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rseccmd, + RelationGetDescr(catalog), &isnull); + if (isnull) + cmd_value = 0; + else + 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 roles */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecroles, + RelationGetDescr(catalog), &isnull); + Assert(!isnull); + roles = DatumGetArrayTypeP(value_datum); + + /* Get policy qual */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecqual, + RelationGetDescr(catalog), &isnull); + if (!isnull) + { + qual_value = TextDatumGetCString(value_datum); + qual_expr = (Expr *) stringToNode(qual_value); + } + else + qual_expr = NULL; + + /* Get WITH CHECK qual */ + value_datum = heap_getattr(tuple, Anum_pg_rowsecurity_rsecwithcheck, + RelationGetDescr(catalog), &isnull); + + if (!isnull) + { + with_check_value = TextDatumGetCString(value_datum); + with_check_qual = (Expr *) stringToNode(with_check_value); + } + else + with_check_qual = NULL; + + policy_id = HeapTupleGetOid(tuple); + + policy = palloc0(sizeof(RowSecurityPolicy)); + policy->policy_name = policy_name_value; + policy->rsecid = policy_id; + policy->cmd = cmd_value; + policy->roles = roles; + policy->qual = copyObject(qual_expr); + policy->with_check_qual = copyObject(with_check_qual); + policy->hassublinks = contain_subplans((Node *) qual_expr); + + rsdesc->policies = lcons(policy, rsdesc->policies); + + MemoryContextSwitchTo(oldcxt); + + if (qual_expr != NULL) + pfree(qual_expr); + + if (with_check_qual != NULL) + pfree(with_check_qual); + } + } + 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 provided 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 remove 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; + ArrayType *role_ids; + ParseState *qual_pstate; + ParseState *with_check_pstate; + RangeTblEntry *rte; + Node *qual; + Node *with_check_qual; + ScanKeyData skeys[2]; + 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); + + /* + * If the command is SELECT or DELETE then WITH CHECK should be NULL. + */ + if ((rseccmd == ACL_SELECT_CHR || rseccmd == ACL_DELETE_CHR) + && stmt->with_check != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("WITH CHECK cannot be applied to SELECT or DELETE"))); + + /* + * If the command is INSERT then WITH CHECK should be the only expression + * provided. + */ + if (rseccmd == ACL_INSERT_CHR && stmt->qual != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("Only WITH CHECK expression allowed for INSERT"))); + + + /* Collect role ids */ + role_ids = parse_role_ids(stmt->roles); + + /* Parse the supplied clause */ + qual_pstate = make_parsestate(NULL); + with_check_pstate = make_parsestate(NULL); + + /* zero-clear */ + memset(values, 0, sizeof(values)); + memset(isnull, 0, sizeof(isnull)); + + /* Get id of table. Also handles permissions checks. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForPolicy, + (void *) stmt); + + /* Open target_table to build quals. No lock is necessary.*/ + target_table = relation_open(table_id, NoLock); + + /* Add for the regular security quals */ + rte = addRangeTableEntryForRelation(qual_pstate, target_table, + NULL, false, false); + addRTEtoQuery(qual_pstate, rte, false, true, true); + + /* Add for the with-check quals */ + rte = addRangeTableEntryForRelation(with_check_pstate, target_table, + NULL, false, false); + addRTEtoQuery(with_check_pstate, rte, false, true, true); + + qual = transformWhereClause(qual_pstate, + copyObject(stmt->qual), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + with_check_qual = transformWhereClause(with_check_pstate, + copyObject(stmt->with_check), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + /* Open pg_rowsecurity catalog */ + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Set key - row security relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skeys); + + rsec_tuple = systable_getnext(sscan); + + /* Complain if the policy name already exists for the table */ + if (HeapTupleIsValid(rsec_tuple)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("policy \"%s\" for relation \"%s\" already exists", + stmt->policy_name, RelationGetRelationName(target_table)))); + + values[Anum_pg_rowsecurity_rsecrelid - 1] = ObjectIdGetDatum(table_id); + values[Anum_pg_rowsecurity_rsecpolname - 1] + = CStringGetDatum(stmt->policy_name); + + if (rseccmd) + values[Anum_pg_rowsecurity_rseccmd - 1] = CharGetDatum(rseccmd); + else + isnull[Anum_pg_rowsecurity_rseccmd - 1] = true; + + values[Anum_pg_rowsecurity_rsecroles - 1] = PointerGetDatum(role_ids); + + /* Add qual if present. */ + if (qual) + values[Anum_pg_rowsecurity_rsecqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + else + isnull[Anum_pg_rowsecurity_rsecqual - 1] = true; + + /* Add WITH CHECK qual if present */ + if (with_check_qual) + values[Anum_pg_rowsecurity_rsecwithcheck - 1] + = CStringGetTextDatum(nodeToString(with_check_qual)); + else + isnull[Anum_pg_rowsecurity_rsecwithcheck - 1] = true; + + rsec_tuple = heap_form_tuple(RelationGetDescr(pg_rowsecurity_rel), values, + isnull); + + rowsec_id = simple_heap_insert(pg_rowsecurity_rel, rsec_tuple); + + /* 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, qual_pstate->p_rtable, + DEPENDENCY_NORMAL); + + recordDependencyOnExpr(&myself, with_check_qual, + with_check_pstate->p_rtable, DEPENDENCY_NORMAL); + + /* Turn on relhasrowsecurity for the table, if not done. */ + if (!RelationGetForm(target_table)->relhasrowsecurity) + { + HeapTuple tuple; + Relation class_rel; + + class_rel = heap_open(RelationRelationId, RowExclusiveLock); + 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(qual_pstate); + free_parsestate(with_check_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; + ArrayType *role_ids = NULL; + List *qual_parse_rtable = NIL; + List *with_check_parse_rtable = NIL; + Node *qual = NULL; + Node *with_check_qual = NULL; + ScanKeyData skeys[2]; + 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; + Datum cmd_datum; + char rseccmd; + bool rseccmd_isnull; + + /* Parse role_ids */ + if (stmt->roles != NULL) + role_ids = parse_role_ids(stmt->roles); + + /* Get id of table. Also handles permissions checks. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForPolicy, + (void *) stmt); + + target_table = relation_open(table_id, NoLock); + + /* Parse the row-security clause */ + if (stmt->qual) + { + RangeTblEntry *rte; + ParseState *qual_pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(qual_pstate, target_table, + NULL, false, false); + + addRTEtoQuery(qual_pstate, rte, false, true, true); + + qual = transformWhereClause(qual_pstate, copyObject(stmt->qual), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + qual_parse_rtable = qual_pstate->p_rtable; + free_parsestate(qual_pstate); + } + + /* Parse the with-check row-security clause */ + if (stmt->with_check) + { + RangeTblEntry *rte; + ParseState *with_check_pstate = make_parsestate(NULL); + + rte = addRangeTableEntryForRelation(with_check_pstate, target_table, + NULL, false, false); + + addRTEtoQuery(with_check_pstate, rte, false, true, true); + + with_check_qual = transformWhereClause(with_check_pstate, + copyObject(stmt->with_check), + EXPR_KIND_ROW_SECURITY, + "ROW SECURITY"); + + with_check_parse_rtable = with_check_pstate->p_rtable; + free_parsestate(with_check_pstate); + } + + /* 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 relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skeys); + + rsec_tuple = systable_getnext(sscan); + + /* Check that the policy is found, raise an error if not. */ + if (!HeapTupleIsValid(rsec_tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("policy '%s' for does not exist on table %s", + stmt->policy_name, + RelationGetRelationName(target_table)))); + + /* Get policy command */ + cmd_datum = heap_getattr(rsec_tuple, Anum_pg_rowsecurity_rseccmd, + RelationGetDescr(pg_rowsecurity_rel), + &rseccmd_isnull); + if (rseccmd_isnull) + rseccmd = 0; + else + rseccmd = DatumGetChar(cmd_datum); + + /* + * If the command is SELECT or DELETE then WITH CHECK should be NULL. + */ + if ((rseccmd == ACL_SELECT_CHR || rseccmd == ACL_DELETE_CHR) + && stmt->with_check != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only USING expression allowed for SELECT, DELETE"))); + + /* + * If the command is INSERT then WITH CHECK should be the only + * expression provided. + */ + if ((rseccmd == ACL_INSERT_CHR) + && stmt->qual != NULL) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("only WITH CHECK expression allowed for INSERT"))); + + rowsec_id = HeapTupleGetOid(rsec_tuple); + + if (role_ids != NULL) + { + replaces[Anum_pg_rowsecurity_rsecroles - 1] = true; + values[Anum_pg_rowsecurity_rsecroles - 1] = PointerGetDatum(role_ids); + } + + if (qual != NULL) + { + replaces[Anum_pg_rowsecurity_rsecqual - 1] = true; + values[Anum_pg_rowsecurity_rsecqual - 1] + = CStringGetTextDatum(nodeToString(qual)); + } + + if (with_check_qual != NULL) + { + replaces[Anum_pg_rowsecurity_rsecwithcheck - 1] = true; + values[Anum_pg_rowsecurity_rsecwithcheck - 1] + = CStringGetTextDatum(nodeToString(with_check_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, qual_parse_rtable, DEPENDENCY_NORMAL); + + recordDependencyOnExpr(&myself, with_check_qual, with_check_parse_rtable, + DEPENDENCY_NORMAL); + + heap_freetuple(new_tuple); + + /* Invalidate Relation Cache */ + CacheInvalidateRelcache(target_table); + + /* Clean up. */ + systable_endscan(sscan); + relation_close(target_table, NoLock); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + + return rowsec_id; + } + + /* + * rename_policy - + * change the name of a policy on a relation + */ + Oid + rename_policy(RenameStmt *stmt) + { + Relation pg_rowsecurity_rel; + Relation target_table; + Oid table_id; + Oid opoloid; + ScanKeyData skeys[2]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + + /* Get id of table. Also handles permissions checks. */ + table_id = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock, + false, false, + RangeVarCallbackForPolicy, + (void *) stmt); + + target_table = relation_open(table_id, NoLock); + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* First pass- check for conflict */ + + /* Add key - row security relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->newname)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skeys); + + if (HeapTupleIsValid(rsec_tuple = systable_getnext(sscan))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_OBJECT), + errmsg("row-policy \"%s\" for table \"%s\" already exists", + stmt->newname, RelationGetRelationName(target_table)))); + + systable_endscan(sscan); + + /* Second pass -- find existing policy and update */ + /* Add key - row security relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->subname)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skeys); + + rsec_tuple = systable_getnext(sscan); + + /* Complain if we did not find the policy */ + if (!HeapTupleIsValid(rsec_tuple)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("row-policy \"%s\" for table \"%s\" does not exist", + stmt->subname, RelationGetRelationName(target_table)))); + + opoloid = HeapTupleGetOid(rsec_tuple); + + rsec_tuple = heap_copytuple(rsec_tuple); + + namestrcpy(&((Form_pg_rowsecurity) GETSTRUCT(rsec_tuple))->rsecpolname, + stmt->newname); + + simple_heap_update(pg_rowsecurity_rel, &rsec_tuple->t_self, rsec_tuple); + + /* keep system catalog indexes current */ + CatalogUpdateIndexes(pg_rowsecurity_rel, rsec_tuple); + + InvokeObjectPostAlterHook(RowSecurityRelationId, + HeapTupleGetOid(rsec_tuple), 0); + + /* + * Invalidate relation's relcache entry so that other backends (and + * this one too!) are sent SI message to make them rebuild relcache + * entries. (Ideally this should happen automatically...) + */ + CacheInvalidateRelcache(target_table); + + /* Clean up. */ + systable_endscan(sscan); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + relation_close(target_table, NoLock); + + return opoloid; + } + + /* + * 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; + Relation target_table; + Oid table_id; + ScanKeyData skeys[2]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + + /* Get id of table. Also handles permissions checks. */ + table_id = RangeVarGetRelidExtended(stmt->table, AccessExclusiveLock, + false, false, + RangeVarCallbackForPolicy, + (void *) stmt); + + target_table = relation_open(table_id, NoLock); + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Add key - row security relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + 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\"", + stmt->policy_name, stmt->table->relname))); + else + ereport(NOTICE, + (errmsg("row-security policy \"%s\" does not exist on table" + " \"%s\", skipping", + stmt->policy_name, stmt->table->relname))); + } + + /* Clean up. */ + systable_endscan(sscan); + relation_close(target_table, NoLock); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + } + + /* + * get_relation_policy_oid - Look up a policy by name to find its OID + * + * If missing_ok is false, throw an error if policy not found. If + * true, just return InvalidOid. + */ + Oid + get_relation_policy_oid(Oid relid, const char *policy_name, bool missing_ok) + { + Relation pg_rowsecurity_rel; + ScanKeyData skeys[2]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + Oid policy_oid; + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, AccessShareLock); + + /* Add key - row security relation id. */ + ScanKeyInit(&skeys[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skeys[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skeys); + + rsec_tuple = systable_getnext(sscan); + + if (!HeapTupleIsValid(rsec_tuple)) + { + if (!missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("row-security policy \"%s\" for table \"%s\" does not exist", + policy_name, get_rel_name(relid)))); + + policy_oid = InvalidOid; + } + else + policy_oid = HeapTupleGetOid(rsec_tuple); + + /* Clean up. */ + systable_endscan(sscan); + heap_close(pg_rowsecurity_rel, RowExclusiveLock); + + return policy_oid; + } diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c new file mode 100644 index 7bc579b..a0ee9a2 *** a/src/backend/commands/tablecmds.c --- b/src/backend/commands/tablecmds.c *************** *** 36,41 **** --- 36,42 ---- #include "catalog/pg_inherits_fn.h" #include "catalog/pg_namespace.h" #include "catalog/pg_opclass.h" + #include "catalog/pg_rowsecurity.h" #include "catalog/pg_tablespace.h" #include "catalog/pg_trigger.h" #include "catalog/pg_type.h" *************** *** 45,50 **** --- 46,52 ---- #include "commands/cluster.h" #include "commands/comment.h" #include "commands/defrem.h" + #include "commands/policy.h" #include "commands/sequence.h" #include "commands/tablecmds.h" #include "commands/tablespace.h" *************** static void ATExecAddOf(Relation rel, co *** 408,413 **** --- 410,417 ---- static void ATExecDropOf(Relation rel, LOCKMODE lockmode); static void ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode); static void ATExecGenericOptions(Relation rel, List *options); + static void ATExecEnableRowSecurity(Relation rel); + static void ATExecDisableRowSecurity(Relation rel, DropBehavior behavior); static void copy_relation_data(SMgrRelation rel, SMgrRelation dst, ForkNumber forkNum, char relpersistence); *************** AlterTableGetLockLevel(List *cmds) *** 2872,2877 **** --- 2876,2883 ---- case AT_AddIndexConstraint: case AT_ReplicaIdentity: case AT_SetNotNull: + case AT_EnableRowSecurity: + case AT_DisableRowSecurity: cmd_lockmode = AccessExclusiveLock; break; *************** ATPrepCmd(List **wqueue, Relation rel, A *** 3280,3285 **** --- 3286,3293 ---- case AT_DropInherit: /* NO INHERIT */ case AT_AddOf: /* OF */ case AT_DropOf: /* NOT OF */ + case AT_EnableRowSecurity: + case AT_DisableRowSecurity: ATSimplePermissions(rel, ATT_TABLE); /* These commands never recurse */ /* No command-specific prep needed */ *************** ATExecCmd(List **wqueue, AlteredTableInf *** 3571,3576 **** --- 3579,3590 ---- case AT_ReplicaIdentity: ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode); break; + case AT_EnableRowSecurity: + ATExecEnableRowSecurity(rel); + break; + case AT_DisableRowSecurity: + ATExecDisableRowSecurity(rel, cmd->behavior); + break; case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; *************** ATExecReplicaIdentity(Relation rel, Repl *** 10615,10620 **** --- 10629,10723 ---- } /* + * ALTER TABLE ENABLE/DISABLE ROW LEVEL SECURITY + */ + static void + ATExecEnableRowSecurity(Relation rel) + { + Relation pg_class; + Oid relid; + HeapTuple tuple; + + relid = RelationGetRelid(rel); + + pg_class = heap_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = true; + simple_heap_update(pg_class, &tuple->t_self, tuple); + + /* keep catalog indexes current */ + CatalogUpdateIndexes(pg_class, tuple); + + heap_close(pg_class, RowExclusiveLock); + heap_freetuple(tuple); + } + + static void + ATExecDisableRowSecurity(Relation rel, DropBehavior behavior) + { + Relation pg_rowsecurity; + Relation pg_class; + Oid relid; + ScanKeyData skey; + SysScanDesc sscan; + HeapTuple tuple; + + relid = RelationGetRelid(rel); + pg_rowsecurity = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Look for any existing policies for this table */ + ScanKeyInit(&skey, + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + sscan = systable_beginscan(pg_rowsecurity, RowSecurityRelidPolnameIndexId, + true, NULL, 1, &skey); + + /* + * If there are policies defined for this table then we need to either + * drop them (CASCADE case), or error out and complain to the user. + */ + while (HeapTupleIsValid(tuple = systable_getnext(sscan))) + { + if (behavior != DROP_CASCADE) + ereport(ERROR, + (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST), + errmsg("could not disable row-security on relation %s", + RelationGetRelationName(rel)), + errdetail("relation has row-security policies."), + errhint("Use ALTER TABLE %s DISABLE ROW LEVEL SECURITY CASCADE", + RelationGetRelationName(rel)))); + + RemovePolicyById(HeapTupleGetOid(tuple)); + } + + /* Pull the record for this relation and update it */ + pg_class = heap_open(RelationRelationId, RowExclusiveLock); + + tuple = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); + + if (!HeapTupleIsValid(tuple)) + elog(ERROR, "cache lookup failed for relation %u", relid); + + ((Form_pg_class) GETSTRUCT(tuple))->relhasrowsecurity = false; + simple_heap_update(pg_class, &tuple->t_self, tuple); + + /* keep catalog indexes current */ + CatalogUpdateIndexes(pg_class, tuple); + + systable_endscan(sscan); + heap_close(pg_rowsecurity, RowExclusiveLock); + heap_close(pg_class, RowExclusiveLock); + heap_freetuple(tuple); + } + + /* * ALTER FOREIGN TABLE OPTIONS (...) */ static void diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c new file mode 100644 index 91b6fa5..1a73fd8 *** 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 bypassrls 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) *** 474,479 **** --- 495,501 ---- 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) *** 484,489 **** --- 506,512 ---- DefElem *dconnlimit = NULL; DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; + DefElem *dbypassRLS = NULL; Oid roleid; /* Extract options from the statement node tree */ *************** AlterRole(AlterRoleStmt *stmt) *** 578,583 **** --- 601,614 ---- 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) *** 609,614 **** --- 640,647 ---- 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) *** 642,647 **** --- 675,687 ---- (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 change bypassrls attribute"))); + } else if (!have_createrole_privilege()) { if (!(inherit < 0 && *************** AlterRole(AlterRoleStmt *stmt) *** 775,780 **** --- 815,826 ---- 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 aa053a0..f916089 *** a/src/backend/nodes/copyfuncs.c --- b/src/backend/nodes/copyfuncs.c *************** _copyQuery(const Query *from) *** 2488,2493 **** --- 2488,2494 ---- COPY_SCALAR_FIELD(hasRecursive); COPY_SCALAR_FIELD(hasModifyingCTE); COPY_SCALAR_FIELD(hasForUpdate); + COPY_SCALAR_FIELD(hasRowSecurity); COPY_NODE_FIELD(cteList); COPY_NODE_FIELD(rtable); COPY_NODE_FIELD(jointree); *************** _copyAlterTSConfigurationStmt(const Alte *** 3849,3854 **** --- 3850,3895 ---- 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(roles); + COPY_NODE_FIELD(qual); + COPY_NODE_FIELD(with_check); + + return newnode; + } + + static AlterPolicyStmt * + _copyAlterPolicyStmt(const AlterPolicyStmt *from) + { + AlterPolicyStmt *newnode = makeNode(AlterPolicyStmt); + + COPY_STRING_FIELD(policy_name); + COPY_NODE_FIELD(table); + COPY_NODE_FIELD(roles); + COPY_NODE_FIELD(qual); + COPY_NODE_FIELD(with_check); + + return newnode; + } + + static DropPolicyStmt * + _copyDropPolicyStmt(const DropPolicyStmt *from) + { + DropPolicyStmt *newnode = makeNode(DropPolicyStmt); + + COPY_STRING_FIELD(policy_name); + COPY_NODE_FIELD(table); + + return newnode; + } + /* **************************************************************** * pg_list.h copy functions * **************************************************************** *************** copyObject(const void *from) *** 4561,4566 **** --- 4602,4616 ---- 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 719923e..968bae1 *** a/src/backend/nodes/equalfuncs.c --- b/src/backend/nodes/equalfuncs.c *************** _equalQuery(const Query *a, const Query *** 857,862 **** --- 857,863 ---- COMPARE_SCALAR_FIELD(hasRecursive); COMPARE_SCALAR_FIELD(hasModifyingCTE); COMPARE_SCALAR_FIELD(hasForUpdate); + COMPARE_SCALAR_FIELD(hasRowSecurity); COMPARE_NODE_FIELD(cteList); COMPARE_NODE_FIELD(rtable); COMPARE_NODE_FIELD(jointree); *************** _equalAlterTSConfigurationStmt(const Alt *** 2008,2013 **** --- 2009,2048 ---- } 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(roles); + COMPARE_NODE_FIELD(qual); + COMPARE_NODE_FIELD(with_check); + + return true; + } + + static bool + _equalAlterPolicyStmt(const AlterPolicyStmt *a, const AlterPolicyStmt *b) + { + COMPARE_STRING_FIELD(policy_name); + COMPARE_NODE_FIELD(table); + COMPARE_NODE_FIELD(roles); + COMPARE_NODE_FIELD(qual); + COMPARE_NODE_FIELD(with_check); + + return true; + } + + static bool + _equalDropPolicyStmt(const DropPolicyStmt *a, const DropPolicyStmt *b) + { + COMPARE_STRING_FIELD(policy_name); + COMPARE_NODE_FIELD(table); + + 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 **** --- 3060,3074 ---- 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/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c new file mode 100644 index e686a6c..1ff78eb *** a/src/backend/nodes/outfuncs.c --- b/src/backend/nodes/outfuncs.c *************** _outQuery(StringInfo str, const Query *n *** 2263,2268 **** --- 2263,2269 ---- WRITE_BOOL_FIELD(hasRecursive); WRITE_BOOL_FIELD(hasModifyingCTE); WRITE_BOOL_FIELD(hasForUpdate); + WRITE_BOOL_FIELD(hasRowSecurity); WRITE_NODE_FIELD(cteList); WRITE_NODE_FIELD(rtable); WRITE_NODE_FIELD(jointree); diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c new file mode 100644 index 69d9989..a324100 *** a/src/backend/nodes/readfuncs.c --- b/src/backend/nodes/readfuncs.c *************** _readQuery(void) *** 208,213 **** --- 208,214 ---- READ_BOOL_FIELD(hasRecursive); READ_BOOL_FIELD(hasModifyingCTE); READ_BOOL_FIELD(hasForUpdate); + READ_BOOL_FIELD(hasRowSecurity); READ_NODE_FIELD(cteList); READ_NODE_FIELD(rtable); READ_NODE_FIELD(jointree); diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c new file mode 100644 index e1480cd..de1149d *** a/src/backend/optimizer/plan/planner.c --- b/src/backend/optimizer/plan/planner.c *************** standard_planner(Query *parse, int curso *** 177,182 **** --- 177,184 ---- glob->lastPHId = 0; glob->lastRowMarkId = 0; glob->transientPlan = false; + glob->has_rls = false; + glob->targetRelId = InvalidOid; /* Determine what fraction of the plan is likely to be scanned */ if (cursorOptions & CURSOR_OPT_FAST_PLAN) *************** standard_planner(Query *parse, int curso *** 254,259 **** --- 256,263 ---- result->relationOids = glob->relationOids; result->invalItems = glob->invalItems; result->nParamExec = glob->nParamExec; + result->has_rls = glob->has_rls; + result->targetRelId = glob->targetRelId; return result; } *************** grouping_planner(PlannerInfo *root, doub *** 1206,1211 **** --- 1210,1216 ---- * This may add new security barrier subquery RTEs to the rangetable. */ expand_security_quals(root, tlist); + root->glob->has_rls = parse->hasRowSecurity; /* * Locate any window functions in the tlist. (We don't need to look diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c new file mode 100644 index 4d717df..5f01fd3 *** a/src/backend/optimizer/plan/setrefs.c --- b/src/backend/optimizer/plan/setrefs.c *************** record_plan_function_dependency(PlannerI *** 2081,2087 **** void extract_query_dependencies(Node *query, List **relationOids, ! List **invalItems) { PlannerGlobal glob; PlannerInfo root; --- 2081,2089 ---- void extract_query_dependencies(Node *query, List **relationOids, ! List **invalItems, ! bool *hasRowSecurity, ! Oid *targetRelId) { PlannerGlobal glob; PlannerInfo root; *************** extract_query_dependencies(Node *query, *** 2091,2096 **** --- 2093,2100 ---- glob.type = T_PlannerGlobal; glob.relationOids = NIL; glob.invalItems = NIL; + glob.has_rls = false; + glob.targetRelId = InvalidOid; MemSet(&root, 0, sizeof(root)); root.type = T_PlannerInfo; *************** extract_query_dependencies(Node *query, *** 2100,2105 **** --- 2104,2111 ---- *relationOids = glob.relationOids; *invalItems = glob.invalItems; + *hasRowSecurity = glob.has_rls; + *targetRelId = glob.targetRelId; } static bool *************** extract_query_dependencies_walker(Node * *** 2115,2120 **** --- 2121,2129 ---- Query *query = (Query *) node; ListCell *lc; + /* Collect row-security information */ + context->glob->has_rls = query->hasRowSecurity; + if (query->commandType == CMD_UTILITY) { /* *************** extract_query_dependencies_walker(Node * *** 2126,2131 **** --- 2135,2153 ---- return false; } + /* Collect Target Relation Information */ + if (query->rtable != NULL) + { + if (list_length(query->rtable) > query->resultRelation) + { + ListCell *targetCell = list_nth_cell(query->rtable, query->resultRelation); + RangeTblEntry *rte = (RangeTblEntry *) lfirst(targetCell); + + if (rte->rtekind == RTE_RELATION) + context->glob->targetRelId = rte->relid; + } + } + /* Collect relation OIDs in this Query's rtable */ foreach(lc, query->rtable) { diff --git a/src/backend/optimizer/prep/prepsecurity.c b/src/backend/optimizer/prep/prepsecurity.c new file mode 100644 index 2420f97..6710d28 *** a/src/backend/optimizer/prep/prepsecurity.c --- b/src/backend/optimizer/prep/prepsecurity.c *************** security_barrier_replace_vars_walker(Nod *** 448,453 **** --- 448,456 ---- /* Update the outer query's variable */ var->varattno = var->varoattno = attno; + /* Re-set varoattno also, as equalVar looks at it too */ + var->varoattno = attno; + /* Remember this Var so that we don't process it again */ context->vars_processed = lappend(context->vars_processed, var); } diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y new file mode 100644 index b46dd7b..f74adc7 *** 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 *** 319,324 **** --- 319,328 ---- %type all_Op MathOp + %type row_security_cmd RowSecurityDefaultForCmd + %type RowSecurityOptionalWithCheck RowSecurityOptionalExpr + %type RowSecurityDefaultToRole RowSecurityOptionalToRole + %type iso_level opt_encoding %type grantee %type grantee_list *************** static Node *makeRecursiveViewSelect(cha *** 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 POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM --- 593,599 ---- 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 : *** 740,745 **** --- 744,750 ---- | AlterGroupStmt | AlterObjectSchemaStmt | AlterOwnerStmt + | AlterPolicyStmt | AlterSeqStmt | AlterSystemStmt | AlterTableStmt *************** stmt : *** 774,779 **** --- 779,785 ---- | CreateOpClassStmt | CreateOpFamilyStmt | AlterOpFamilyStmt + | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt | CreateSeqStmt *************** stmt : *** 799,804 **** --- 805,811 ---- | DropOpClassStmt | DropOpFamilyStmt | DropOwnedStmt + | DropPolicyStmt | DropPLangStmt | DropRuleStmt | DropStmt *************** AlterOptRoleElem: *** 957,962 **** --- 964,973 ---- $$ = 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) { /* *************** alter_table_cmd: *** 2302,2307 **** --- 2313,2333 ---- n->def = $3; $$ = (Node *)n; } + /* ALTER TABLE ENABLE ROW LEVEL SECURITY */ + | ENABLE_P ROW LEVEL SECURITY + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_EnableRowSecurity; + $$ = (Node *)n; + } + /* ALTER TABLE DISABLE ROW LEVEL SECURITY */ + | DISABLE_P ROW LEVEL SECURITY opt_drop_behavior + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_DisableRowSecurity; + n->behavior = $5; + $$ = (Node *)n; + } | alter_generic_options { AlterTableCmd *n = makeNode(AlterTableCmd); *************** AlterUserMappingStmt: ALTER USER MAPPING *** 4497,4502 **** --- 4523,4619 ---- /***************************************************************************** * + * QUERIES: + * CREATE POLICY name ON table FOR cmd TO role USING (qual) + * ALTER POLICY name ON table FOR cmd TO role USING (qual) + * DROP POLICY name ON table FOR cmd + * + *****************************************************************************/ + + CreatePolicyStmt: + CREATE POLICY name ON qualified_name RowSecurityDefaultForCmd + RowSecurityDefaultToRole RowSecurityOptionalExpr + RowSecurityOptionalWithCheck + { + CreatePolicyStmt *n = makeNode(CreatePolicyStmt); + n->policy_name = $3; + n->table = $5; + n->cmd = $6; + n->roles = $7; + n->qual = $8; + n->with_check = $9; + $$ = (Node *) n; + } + ; + + AlterPolicyStmt: + ALTER POLICY name ON qualified_name RowSecurityOptionalToRole + RowSecurityOptionalExpr RowSecurityOptionalWithCheck + { + AlterPolicyStmt *n = makeNode(AlterPolicyStmt); + n->policy_name = $3; + n->table = $5; + n->roles = $6; + n->qual = $7; + n->with_check = $8; + $$ = (Node *) n; + } + ; + + DropPolicyStmt: + DROP POLICY name ON qualified_name + { + DropPolicyStmt *n = makeNode(DropPolicyStmt); + n->policy_name = $3; + n->table = $5; + n->missing_ok = FALSE; + $$ = (Node *) n; + } + | DROP POLICY IF_P EXISTS name ON qualified_name + { + DropPolicyStmt *n = makeNode(DropPolicyStmt); + n->policy_name = $5; + n->table = $7; + n->missing_ok = TRUE; + $$ = (Node *) n; + } + ; + + RowSecurityOptionalExpr: + USING '(' a_expr ')' { $$ = $3; } + | /* EMPTY */ { $$ = NULL; } + ; + + RowSecurityOptionalWithCheck: + WITH CHECK '(' a_expr ')' { $$ = $4; } + | /* EMPTY */ { $$ = NULL; } + ; + + RowSecurityDefaultToRole: + TO role_list { $$ = $2; } + | /* EMPTY */ { $$ = list_make1(makeString("public")); } + ; + + RowSecurityOptionalToRole: + TO role_list { $$ = $2; } + | /* EMPTY */ { $$ = NULL; } + ; + + RowSecurityDefaultForCmd: + FOR row_security_cmd { $$ = $2; } + | /* EMPTY */ { $$ = "all"; } + ; + + row_security_cmd: + ALL { $$ = "all"; } + | SELECT { $$ = "select"; } + | INSERT { $$ = "insert"; } + | UPDATE { $$ = "update"; } + | DELETE_P { $$ = "delete"; } + ; + + /***************************************************************************** + * * QUERIES : * CREATE TRIGGER ... * DROP TRIGGER ... *************** RenameStmt: ALTER AGGREGATE func_name ag *** 7240,7245 **** --- 7357,7382 ---- n->missing_ok = false; $$ = (Node *)n; } + | ALTER POLICY name ON qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_POLICY; + n->relation = $5; + n->subname = $3; + n->newname = $8; + n->missing_ok = false; + $$ = (Node *)n; + } + | ALTER POLICY IF_P EXISTS name ON qualified_name RENAME TO name + { + RenameStmt *n = makeNode(RenameStmt); + n->renameType = OBJECT_POLICY; + n->relation = $7; + n->subname = $5; + n->newname = $10; + n->missing_ok = true; + $$ = (Node *)n; + } | ALTER SCHEMA name RENAME TO name { RenameStmt *n = makeNode(RenameStmt); *************** unreserved_keyword: *** 13036,13041 **** --- 13173,13179 ---- | 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 cb65c05..bb27728 *** 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,1735 ---- * 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_policies(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 ...1ff91b9 *** a/src/backend/rewrite/rowsecurity.c --- b/src/backend/rewrite/rowsecurity.c *************** *** 0 **** --- 1,330 ---- + /* + * 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" + + static bool check_role_for_policy(RowSecurityPolicy *policy); + static List *pull_row_security_policies(CmdType cmd, Relation relation); + + /* 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_policies(Query* root, RangeTblEntry* rte, int rt_index) + { + List *rowsec_policies; + WithCheckOption *wco; + 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 any row-security policies and add the quals to the list of + * quals to be expanded by expand_security_quals. For with-check + * quals, add them to the Query's with-check-options list. + */ + rel = heap_open(rte->relid, NoLock); + + rowsec_policies = pull_row_security_policies(root->commandType, rel); + + if (rowsec_policies) + { + List *sec_quals = NIL; + List *with_check_quals = NIL; + ListCell *item; + + /* + * Extract the USING and WITH CHECK quals from each of the policies + * and add them to our lists. + */ + foreach(item, rowsec_policies) + { + RowSecurityPolicy *policy = (RowSecurityPolicy *) lfirst(item); + + if (policy->qual != NULL) + sec_quals = lcons(copyObject(policy->qual), sec_quals); + + if (policy->with_check_qual != NULL) + with_check_quals = + lcons(copyObject(policy->with_check_qual), + with_check_quals); + } + + /* + * If we end up with only sec_quals, then use those for + * with_check_quals also. + */ + if (with_check_quals == NIL) + with_check_quals = sec_quals; + + /* + * 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 *) sec_quals, 1, rt_index, 0); + ChangeVarNodes((Node *) with_check_quals, 1, rt_index, 0); + + /* + * If more than one security qual is returned, then they need to be + * OR'ed together. + */ + if (list_length(sec_quals) > 1) + sec_quals = list_make1(makeBoolExpr(OR_EXPR, sec_quals, -1)); + + /* + * If more than one WITH CHECK qual is returned, then they need to + * be OR'ed together. + */ + if (list_length(with_check_quals) > 1) + with_check_quals = + list_make1(makeBoolExpr(OR_EXPR, with_check_quals, -1)); + + /* + * For INSERT or UPDATE, we need to add the WITH CHECK quals to + * Query's withCheckOptions to verify the any new records pass the + * WITH CHECK policy (or USING policy, if no WITH CHECK policy + * exists). + */ + if ((root->commandType == CMD_INSERT + || root->commandType == CMD_UPDATE) + && with_check_quals != NIL) + { + wco = (WithCheckOption *) makeNode(WithCheckOption); + wco->viewname = RelationGetRelationName(rel); + wco->qual = (Node *) lfirst(list_head(with_check_quals)); + wco->cascaded = false; + root->withCheckOptions = lcons(wco, root->withCheckOptions); + } + + /* For SELECT, UPDATE, and DELETE, set the security quals */ + if (sec_quals != NIL && (root->commandType == CMD_SELECT + || root->commandType == CMD_UPDATE + || root->commandType == CMD_DELETE)) + rte->securityQuals = list_concat(sec_quals, rte->securityQuals); + } + + heap_close(rel, NoLock); + } + + /* + * Mark this query as having row security, so plancache can invalidate it + * when necessary (eg: role changes) + */ + root->hasRowSecurity = qualsAdded; + + return qualsAdded; + } + + /* + * pull_row_security_policies + * + * Returns the list of policies to be added for this relation, if any, based on + * the type of command, the user executing the query, and the row_security GUC. + * + * Handles permissions checking related to row security policies and BYPASSRLS. + * + * Also provides a hook for extensions to add their own policies. + * + * If RLS is enabled for the relation, row security is 'on' or 'force', and + * no policies are found, then a single 'default deny' policy is returned which + * consists of just 'false'. + */ + static List * + pull_row_security_policies(CmdType cmd, Relation relation) + { + List *policies = NIL; + + /* Nothing to do if the relation does not have RLS */ + if (!RelationGetForm(relation)->relhasrowsecurity) + return NIL; + + /* + * Check permissions + * + * If the relation has row level security enabled and the row_security GUC + * is off, then check if the user has rights to bypass RLS for this + * relation. Table owners can always bypass, as can any role with the + * BYPASSRLS capability. + */ + + /* + * If the role is the table owner or a superuser, then we bypass RLS + * unless row_security is set to 'force'. + */ + if (row_security != ROW_SECURITY_FORCE + && (GetUserId() == RelationGetForm(relation)->relowner || superuser())) + return NIL; + + /* + * If the row_security GUC is 'off' then check if the user has permission + * to bypass it. Note that we have already handled the case where the user + * is the table owner above. + */ + if (row_security == ROW_SECURITY_OFF) + { + if (has_bypassrls_privilege(GetUserId())) + /* OK to bypass */ + return NIL; + else + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("insufficient privilege to bypass row security."))); + } + + /* + * Row security is enabled for the relation and the row security GUC is + * either 'on' or 'force' here, so go ahead and add in any policies which + * exist on the table or which are pulled in from an extension. If no + * policies are discovered, then we will create a single 'default deny' + * policy. + * + * First pull any row-security policies defined in the PG catalog, which + * have been populated into relation->rsdesc for us already. We provide a + * hook below for extensions to add their own policies. + */ + if (relation->rsdesc) + { + ListCell *item; + RowSecurityPolicy *policy; + + foreach(item, relation->rsdesc->policies) + { + policy = (RowSecurityPolicy *) lfirst(item); + + /* Always add ALL policy if they exist. */ + if (policy->cmd == 0 && check_role_for_policy(policy)) + policies = lcons(policy, policies); + + /* Build the list of policies to return. */ + switch(cmd) + { + case CMD_SELECT: + if (policy->cmd == ACL_SELECT_CHR + && check_role_for_policy(policy)) + policies = lcons(policy, policies); + break; + case CMD_INSERT: + /* If INSERT then only need to add the WITH CHECK qual */ + if (policy->cmd == ACL_INSERT_CHR + && check_role_for_policy(policy)) + policies = lcons(policy, policies); + break; + case CMD_UPDATE: + if (policy->cmd == ACL_UPDATE_CHR + && check_role_for_policy(policy)) + policies = lcons(policy, policies); + break; + case CMD_DELETE: + if (policy->cmd == ACL_DELETE_CHR + && check_role_for_policy(policy)) + policies = lcons(policy, policies); + break; + default: + elog(ERROR, "unrecognized command type."); + break; + } + } + } + + /* + * Also, ask extensions whether they want to apply their own + * row-security policies. + */ + if (row_security_policy_hook) + { + List *temp; + + temp = (*row_security_policy_hook)(cmd, relation); + if (temp != NIL) + policies = lcons(temp, policies); + } + + /* + * If there are no policies found then we need to create a default + * deny policy which is always 'false' and return it. + */ + if (policies == NIL) + { + RowSecurityPolicy *policy; + + policy = palloc0(sizeof(RowSecurityPolicy)); + policy->rsecid = RelationGetRelid(relation); + policy->policy_name = pstrdup("Default deny policy"); + policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid, sizeof(bool), + BoolGetDatum(false), false, true); + + policies = list_make1(policy); + } + + return policies; + } + + /* + * check_role_for_policy - + * determines if the policy should be applied for the current role + */ + bool + check_role_for_policy(RowSecurityPolicy *policy) + { + int i; + Oid *roles = (Oid *) ARR_DATA_PTR(policy->roles); + + /* Quick fall-thru for policies applied to all roles */ + if (roles[0] == ACL_ID_PUBLIC) + return true; + + for (i = 0; i < ARR_DIMS(policy->roles)[0]; i++) + { + if (is_member_of_role(GetUserId(), roles[i])) + return true; + } + + return false; + } diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c new file mode 100644 index 40ac47f..c02a791 *** 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, *** 836,841 **** --- 837,852 ---- } 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, *************** AlterObjectTypeCommandTag(ObjectType obj *** 1623,1628 **** --- 1634,1642 ---- case OBJECT_OPFAMILY: tag = "ALTER OPERATOR FAMILY"; break; + case OBJECT_POLICY: + tag = "ALTER POLICY"; + break; case OBJECT_ROLE: tag = "ALTER ROLE"; break; *************** CreateCommandTag(Node *parsetree) *** 1944,1949 **** --- 1958,1966 ---- case OBJECT_OPFAMILY: tag = "DROP OPERATOR FAMILY"; break; + case OBJECT_POLICY: + tag = "DROP POLICY"; + break; default: tag = "???"; } *************** CreateCommandTag(Node *parsetree) *** 2437,2442 **** --- 2454,2469 ---- } 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/acl.c b/src/backend/utils/adt/acl.c new file mode 100644 index 38cd5b8..dc6eb2c *** a/src/backend/utils/adt/acl.c --- b/src/backend/utils/adt/acl.c *************** static AclMode convert_role_priv_string( *** 117,123 **** static AclResult pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode); static void RoleMembershipCacheCallback(Datum arg, int cacheid, uint32 hashvalue); - static Oid get_role_oid_or_public(const char *rolname); /* --- 117,122 ---- *************** get_role_oid(const char *rolname, bool m *** 5126,5132 **** * get_role_oid_or_public - As above, but return ACL_ID_PUBLIC if the * role name is "public". */ ! static Oid get_role_oid_or_public(const char *rolname) { if (strcmp(rolname, "public") == 0) --- 5125,5131 ---- * get_role_oid_or_public - As above, but return ACL_ID_PUBLIC if the * role name is "public". */ ! Oid get_role_oid_or_public(const char *rolname) { if (strcmp(rolname, "public") == 0) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c new file mode 100644 index e4d7b2c..16bb474 *** 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,2990 ---- /* 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 + || qkey->constr_queryno == RI_PLAN_CHECK_LOOKUPPK_FROM_PK + || qkey->constr_queryno == RI_PLAN_RESTRICT_DEL_CHECKREF + || qkey->constr_queryno == RI_PLAN_RESTRICT_UPD_CHECKREF) + 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/plancache.c b/src/backend/utils/cache/plancache.c new file mode 100644 index d03d3b3..33b594d *** a/src/backend/utils/cache/plancache.c --- b/src/backend/utils/cache/plancache.c *************** *** 53,64 **** --- 53,66 ---- #include "catalog/namespace.h" #include "executor/executor.h" #include "executor/spi.h" + #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "optimizer/cost.h" #include "optimizer/planmain.h" #include "optimizer/prep.h" #include "parser/analyze.h" #include "parser/parsetree.h" + #include "rewrite/rowsecurity.h" #include "storage/lmgr.h" #include "tcop/pquery.h" #include "tcop/utility.h" *************** CreateCachedPlan(Node *raw_parse_tree, *** 151,156 **** --- 153,160 ---- CachedPlanSource *plansource; MemoryContext source_context; MemoryContext oldcxt; + Oid user_id; + int security_context; Assert(query_string != NULL); /* required as of 8.4 */ *************** CreateCachedPlan(Node *raw_parse_tree, *** 173,178 **** --- 177,184 ---- */ oldcxt = MemoryContextSwitchTo(source_context); + GetUserIdAndSecContext(&user_id, &security_context); + plansource = (CachedPlanSource *) palloc0(sizeof(CachedPlanSource)); plansource->magic = CACHEDPLANSOURCE_MAGIC; plansource->raw_parse_tree = copyObject(raw_parse_tree); *************** CreateCachedPlan(Node *raw_parse_tree, *** 201,206 **** --- 207,217 ---- plansource->generic_cost = -1; plansource->total_custom_cost = 0; plansource->num_custom_plans = 0; + plansource->has_rls = false; + plansource->targetRelId = InvalidOid; + plansource->rowSecurityDisabled + = (security_context & SECURITY_ROW_LEVEL_DISABLED) != 0; + plansource->planUserId = InvalidOid; MemoryContextSwitchTo(oldcxt); *************** CompleteCachedPlan(CachedPlanSource *pla *** 371,377 **** */ extract_query_dependencies((Node *) querytree_list, &plansource->relationOids, ! &plansource->invalItems); /* * Also save the current search_path in the query_context. (This --- 382,390 ---- */ extract_query_dependencies((Node *) querytree_list, &plansource->relationOids, ! &plansource->invalItems, ! &plansource->has_rls, ! &plansource->targetRelId); /* * Also save the current search_path in the query_context. (This *************** RevalidateCachedQuery(CachedPlanSource * *** 565,570 **** --- 578,587 ---- return NIL; } + /* If this is a new cached plan, then set the user id it was planned by. */ + if (!OidIsValid(plansource->planUserId)) + plansource->planUserId = GetUserId(); + /* * If the query is currently valid, we should have a saved search_path --- * check to see if that matches the current environment. If not, we want *************** RevalidateCachedQuery(CachedPlanSource * *** 582,587 **** --- 599,626 ---- } } + if (plansource->is_valid + && !plansource->rowSecurityDisabled + && (plansource->targetRelId != InvalidOid)) + { + Relation rel = RelationIdGetRelation(plansource->targetRelId); + const char *rls_option = GetConfigOption("row_security", true, false); + + if (rel->rd_rel->relhasrowsecurity) + { + /* + * If previously planed with or without RLS and the row_security GUC + * setting was changed since planned or if the current user has + * changed, then the cached plan needs to be invalidated. + */ + if (plansource->has_rls != (strcmp(rls_option, "on") == 0) + || (plansource->planUserId != GetUserId())) + plansource->is_valid = false; + } + + RelationClose(rel); + } + /* * If the query is currently valid, acquire locks on the referenced * objects; then check again. We need to do it this way to cover the race *************** RevalidateCachedQuery(CachedPlanSource * *** 723,729 **** */ extract_query_dependencies((Node *) qlist, &plansource->relationOids, ! &plansource->invalItems); /* * Also save the current search_path in the query_context. (This should --- 762,770 ---- */ extract_query_dependencies((Node *) qlist, &plansource->relationOids, ! &plansource->invalItems, ! &plansource->has_rls, ! &plansource->targetRelId); /* * Also save the current search_path in the query_context. (This should diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c new file mode 100644 index b6f03df..9d34ac9 *** a/src/backend/utils/cache/relcache.c --- b/src/backend/utils/cache/relcache.c *************** *** 55,60 **** --- 55,61 ---- #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 **** --- 967,977 ---- 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 **** --- 1942,1949 ---- 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 **** --- 3342,3355 ---- 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) *** 4706,4711 **** --- 4722,4728 ---- 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 af667f5..f6bf64b *** a/src/backend/utils/misc/guc.c --- b/src/backend/utils/misc/guc.c *************** *** 61,66 **** --- 61,67 ---- #include "replication/syncrep.h" #include "replication/walreceiver.h" #include "replication/walsender.h" + #include "rewrite/rowsecurity.h" #include "storage/bufmgr.h" #include "storage/dsm_impl.h" #include "storage/standby.h" *************** static const struct config_enum_entry hu *** 401,406 **** --- 402,424 ---- }; /* + * Although only "on", "off", and "force" are documented, we + * accept all the likely variants of "on" and "off". + */ + static const struct config_enum_entry row_security_options[] = { + {"on", ROW_SECURITY_ON, false}, + {"off", ROW_SECURITY_OFF, false}, + {"force", ROW_SECURITY_FORCE, 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 **** --- 474,481 ---- int tcp_keepalives_interval; int tcp_keepalives_count; + int row_security = true; + /* * 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 *** 3507,3512 **** --- 3527,3542 ---- 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.") + }, + &row_security, + ROW_SECURITY_ON, row_security_options, + NULL, NULL, NULL + }, + /* End-of-list marker */ { {NULL, 0, 0, NULL, NULL}, NULL, 0, NULL, NULL, NULL, NULL diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample new file mode 100644 index df98b02..8fdb7a9 *** 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.h b/src/bin/pg_dump/pg_backup.h new file mode 100644 index 25780cf..921bc1b *** a/src/bin/pg_dump/pg_backup.h --- b/src/bin/pg_dump/pg_backup.h *************** typedef struct _restoreOptions *** 150,155 **** --- 150,156 ---- bool single_txn; bool *idWanted; /* array showing which dump IDs to emit */ + int enable_row_security; } RestoreOptions; typedef void (*SetupWorkerPtr) (Archive *AH, RestoreOptions *ropt); 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 ded9135..64f8425 *** a/src/bin/pg_dump/pg_backup_archiver.c --- b/src/bin/pg_dump/pg_backup_archiver.c *************** RestoreArchive(Archive *AHX) *** 374,379 **** --- 374,387 ---- } /* + * Enable row-security if necessary. + */ + if (!ropt->enable_row_security) + ahprintf(AH, "SET row_security = off\n"); + else + ahprintf(AH, "SET row_security = on\n"); + + /* * Establish important parameter values right away. */ _doSetFixedOutputState(AH); *************** _printTocEntry(ArchiveHandle *AH, TocEnt *** 3242,3247 **** --- 3250,3256 ---- 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 c084ee9..dcf6765 *** 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 enable_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) *** 345,350 **** --- 347,353 ---- {"column-inserts", no_argument, &column_inserts, 1}, {"disable-dollar-quoting", no_argument, &disable_dollar_quoting, 1}, {"disable-triggers", no_argument, &disable_triggers, 1}, + {"enable-row-security", no_argument, &enable_row_security, 1}, {"exclude-table-data", required_argument, NULL, 4}, {"if-exists", no_argument, &if_exists, 1}, {"inserts", no_argument, &dump_inserts, 1}, *************** main(int argc, char **argv) *** 825,830 **** --- 828,834 ---- ropt->noTablespace = outputNoTablespaces; ropt->disable_triggers = disable_triggers; ropt->use_setsessauth = use_setsessauth; + ropt->enable_row_security = enable_row_security; if (compressLevel == -1) ropt->compression = 0; *************** help(const char *progname) *** 897,902 **** --- 901,907 ---- printf(_(" --column-inserts dump data as INSERT commands with column names\n")); printf(_(" --disable-dollar-quoting disable dollar quoting, use SQL standard quoting\n")); printf(_(" --disable-triggers disable triggers during data-only restore\n")); + printf(_(" --enable-row-security enable row level security\n")); printf(_(" --exclude-table-data=TABLE do NOT dump data for the named table(s)\n")); printf(_(" --if-exists use IF EXISTS when dropping objects\n")); printf(_(" --inserts dump data as INSERT commands, rather than COPY\n")); *************** setup_connection(Archive *AH, const char *** 1050,1055 **** --- 1055,1068 ---- else AH->sync_snapshot_id = get_synchronized_snapshot(AH); } + + if (AH->remoteVersion >= 90500) + { + if (enable_row_security) + ExecuteSqlStatement(AH, "SET row_security TO ON"); + else + ExecuteSqlStatement(AH, "SET row_security TO OFF"); + } } static void *************** dumpBlobs(Archive *fout, void *arg) *** 2757,2762 **** --- 2770,2922 ---- 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_rsecroles; + 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, " + "array_to_string(ARRAY(SELECT rolname from pg_roles WHERE oid = ANY(s.rsecroles)), ', ') AS rsecroles, " + "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_rsecroles = PQfnumber(res, "rsecroles"); + 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)); + if (PQgetisnull(res, j, i_rseccmd)) + rsinfo[j].rseccmd = NULL; + else + rsinfo[j].rseccmd = pg_strdup(PQgetvalue(res, j, i_rseccmd)); + + rsinfo[j].rsecroles = pg_strdup(PQgetvalue(res, j, i_rsecroles)); + 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 (!rsinfo->rseccmd) + cmd = "ALL"; + else if (strcmp(rsinfo->rseccmd, "r") == 0) + cmd = "SELECT"; + else if (strcmp(rsinfo->rseccmd, "a") == 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 ", + rsinfo->rsecpolname, fmtId(tbinfo->dobj.name), cmd); + + if (strcmp(rsinfo->rsecroles, "") == 0) + appendPQExpBuffer(query, "TO PUBLIC "); + else + appendPQExpBuffer(query, "TO %s ", rsinfo->rsecroles); + + appendPQExpBuffer(query, "USING %s\n", rsinfo->rsecqual); + + appendPQExpBuffer(delqry, "DROP POLICY %s ON %s;\n", + rsinfo->rsecpolname, fmtId(tbinfo->dobj.name)); + + 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) *** 4287,4292 **** --- 4447,4453 ---- 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) *** 4328,4334 **** * we cannot correctly identify inherited columns, owned sequences, etc. */ ! if (fout->remoteVersion >= 90400) { /* * Left join to pick up dependency info linking sequences to their --- 4489,4536 ---- * we cannot correctly identify inherited columns, owned sequences, etc. */ ! if (fout->remoteVersion >= 90500) ! { ! /* ! * 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, " ! "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 *************** getTables(Archive *fout, int *numTables) *** 4340,4345 **** --- 4542,4548 ---- "(%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) *** 4380,4385 **** --- 4583,4589 ---- "(%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) *** 4420,4425 **** --- 4624,4630 ---- "(%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) *** 4458,4463 **** --- 4663,4669 ---- "(%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) *** 4495,4500 **** --- 4701,4707 ---- "(%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) *** 4532,4537 **** --- 4739,4745 ---- "(%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) *** 4569,4574 **** --- 4777,4783 ---- "(%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) *** 4605,4610 **** --- 4814,4820 ---- "(%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) *** 4637,4642 **** --- 4847,4853 ---- "(%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) *** 4664,4669 **** --- 4875,4881 ---- "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) *** 4701,4706 **** --- 4913,4919 ---- "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) *** 4748,4753 **** --- 4961,4967 ---- 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) *** 4799,4804 **** --- 5013,5019 ---- 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 *** 7930,7935 **** --- 8145,8153 ---- 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 * *** 15332,15337 **** --- 15550,15556 ---- 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..092542f *** 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,503 ---- char *blobacl; } BlobInfo; + typedef struct _rowSecurityInfo + { + DumpableObject dobj; + TableInfo *rstable; + char *rsecpolname; + char *rseccmd; + char *rsecroles; + 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 **** --- 589,594 ---- 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 b2b3e6f..c25ea85 *** 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, " + "false 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, " + "false 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, " + "false 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, " + "false 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/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c new file mode 100644 index fdfdc19..1c1b80f *** a/src/bin/pg_dump/pg_restore.c --- b/src/bin/pg_dump/pg_restore.c *************** main(int argc, char **argv) *** 70,75 **** --- 70,76 ---- Archive *AH; char *inputFileSpec; static int disable_triggers = 0; + static int enable_row_security = 0; static int if_exists = 0; static int no_data_for_failed_tables = 0; static int outputNoTablespaces = 0; *************** main(int argc, char **argv) *** 111,116 **** --- 112,118 ---- * the following options don't have an equivalent short option letter */ {"disable-triggers", no_argument, &disable_triggers, 1}, + {"enable-row-security", no_argument, &enable_row_security, 1}, {"if-exists", no_argument, &if_exists, 1}, {"no-data-for-failed-tables", no_argument, &no_data_for_failed_tables, 1}, {"no-tablespaces", no_argument, &outputNoTablespaces, 1}, *************** main(int argc, char **argv) *** 333,338 **** --- 335,341 ---- } opts->disable_triggers = disable_triggers; + opts->enable_row_security = enable_row_security; opts->noDataForFailedTables = no_data_for_failed_tables; opts->noTablespace = outputNoTablespaces; opts->use_setsessauth = use_setsessauth; *************** usage(const char *progname) *** 460,465 **** --- 463,469 ---- printf(_(" -x, --no-privileges skip restoration of access privileges (grant/revoke)\n")); printf(_(" -1, --single-transaction restore as a single transaction\n")); printf(_(" --disable-triggers disable triggers during data-only restore\n")); + printf(_(" --enable-row-security enable row level security\n")); printf(_(" --if-exists use IF EXISTS when dropping objects\n")); printf(_(" --no-data-for-failed-tables do not restore data of tables that could not be\n" " created\n")); diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c new file mode 100644 index 282cd43..5ba61d9 *** a/src/bin/psql/describe.c --- b/src/bin/psql/describe.c *************** permissionsList(const char *pattern) *** 742,748 **** PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; ! static const bool translate_columns[] = {false, false, true, false, false}; initPQExpBuffer(&buf); --- 742,748 ---- PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; ! static const bool translate_columns[] = {false, false, true, false, false, false}; initPQExpBuffer(&buf); *************** permissionsList(const char *pattern) *** 778,784 **** " FROM pg_catalog.pg_attribute a\n" " WHERE attrelid = c.oid AND NOT attisdropped AND attacl IS NOT NULL\n" " ), E'\\n') AS \"%s\"", ! gettext_noop("Column access privileges")); appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_class c\n" " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" --- 778,815 ---- " FROM pg_catalog.pg_attribute a\n" " WHERE attrelid = c.oid AND NOT attisdropped AND attacl IS NOT NULL\n" " ), E'\\n') AS \"%s\"", ! gettext_noop("Column privileges")); ! ! if (pset.sversion >= 90500) ! appendPQExpBuffer(&buf, ! ",\n pg_catalog.array_to_string(ARRAY(\n" ! " SELECT rsecpolname\n" ! " || CASE WHEN rseccmd IS NOT NULL THEN\n" ! " E' (' || rseccmd || E')'\n" ! " ELSE E':' \n" ! " END\n" ! " || CASE WHEN rs.rsecqual IS NOT NULL THEN\n" ! " E'\\n (u): ' || pg_catalog.pg_get_expr(rsecqual, rsecrelid)\n" ! " ELSE E''\n" ! " END\n" ! " || CASE WHEN rsecwithcheck IS NOT NULL THEN\n" ! " E'\\n (c): ' || pg_catalog.pg_get_expr(rsecwithcheck, rsecrelid)\n" ! " ELSE E''\n" ! " END" ! " || CASE WHEN rs.rsecroles <> '{0}' THEN\n" ! " E'\\n to: ' || pg_catalog.array_to_string(\n" ! " ARRAY(\n" ! " SELECT rolname\n" ! " FROM pg_catalog.pg_roles\n" ! " WHERE oid = ANY (rs.rsecroles)\n" ! " ORDER BY 1\n" ! " ), E', ')\n" ! " ELSE E''\n" ! " END\n" ! " FROM pg_catalog.pg_rowsecurity rs\n" ! " WHERE rsecrelid = c.oid), E'\\n')\n" ! " AS \"%s\"", ! gettext_noop("Policies")); appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_class c\n" " LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace\n" *************** describeOneTableDetails(const char *sche *** 1173,1178 **** --- 1204,1210 ---- bool hasindex; bool hasrules; bool hastriggers; + bool hasrowsecurity; bool hasoids; Oid tablespace; char *reloptions; *************** describeOneTableDetails(const char *sche *** 1194,1204 **** initPQExpBuffer(&tmpbuf); /* Get general table info */ ! if (pset.sversion >= 90400) { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, " "c.relpersistence, c.relreplident\n" --- 1226,1253 ---- initPQExpBuffer(&tmpbuf); /* Get general table info */ ! if (pset.sversion >= 90500) { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, c.relhasrowsecurity, c.relhasoids, " ! "%s, c.reltablespace, " ! "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, " ! "c.relpersistence, c.relreplident\n" ! "FROM pg_catalog.pg_class c\n " ! "LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n" ! "WHERE c.oid = '%s';", ! (verbose ? ! "pg_catalog.array_to_string(c.reloptions || " ! "array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x), ', ')\n" ! : "''"), ! oid); ! } ! else if (pset.sversion >= 90400) ! { ! printfPQExpBuffer(&buf, ! "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, false, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, " "c.relpersistence, c.relreplident\n" *************** describeOneTableDetails(const char *sche *** 1215,1221 **** { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, " "c.relpersistence\n" --- 1264,1270 ---- { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, false, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, " "c.relpersistence\n" *************** describeOneTableDetails(const char *sche *** 1232,1238 **** { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END\n" "FROM pg_catalog.pg_class c\n " --- 1281,1287 ---- { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, false, c.relhasoids, " "%s, c.reltablespace, " "CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END\n" "FROM pg_catalog.pg_class c\n " *************** describeOneTableDetails(const char *sche *** 1248,1254 **** { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, c.relhasoids, " "%s, c.reltablespace\n" "FROM pg_catalog.pg_class c\n " "LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n" --- 1297,1303 ---- { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " ! "c.relhastriggers, false, c.relhasoids, " "%s, c.reltablespace\n" "FROM pg_catalog.pg_class c\n " "LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)\n" *************** describeOneTableDetails(const char *sche *** 1263,1269 **** { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, relhasoids, " "%s, reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", (verbose ? --- 1312,1318 ---- { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, false, relhasoids, " "%s, reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", (verbose ? *************** describeOneTableDetails(const char *sche *** 1274,1280 **** { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, relhasoids, " "'', reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); --- 1323,1329 ---- { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, false, relhasoids, " "'', reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); *************** describeOneTableDetails(const char *sche *** 1283,1289 **** { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, relhasoids, " "'', ''\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); --- 1332,1338 ---- { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " ! "reltriggers <> 0, false, relhasoids, " "'', ''\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); *************** describeOneTableDetails(const char *sche *** 1306,1323 **** tableinfo.hasindex = strcmp(PQgetvalue(res, 0, 2), "t") == 0; tableinfo.hasrules = strcmp(PQgetvalue(res, 0, 3), "t") == 0; tableinfo.hastriggers = strcmp(PQgetvalue(res, 0, 4), "t") == 0; ! tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 5), "t") == 0; tableinfo.reloptions = (pset.sversion >= 80200) ? ! pg_strdup(PQgetvalue(res, 0, 6)) : NULL; tableinfo.tablespace = (pset.sversion >= 80000) ? ! atooid(PQgetvalue(res, 0, 7)) : 0; tableinfo.reloftype = (pset.sversion >= 90000 && ! strcmp(PQgetvalue(res, 0, 8), "") != 0) ? ! pg_strdup(PQgetvalue(res, 0, 8)) : NULL; tableinfo.relpersistence = (pset.sversion >= 90100) ? ! *(PQgetvalue(res, 0, 9)) : 0; tableinfo.relreplident = (pset.sversion >= 90400) ? ! *(PQgetvalue(res, 0, 10)) : 'd'; PQclear(res); res = NULL; --- 1355,1373 ---- tableinfo.hasindex = strcmp(PQgetvalue(res, 0, 2), "t") == 0; tableinfo.hasrules = strcmp(PQgetvalue(res, 0, 3), "t") == 0; tableinfo.hastriggers = strcmp(PQgetvalue(res, 0, 4), "t") == 0; ! tableinfo.hasrowsecurity = strcmp(PQgetvalue(res, 0, 5), "t") == 0; ! tableinfo.hasoids = strcmp(PQgetvalue(res, 0, 6), "t") == 0; tableinfo.reloptions = (pset.sversion >= 80200) ? ! pg_strdup(PQgetvalue(res, 0, 7)) : NULL; tableinfo.tablespace = (pset.sversion >= 80000) ? ! atooid(PQgetvalue(res, 0, 8)) : 0; tableinfo.reloftype = (pset.sversion >= 90000 && ! strcmp(PQgetvalue(res, 0, 9), "") != 0) ? ! pg_strdup(PQgetvalue(res, 0, 9)) : NULL; tableinfo.relpersistence = (pset.sversion >= 90100) ? ! *(PQgetvalue(res, 0, 10)) : 0; tableinfo.relreplident = (pset.sversion >= 90400) ? ! *(PQgetvalue(res, 0, 11)) : 'd'; PQclear(res); res = NULL; *************** describeOneTableDetails(const char *sche *** 1948,1953 **** --- 1998,2058 ---- PQclear(result); } + + if (pset.sversion >= 90500) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_get_expr(rs.rsecqual, c.oid) as \"%s\"", + gettext_noop("Row-security")); + if (verbose && pset.sversion >= 90500) + appendPQExpBuffer(&buf, + "\n LEFT JOIN pg_rowsecurity rs ON rs.rsecrelid = c.oid"); + + /* print any row-level policies */ + if (tableinfo.hasrowsecurity) + { + printfPQExpBuffer(&buf, + "SELECT rs.rsecpolname,\n" + "CASE WHEN rs.rsecroles = '{0}' THEN NULL ELSE array(select rolname from pg_authid where oid = any (rs.rsecroles) order by 1) END,\n" + "pg_catalog.pg_get_expr(rs.rsecqual, rs.rsecrelid),\n" + "rs.rseccmd AS cmd\n" + "FROM pg_catalog.pg_rowsecurity rs\n" + "WHERE rs.rsecrelid = '%s' ORDER BY 1;", + oid); + result = PSQLexec(buf.data, false); + if (!result) + goto error_return; + else + tuples = PQntuples(result); + + if (tuples > 0) + { + printTableAddFooter(&cont, _("Policies:")); + for (i = 0; i < tuples; i++) + { + if (PQgetisnull(result, i, 3)) + printfPQExpBuffer(&buf, " POLICY \"%s\" EXPRESSION %s", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 2)); + else + printfPQExpBuffer(&buf, " POLICY \"%s\" (%s) EXPRESSION %s", + PQgetvalue(result, i, 0), + PQgetvalue(result, i, 3), + PQgetvalue(result, i, 2)); + + printTableAddFooter(&cont, buf.data); + + if (!PQgetisnull(result, i, 1)) + { + printfPQExpBuffer(&buf, " APPLIED TO %s", + PQgetvalue(result, i, 1)); + + printTableAddFooter(&cont, buf.data); + } + } + } + PQclear(result); + } + /* print rules */ if (tableinfo.hasrules && tableinfo.relkind != 'm') { *************** describeRoles(const char *pattern, bool *** 2529,2534 **** --- 2634,2644 ---- 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, diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h new file mode 100644 index e1b62a5..e0875b7 *** a/src/include/catalog/catversion.h --- b/src/include/catalog/catversion.h *************** *** 53,58 **** */ /* yyyymmddN */ ! #define CATALOG_VERSION_NO 201408281 #endif --- 53,58 ---- */ /* yyyymmddN */ ! #define CATALOG_VERSION_NO 201409021 #endif 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..870692c *** 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, 3257, on pg_rowsecurity using btree(oid oid_ops)); + #define RowSecurityOidIndexId 3257 + + DECLARE_UNIQUE_INDEX(pg_rowsecurity_polname_relid_index, 3258, on pg_rowsecurity using btree(rsecrelid oid_ops, rsecpolname name_ops)); + #define RowSecurityRelidPolnameIndexId 3258 + /* 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 ...0b882ce *** a/src/include/catalog/pg_rowsecurity.h --- b/src/include/catalog/pg_rowsecurity.h *************** *** 0 **** --- 1,53 ---- + /* + * 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" + + /* ---------------- + * pg_rowsecurity definition. cpp turns this into + * typedef struct FormData_pg_rowsecurity + * ---------------- + */ + #define RowSecurityRelationId 3256 + + CATALOG(pg_rowsecurity,3256) + { + NameData rsecpolname; /* Policy name. */ + Oid rsecrelid; /* Oid of the relation with policy. */ + char rseccmd; /* One of ACL_*_CHR, or NULL for all */ + + #ifdef CATALOG_VARLEN + Oid rsecroles[1] /* Roles associated with policy */ + pg_node_tree rsecqual; /* Policy quals. */ + pg_node_tree rsecwithcheck; /* WITH CHECK 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 6 + #define Anum_pg_rowsecurity_rsecpolname 1 + #define Anum_pg_rowsecurity_rsecrelid 2 + #define Anum_pg_rowsecurity_rseccmd 3 + #define Anum_pg_rowsecurity_rsecroles 4 + #define Anum_pg_rowsecurity_rsecqual 5 + #define Anum_pg_rowsecurity_rsecwithcheck 6 + + #endif /* PG_ROWSECURITY_H */ diff --git a/src/include/commands/policy.h b/src/include/commands/policy.h new file mode 100644 index ...e4051fc *** a/src/include/commands/policy.h --- b/src/include/commands/policy.h *************** *** 0 **** --- 1,32 ---- + /*------------------------------------------------------------------------- + * + * 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); + + Oid get_relation_policy_oid(Oid relid, const char *policy_name, bool missing_ok); + Oid rename_policy(RenameStmt *stmt); + + + #endif /* POLICY_H */ diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h new file mode 100644 index 3807955..2ba9885 *** 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 a031b88..8eb6030 *** 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 d2c0b29..ed494e9 *** a/src/include/nodes/parsenodes.h --- b/src/include/nodes/parsenodes.h *************** typedef struct Query *** 120,125 **** --- 120,126 ---- bool hasRecursive; /* WITH RECURSIVE was specified */ bool hasModifyingCTE; /* has INSERT/UPDATE/DELETE in WITH */ bool hasForUpdate; /* FOR [KEY] UPDATE/SHARE was specified */ + bool hasRowSecurity; /* Row-security policy is applied */ List *cteList; /* WITH list (of CommonTableExpr's) */ *************** typedef enum ObjectType *** 1224,1229 **** --- 1225,1231 ---- OBJECT_OPCLASS, OBJECT_OPERATOR, OBJECT_OPFAMILY, + OBJECT_POLICY, OBJECT_ROLE, OBJECT_RULE, OBJECT_SCHEMA, *************** typedef enum AlterTableType *** 1333,1338 **** --- 1335,1342 ---- AT_AddOf, /* OF */ AT_DropOf, /* NOT OF */ AT_ReplicaIdentity, /* REPLICA IDENTITY */ + AT_EnableRowSecurity, /* ENABLE ROW SECURITY */ + AT_DisableRowSecurity, /* DISABLE ROW SECURITY */ AT_GenericOptions /* OPTIONS (...) */ } AlterTableType; *************** typedef struct ImportForeignSchemaStmt *** 1855,1860 **** --- 1859,1905 ---- 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 */ + List *roles; /* the roles associated with the policy */ + Node *qual; /* the policy's condition */ + Node *with_check; /* the policy's WITH CHECK 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 */ + List *roles; /* the roles associated with the policy */ + Node *qual; /* the policy's condition */ + Node *with_check; /* the policy's WITH CHECK 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 */ + bool missing_ok; /* skip error if policy is missing. */ + } DropPolicyStmt; + /* ---------------------- * Create TRIGGER Statement * ---------------------- diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h new file mode 100644 index 3b9c683..c03e431 *** a/src/include/nodes/plannodes.h --- b/src/include/nodes/plannodes.h *************** typedef struct PlannedStmt *** 67,72 **** --- 67,76 ---- List *invalItems; /* other dependencies, as PlanInvalItems */ int nParamExec; /* number of PARAM_EXEC Params used */ + + bool has_rls; /* row-security applied? */ + + Oid targetRelId; /* Oid of the target relation */ } PlannedStmt; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/nodes/relation.h b/src/include/nodes/relation.h new file mode 100644 index dacbe9c..1e49331 *** a/src/include/nodes/relation.h --- b/src/include/nodes/relation.h *************** typedef struct PlannerGlobal *** 99,104 **** --- 99,108 ---- Index lastRowMarkId; /* highest PlanRowMark ID assigned */ bool transientPlan; /* redo plan when TransactionXmin changes? */ + + bool has_rls; /* row-security is applied? */ + + Oid targetRelId; /* Oid of the target relation */ } PlannerGlobal; /* macro for fetching the Plan associated with a SubPlan node */ diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h new file mode 100644 index 4504250..3318731 *** a/src/include/optimizer/planmain.h --- b/src/include/optimizer/planmain.h *************** extern void set_sa_opfuncid(ScalarArrayO *** 135,140 **** extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void extract_query_dependencies(Node *query, List **relationOids, ! List **invalItems); #endif /* PLANMAIN_H */ --- 135,142 ---- extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void extract_query_dependencies(Node *query, List **relationOids, ! List **invalItems, ! bool *hasRowSecurity, ! Oid *targetRelId); #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h new file mode 100644 index 17888ad..3c8c1b9 *** a/src/include/parser/kwlist.h --- b/src/include/parser/kwlist.h *************** PG_KEYWORD("passing", PASSING, UNRESERVE *** 284,289 **** --- 284,290 ---- 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 ...e7adbeb *** a/src/include/rewrite/rowsecurity.h --- b/src/include/rewrite/rowsecurity.h *************** *** 0 **** --- 1,55 ---- + /* ------------------------------------------------------------------------- + * + * 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/array.h" + + typedef struct RowSecurityPolicy + { + Oid rsecid; + char *policy_name; + char cmd; + ArrayType *roles; + Expr *qual; + Expr *with_check_qual; + bool hassublinks; + } RowSecurityPolicy; + + typedef struct RowSecurityDesc + { + 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 RowSecurityConfigType + { + ROW_SECURITY_OFF, + ROW_SECURITY_ON, + ROW_SECURITY_FORCE + } RowSecurityConfigType; + + typedef List *(*row_security_policy_hook_type)(CmdType cmdtype, + Relation relation); + + extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook; + + extern bool prepend_row_security_policies(Query* root, RangeTblEntry* rte, + int rt_index); + + #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..a8e3164 *** a/src/include/utils/acl.h --- b/src/include/utils/acl.h *************** extern bool is_member_of_role_nosuper(Oi *** 228,233 **** --- 228,234 ---- extern bool is_admin_of_role(Oid member, Oid role); extern void check_is_member_of_role(Oid member, Oid role); extern Oid get_role_oid(const char *rolname, bool missing_ok); + extern Oid get_role_oid_or_public(const char *rolname); extern void select_best_grantor(Oid roleId, AclMode privileges, const Acl *acl, Oid ownerId, *************** extern bool pg_foreign_server_ownercheck *** 326,330 **** --- 327,332 ---- 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/plancache.h b/src/include/utils/plancache.h new file mode 100644 index cfbfaa2..db39e00 *** a/src/include/utils/plancache.h --- b/src/include/utils/plancache.h *************** typedef struct CachedPlanSource *** 93,98 **** --- 93,99 ---- List *invalItems; /* other dependencies, as PlanInvalItems */ struct OverrideSearchPath *search_path; /* search_path used for * parsing and planning */ + Oid planUserId; /* User-id that the plan depends on */ MemoryContext query_context; /* context holding the above, or NULL */ /* If we have a generic plan, this is a reference-counted link to it: */ struct CachedPlan *gplan; /* generic plan, or NULL if not valid */ *************** typedef struct CachedPlanSource *** 108,113 **** --- 109,117 ---- double generic_cost; /* cost of generic plan, or -1 if not known */ double total_custom_cost; /* total cost of custom plans so far */ int num_custom_plans; /* number of plans included in total */ + bool has_rls; /* planned with row-security? */ + Oid targetRelId; /* Oid of the target relation */ + bool rowSecurityDisabled; /* is row-security disabled? */ } CachedPlanSource; /* diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h new file mode 100644 index 37b6cbb..198b98f *** a/src/include/utils/rel.h --- b/src/include/utils/rel.h *************** *** 21,26 **** --- 21,27 ---- #include "fmgr.h" #include "nodes/bitmapset.h" #include "rewrite/prs2lock.h" + #include "rewrite/rowsecurity.h" #include "storage/block.h" #include "storage/relfilenode.h" #include "utils/relcache.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/dependency.out b/src/test/regress/expected/dependency.out new file mode 100644 index bcff8dd..09b0212 *** a/src/test/regress/expected/dependency.out --- b/src/test/regress/expected/dependency.out *************** CREATE TABLE deptest (a serial primary k *** 63,83 **** GRANT ALL ON deptest1 TO regression_user2; RESET SESSION AUTHORIZATION; \z deptest1 ! Access privileges ! Schema | Name | Type | Access privileges | Column access privileges ! --------+----------+-------+--------------------------------------------------+-------------------------- ! public | deptest1 | table | regression_user0=arwdDxt/regression_user0 +| ! | | | regression_user1=a*r*w*d*D*x*t*/regression_user0+| ! | | | regression_user2=arwdDxt/regression_user1 | (1 row) DROP OWNED BY regression_user1; -- all grants revoked \z deptest1 ! Access privileges ! Schema | Name | Type | Access privileges | Column access privileges ! --------+----------+-------+-------------------------------------------+-------------------------- ! public | deptest1 | table | regression_user0=arwdDxt/regression_user0 | (1 row) -- table was dropped --- 63,83 ---- GRANT ALL ON deptest1 TO regression_user2; RESET SESSION AUTHORIZATION; \z deptest1 ! Access privileges ! Schema | Name | Type | Access privileges | Column privileges | Policies ! --------+----------+-------+--------------------------------------------------+-------------------+---------- ! public | deptest1 | table | regression_user0=arwdDxt/regression_user0 +| | ! | | | regression_user1=a*r*w*d*D*x*t*/regression_user0+| | ! | | | regression_user2=arwdDxt/regression_user1 | | (1 row) DROP OWNED BY regression_user1; -- all grants revoked \z deptest1 ! Access privileges ! Schema | Name | Type | Access privileges | Column privileges | Policies ! --------+----------+-------+-------------------------------------------+-------------------+---------- ! public | deptest1 | table | regression_user0=arwdDxt/regression_user0 | | (1 row) -- table was dropped diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out new file mode 100644 index 1675075..5359dd8 *** a/src/test/regress/expected/privileges.out --- b/src/test/regress/expected/privileges.out *************** grant select on dep_priv_test to regress *** 1425,1463 **** set session role regressuser4; grant select on dep_priv_test to regressuser5; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column access privileges ! --------+---------------+-------+-----------------------------------+-------------------------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| ! | | | regressuser2=r*/regressuser1 +| ! | | | regressuser3=r*/regressuser1 +| ! | | | regressuser4=r*/regressuser2 +| ! | | | regressuser4=r*/regressuser3 +| ! | | | regressuser5=r/regressuser4 | (1 row) set session role regressuser2; revoke select on dep_priv_test from regressuser4 cascade; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column access privileges ! --------+---------------+-------+-----------------------------------+-------------------------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| ! | | | regressuser2=r*/regressuser1 +| ! | | | regressuser3=r*/regressuser1 +| ! | | | regressuser4=r*/regressuser3 +| ! | | | regressuser5=r/regressuser4 | (1 row) set session role regressuser3; revoke select on dep_priv_test from regressuser4 cascade; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column access privileges ! --------+---------------+-------+-----------------------------------+-------------------------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| ! | | | regressuser2=r*/regressuser1 +| ! | | | regressuser3=r*/regressuser1 | (1 row) set session role regressuser1; --- 1425,1463 ---- set session role regressuser4; grant select on dep_priv_test to regressuser5; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column privileges | Policies ! --------+---------------+-------+-----------------------------------+-------------------+---------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| | ! | | | regressuser2=r*/regressuser1 +| | ! | | | regressuser3=r*/regressuser1 +| | ! | | | regressuser4=r*/regressuser2 +| | ! | | | regressuser4=r*/regressuser3 +| | ! | | | regressuser5=r/regressuser4 | | (1 row) set session role regressuser2; revoke select on dep_priv_test from regressuser4 cascade; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column privileges | Policies ! --------+---------------+-------+-----------------------------------+-------------------+---------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| | ! | | | regressuser2=r*/regressuser1 +| | ! | | | regressuser3=r*/regressuser1 +| | ! | | | regressuser4=r*/regressuser3 +| | ! | | | regressuser5=r/regressuser4 | | (1 row) set session role regressuser3; revoke select on dep_priv_test from regressuser4 cascade; \dp dep_priv_test ! Access privileges ! Schema | Name | Type | Access privileges | Column privileges | Policies ! --------+---------------+-------+-----------------------------------+-------------------+---------- ! public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| | ! | | | regressuser2=r*/regressuser1 +| | ! | | | regressuser3=r*/regressuser1 | | (1 row) set session role regressuser1; diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out new file mode 100644 index ...f8678c9 *** a/src/test/regress/expected/rowsecurity.out --- b/src/test/regress/expected/rowsecurity.out *************** *** 0 **** --- 1,1465 ---- + -- + -- 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 ROLE IF EXISTS rls_regress_group1; + DROP ROLE IF EXISTS rls_regress_group2; + 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 ROLE rls_regress_group1 NOLOGIN; + CREATE ROLE rls_regress_group2 NOLOGIN; + GRANT rls_regress_group1 TO rls_regress_user1; + GRANT rls_regress_group2 TO rls_regress_user2; + 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 + USING (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); + -- viewpoint from rls_regress_user1 + SET SESSION AUTHORIZATION rls_regress_user1; + SET row_security TO ON; + 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 USING (true); --fail + ERROR: must be owner of relation document + DROP POLICY p1 ON document; --fail + ERROR: must be owner of relation document + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON document 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 + TO PUBLIC + 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'); + -- 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 + -----+-----+--------+-------------------+----------------------- + 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 + 10 | 33 | 1 | rls_regress_user2 | hoge + (9 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- database superuser cannot bypass RLS policy when FORCE enabled. + RESET SESSION AUTHORIZATION; + SET row_security TO FORCE; + 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 + 10 | 33 | 1 | rls_regress_user2 | hoge + (9 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 + 10 | 33 | 1 | rls_regress_user2 | hoge + (9 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- RLS policy applies to table owner when FORCE enabled. + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO FORCE; + SELECT * FROM document; + did | cid | dlevel | dauthor | dtitle + -----+-----+--------+---------+-------- + (0 rows) + + SELECT * FROM category; + cid | cname + -----+------- + (0 rows) + + -- RLS policy does not apply to table owner when RLS enabled. + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + 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 + 10 | 33 | 1 | rls_regress_user2 | hoge + (9 rows) + + SELECT * FROM category; + cid | cname + -----+----------------- + 11 | novel + 22 | science fiction + 33 | technology + 44 | manga + (4 rows) + + -- RLS policy does not apply to table owner when RLS disabled. + SET SESSION AUTHORIZATION rls_regress_user0; + 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 + 10 | 33 | 1 | rls_regress_user2 | hoge + (9 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 TO PUBLIC USING (a % 2 = 0); -- be even number + CREATE POLICY p2 ON t2 FOR ALL TO PUBLIC USING (a % 2 = 1); -- be odd number + SET SESSION AUTHORIZATION rls_regress_user1; + 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 + TO PUBLIC + 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: policy d1 on table dependent depends on table dependee + HINT: Use DROP ... CASCADE to drop the dependent objects too. + DROP TABLE dependee CASCADE; + NOTICE: drop cascades to policy d1 on table dependent + EXPLAIN (COSTS OFF) SELECT * FROM dependent; -- After drop, should be unqualified + QUERY PLAN + ----------------------- + Seq Scan on dependent + (1 row) + + ----- RECURSION ---- + -- + -- Simple recursion + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE rec1 (x integer, y integer); + CREATE POLICY r1 ON rec1 USING (x = (SELECT r.x FROM rec1 r WHERE y = r.y)); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, direct recursion + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- Mutual recursion + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE rec2 (a integer, b integer); + ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2 WHERE b = y)); + CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1 WHERE y = b)); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, mutual recursion + ERROR: infinite recursion detected in row-security policy for relation "rec1" + -- + -- Mutual recursion via views + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE VIEW rec1v AS SELECT * FROM rec1; + CREATE VIEW rec2v AS SELECT * FROM rec2; + ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2v WHERE b = y)); + ALTER POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); + SET SESSION AUTHORIZATION rls_regress_user1; + 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 + -- + SET SESSION AUTHORIZATION rls_regress_user0; + DROP VIEW rec1v, rec2v CASCADE; + NOTICE: drop cascades to 2 other objects + DETAIL: drop cascades to policy r1 on table rec1 + drop cascades to policy r2 on table rec2 + 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 USING (x = (SELECT a FROM rec2v WHERE b = y)); + CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); + SET SESSION AUTHORIZATION rls_regress_user1; + 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 + -- + SET SESSION AUTHORIZATION rls_regress_user0; + 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%'; + GRANT SELECT ON s1, s2, v2 TO rls_regress_user1; + CREATE POLICY p1 ON s1 USING (a in (select x from s2 where y like '%2f%')); + CREATE POLICY p2 ON s2 USING (x in (select a from s1 where b like '%22%')); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) + ERROR: infinite recursion detected in row-security policy for relation "s1" + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p2 ON s2 USING (x % 2 = 0); + SET SESSION AUTHORIZATION rls_regress_user1; + 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 (COSTS OFF) SELECT * FROM only 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 ~~ '%2f%'::text) + -> Seq Scan on s2 s2_1 + Filter: ((x % 2) = 0) + (12 rows) + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON s1 USING (a in (select x from v2)); -- using VIEW in RLS policy + SET SESSION AUTHORIZATION rls_regress_user1; + 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) + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p2 ON s2 USING (x in (select a from s1 where b like '%d2%')); + SET SESSION AUTHORIZATION rls_regress_user1; + 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_user1; + 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_user1; + 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 => bcd + NOTICE: f_leak => def + NOTICE: f_leak => yyy + 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 => bcdbcd + NOTICE: f_leak => defdef + NOTICE: f_leak => yyyyyy + a | b + ---+------------- + 2 | bbbbbb_updt + 4 | dddddd_updt + 2 | bcdbcd + 4 | defdef + 2 | yyyyyy + (5 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 => bcdbcd + NOTICE: f_leak => defdef + NOTICE: f_leak => yyyyyy + oid | a | b | t1 + -----+---+-------------+----------------- + 102 | 2 | bbbbbb_updt | (2,bbbbbb_updt) + 104 | 4 | dddddd_updt | (4,dddddd_updt) + 202 | 2 | bcdbcd | (2,bcdbcd) + 204 | 4 | defdef | (4,defdef) + 302 | 2 | yyyyyy | (2,yyyyyy) + (5 rows) + + RESET SESSION AUTHORIZATION; + SET row_security TO OFF; + SELECT * FROM t1; + a | b + ---+------------- + 1 | aaa + 3 | ccc + 2 | bbbbbb_updt + 4 | dddddd_updt + 1 | abc + 3 | cde + 2 | bcdbcd + 4 | defdef + 1 | xxx + 3 | zzz + 2 | yyyyyy + (11 rows) + + SET SESSION AUTHORIZATION rls_regress_user1; + SET row_security TO ON; + EXPLAIN (COSTS OFF) DELETE FROM only t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Delete 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) + + EXPLAIN (COSTS OFF) DELETE FROM t1 WHERE f_leak(b); + QUERY PLAN + ------------------------------------- + Delete 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) + + 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 => bcdbcd + NOTICE: f_leak => defdef + NOTICE: f_leak => yyyyyy + oid | a | b | t1 + -----+---+--------+------------ + 202 | 2 | bcdbcd | (2,bcdbcd) + 204 | 4 | defdef | (4,defdef) + 302 | 2 | yyyyyy | (2,yyyyyy) + (3 rows) + + -- + -- ROLE/GROUP + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE z1 (a int, b text) WITH OIDS; + GRANT ALL ON z1 TO PUBLIC; + COPY z1 FROM STDIN WITH (OIDS); + CREATE POLICY p1 ON z1 TO rls_regress_group1 + USING (a % 2 = 0); + CREATE POLICY p2 ON z1 TO rls_regress_group2 + USING (a % 2 = 1); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM z1 WHERE f_leak(b); + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + a | b + ---+----- + 2 | bbb + 4 | ddd + (2 rows) + + SET ROLE rls_regress_group1; + SELECT * FROM z1 WHERE f_leak(b); + NOTICE: f_leak => bbb + NOTICE: f_leak => ddd + a | b + ---+----- + 2 | bbb + 4 | ddd + (2 rows) + + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM z1 WHERE f_leak(b); + NOTICE: f_leak => aaa + NOTICE: f_leak => ccc + a | b + ---+----- + 1 | aaa + 3 | ccc + (2 rows) + + SET ROLE rls_regress_group2; + SELECT * FROM z1 WHERE f_leak(b); + NOTICE: f_leak => aaa + NOTICE: f_leak => ccc + a | b + ---+----- + 1 | aaa + 3 | ccc + (2 rows) + + -- + -- Command specific + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE x1 (a int, b text, c text); + GRANT ALL ON x1 TO PUBLIC; + INSERT INTO x1 VALUES + (1, 'abc', 'rls_regress_user1'), + (2, 'bcd', 'rls_regress_user1'), + (3, 'cde', 'rls_regress_user2'), + (4, 'def', 'rls_regress_user2'), + (5, 'efg', 'rls_regress_user1'), + (6, 'fgh', 'rls_regress_user1'), + (7, 'fgh', 'rls_regress_user2'), + (8, 'fgh', 'rls_regress_user2'); + CREATE POLICY p0 ON x1 FOR ALL USING (c = current_user); + CREATE POLICY p1 ON x1 FOR SELECT USING (a % 2 = 0); + CREATE POLICY p2 ON x1 FOR INSERT WITH CHECK (a % 2 = 1); + CREATE POLICY p3 ON x1 FOR UPDATE USING (a % 2 = 0); + CREATE POLICY p4 ON x1 FOR DELETE USING (a < 8); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; + NOTICE: f_leak => abc + NOTICE: f_leak => bcd + NOTICE: f_leak => def + NOTICE: f_leak => efg + NOTICE: f_leak => fgh + NOTICE: f_leak => fgh + a | b | c + ---+-----+------------------- + 1 | abc | rls_regress_user1 + 2 | bcd | rls_regress_user1 + 4 | def | rls_regress_user2 + 5 | efg | rls_regress_user1 + 6 | fgh | rls_regress_user1 + 8 | fgh | rls_regress_user2 + (6 rows) + + UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; + NOTICE: f_leak => abc + NOTICE: f_leak => bcd + NOTICE: f_leak => def + NOTICE: f_leak => efg + NOTICE: f_leak => fgh + NOTICE: f_leak => fgh + a | b | c + ---+----------+------------------- + 1 | abc_updt | rls_regress_user1 + 2 | bcd_updt | rls_regress_user1 + 4 | def_updt | rls_regress_user2 + 5 | efg_updt | rls_regress_user1 + 6 | fgh_updt | rls_regress_user1 + 8 | fgh_updt | rls_regress_user2 + (6 rows) + + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; + NOTICE: f_leak => cde + NOTICE: f_leak => fgh + NOTICE: f_leak => bcd_updt + NOTICE: f_leak => def_updt + NOTICE: f_leak => fgh_updt + NOTICE: f_leak => fgh_updt + a | b | c + ---+----------+------------------- + 2 | bcd_updt | rls_regress_user1 + 3 | cde | rls_regress_user2 + 4 | def_updt | rls_regress_user2 + 6 | fgh_updt | rls_regress_user1 + 7 | fgh | rls_regress_user2 + 8 | fgh_updt | rls_regress_user2 + (6 rows) + + UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; + NOTICE: f_leak => cde + NOTICE: f_leak => fgh + NOTICE: f_leak => bcd_updt + NOTICE: f_leak => def_updt + NOTICE: f_leak => fgh_updt + NOTICE: f_leak => fgh_updt + a | b | c + ---+---------------+------------------- + 3 | cde_updt | rls_regress_user2 + 7 | fgh_updt | rls_regress_user2 + 2 | bcd_updt_updt | rls_regress_user1 + 4 | def_updt_updt | rls_regress_user2 + 6 | fgh_updt_updt | rls_regress_user1 + 8 | fgh_updt_updt | rls_regress_user2 + (6 rows) + + DELETE FROM x1 WHERE f_leak(b) RETURNING *; + NOTICE: f_leak => abc_updt + NOTICE: f_leak => efg_updt + NOTICE: f_leak => cde_updt + NOTICE: f_leak => fgh_updt + NOTICE: f_leak => bcd_updt_updt + NOTICE: f_leak => def_updt_updt + NOTICE: f_leak => fgh_updt_updt + NOTICE: f_leak => fgh_updt_updt + a | b | c + ---+---------------+------------------- + 1 | abc_updt | rls_regress_user1 + 5 | efg_updt | rls_regress_user1 + 3 | cde_updt | rls_regress_user2 + 7 | fgh_updt | rls_regress_user2 + 2 | bcd_updt_updt | rls_regress_user1 + 4 | def_updt_updt | rls_regress_user2 + 6 | fgh_updt_updt | rls_regress_user1 + 8 | fgh_updt_updt | rls_regress_user2 + (8 rows) + + -- + -- Duplicate Policy Names + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE y1 (a int, b text); + CREATE TABLE y2 (a int, b text); + GRANT ALL ON y1, y2 TO rls_regress_user1; + CREATE POLICY p1 ON y1 FOR ALL USING (a % 2 = 0); + CREATE POLICY p2 ON y1 FOR SELECT USING (a > 2); + CREATE POLICY p1 ON y1 FOR SELECT USING (a % 2 = 1); --fail + ERROR: policy "p1" for relation "y1" already exists + CREATE POLICY p1 ON y2 FOR ALL USING (a % 2 = 0); --OK + -- + -- Expression structure with SBV + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); + GRANT SELECT ON rls_sbv TO rls_regress_user1; + SET SESSION AUTHORIZATION rls_regress_user1; + EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); + QUERY PLAN + ---------------------------------------------------------- + Subquery Scan on y1 + Filter: f_leak(y1.b) + -> Seq Scan on y1 y1_1 + Filter: ((a = 1) AND ((a > 2) OR ((a % 2) = 0))) + (4 rows) + + -- + -- Expression structure + -- + SET SESSION AUTHORIZATION rls_regress_user0; + INSERT INTO y2 (SELECT x, md5(x::text) FROM generate_series(0,20) x); + CREATE POLICY p2 ON y2 USING (a % 3 = 0); + CREATE POLICY p3 ON y2 USING (a % 4 = 0); + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM y2 WHERE f_leak(b); + NOTICE: f_leak => cfcd208495d565ef66e7dff9f98764da + NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c + NOTICE: f_leak => eccbc87e4b5ce2fe28308fd9f2a7baf3 + NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c + NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc + NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d + NOTICE: f_leak => 45c48cce2e2d7fbdea1afc51c7c6ad26 + NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 + NOTICE: f_leak => c20ad4d76fe97759aa27a0c99bff6710 + NOTICE: f_leak => aab3238922bcc25a6f606eb525ffdc56 + NOTICE: f_leak => 9bf31c7ff062936a96d3c8bd1f8f2ff3 + NOTICE: f_leak => c74d97b01eae257e44aa9d5bade97baf + NOTICE: f_leak => 6f4922f45568161a8cdf4ad2299f6d23 + NOTICE: f_leak => 98f13708210194c475687be6106a3b84 + a | b + ----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + (14 rows) + + EXPLAIN (COSTS OFF) SELECT * FROM y2 WHERE f_leak(b); + QUERY PLAN + ------------------------------------------------------------------- + Subquery Scan on y2 + Filter: f_leak(y2.b) + -> Seq Scan on y2 y2_1 + Filter: (((a % 4) = 0) OR ((a % 3) = 0) OR ((a % 2) = 0)) + (4 rows) + + -- + -- Plancache invalidate on user change. + -- + RESET SESSION AUTHORIZATION; + DROP TABLE t1 CASCADE; + NOTICE: drop cascades to 2 other objects + DETAIL: drop cascades to table t2 + drop cascades to table t3 + CREATE TABLE t1 (a integer); + GRANT SELECT ON t1 TO rls_regress_user1, rls_regress_user2; + CREATE POLICY p1 ON t1 TO rls_regress_user1 USING ((a % 2) = 0); + CREATE POLICY p2 ON t1 TO rls_regress_user2 USING ((a % 4) = 0); + SET ROLE rls_regress_user1; + PREPARE role_inval AS SELECT * FROM t1; + EXPLAIN (COSTS OFF) EXECUTE role_inval; + QUERY PLAN + ------------------------- + Seq Scan on t1 + Filter: ((a % 2) = 0) + (2 rows) + + SET ROLE rls_regress_user2; + EXPLAIN (COSTS OFF) EXECUTE role_inval; + QUERY PLAN + ------------------------- + Seq Scan on t1 + Filter: ((a % 4) = 0) + (2 rows) + + -- + -- Clean up objects + -- + RESET SESSION AUTHORIZATION; + DROP SCHEMA rls_regress_schema CASCADE; + NOTICE: drop cascades to 18 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 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 cascades to table z1 + drop cascades to table x1 + drop cascades to table y1 + drop cascades to table y2 + drop cascades to view rls_sbv + drop cascades to table t1 + 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..889bcd2 *** a/src/test/regress/expected/rules.out --- b/src/test/regress/expected/rules.out *************** pg_matviews| SELECT n.nspname AS scheman *** 1353,1358 **** --- 1353,1384 ---- LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))) LEFT JOIN pg_tablespace t ON ((t.oid = c.reltablespace))) WHERE (c.relkind = 'm'::"char"); + pg_policies| SELECT rs.rsecpolname AS policyname, + ( SELECT pg_class.relname + FROM pg_class + WHERE (pg_class.oid = rs.rsecrelid)) AS tablename, + CASE + WHEN (rs.rsecroles = '{0}'::oid[]) THEN (string_to_array('public'::text, ''::text))::name[] + ELSE ARRAY( SELECT pg_authid.rolname + FROM pg_authid + WHERE (pg_authid.oid = ANY (rs.rsecroles)) + ORDER BY pg_authid.rolname) + END AS roles, + CASE + WHEN (rs.rseccmd IS NULL) THEN 'ALL'::text + ELSE + CASE rs.rseccmd + WHEN 'r'::"char" THEN 'SELECT'::text + WHEN 'a'::"char" THEN 'INSERT'::text + WHEN 'u'::"char" THEN 'UPDATE'::text + WHEN 'd'::"char" THEN 'DELETE'::text + ELSE NULL::text + END + END AS cmd, + pg_get_expr(rs.rsecqual, rs.rsecrelid) AS qual, + pg_get_expr(rs.rsecwithcheck, rs.rsecrelid) AS with_check + FROM pg_rowsecurity rs + ORDER BY rs.rsecpolname; pg_prepared_statements| SELECT p.name, p.statement, p.prepare_time, *************** pg_roles| SELECT pg_authid.rolname, *** 1389,1394 **** --- 1415,1421 ---- 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))) --- 2045,2052 ---- 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/serial_schedule b/src/test/regress/serial_schedule new file mode 100644 index 16a1905..5ed2bf0 *** a/src/test/regress/serial_schedule --- b/src/test/regress/serial_schedule *************** test: collate *** 101,106 **** --- 101,107 ---- test: matview test: lock test: replica_identity + test: rowsecurity test: alter_generic test: misc test: psql diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql new file mode 100644 index ...03fb54a *** a/src/test/regress/sql/rowsecurity.sql --- b/src/test/regress/sql/rowsecurity.sql *************** *** 0 **** --- 1,546 ---- + -- + -- 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 ROLE IF EXISTS rls_regress_group1; + DROP ROLE IF EXISTS rls_regress_group2; + + 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 ROLE rls_regress_group1 NOLOGIN; + CREATE ROLE rls_regress_group2 NOLOGIN; + + GRANT rls_regress_group1 TO rls_regress_user1; + GRANT rls_regress_group2 TO rls_regress_user2; + + 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 + USING (dlevel <= (SELECT seclv FROM uaccount WHERE pguser = current_user)); + + -- viewpoint from rls_regress_user1 + SET SESSION AUTHORIZATION rls_regress_user1; + SET row_security TO ON; + 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 USING (true); --fail + DROP POLICY p1 ON document; --fail + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON document 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 + TO PUBLIC + 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'); + + -- 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 cannot bypass RLS policy when FORCE enabled. + RESET SESSION AUTHORIZATION; + SET row_security TO FORCE; + 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; + + -- RLS policy applies to table owner when FORCE enabled. + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO FORCE; + SELECT * FROM document; + SELECT * FROM category; + + -- RLS policy does not apply to table owner when RLS enabled. + SET SESSION AUTHORIZATION rls_regress_user0; + SET row_security TO ON; + SELECT * FROM document; + SELECT * FROM category; + + -- RLS policy does not apply to table owner when RLS disabled. + SET SESSION AUTHORIZATION rls_regress_user0; + 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, 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); + 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 TO PUBLIC USING (a % 2 = 0); -- be even number + CREATE POLICY p2 ON t2 FOR ALL TO PUBLIC USING (a % 2 = 1); -- be odd number + + SET SESSION AUTHORIZATION rls_regress_user1; + + 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 + TO PUBLIC + 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 + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE rec1 (x integer, y integer); + CREATE POLICY r1 ON rec1 USING (x = (SELECT r.x FROM rec1 r WHERE y = r.y)); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, direct recursion + + -- + -- Mutual recursion + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE rec2 (a integer, b integer); + ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2 WHERE b = y)); + CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1 WHERE y = b)); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, mutual recursion + + -- + -- Mutual recursion via views + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE VIEW rec1v AS SELECT * FROM rec1; + CREATE VIEW rec2v AS SELECT * FROM rec2; + ALTER POLICY r1 ON rec1 USING (x = (SELECT a FROM rec2v WHERE b = y)); + ALTER POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, mutual recursion via views + + -- + -- Mutual recursion via .s.b views + -- + SET SESSION AUTHORIZATION rls_regress_user0; + 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 USING (x = (SELECT a FROM rec2v WHERE b = y)); + CREATE POLICY r2 ON rec2 USING (a = (SELECT x FROM rec1v WHERE y = b)); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM rec1; -- fail, mutual recursion via s.b. views + + -- + -- recursive RLS and VIEWs in policy + -- + SET SESSION AUTHORIZATION rls_regress_user0; + 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%'; + + GRANT SELECT ON s1, s2, v2 TO rls_regress_user1; + + CREATE POLICY p1 ON s1 USING (a in (select x from s2 where y like '%2f%')); + CREATE POLICY p2 ON s2 USING (x in (select a from s1 where b like '%22%')); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p2 ON s2 USING (x % 2 = 0); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM s1 WHERE f_leak(b); -- OK + EXPLAIN (COSTS OFF) SELECT * FROM only s1 WHERE f_leak(b); + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p1 ON s1 USING (a in (select x from v2)); -- using VIEW in RLS policy + SET SESSION AUTHORIZATION rls_regress_user1; + 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%'; + + SET SESSION AUTHORIZATION rls_regress_user0; + ALTER POLICY p2 ON s2 USING (x in (select a from s1 where b like '%d2%')); + SET SESSION AUTHORIZATION rls_regress_user1; + 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_user1; + 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_user1; + 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; + SET row_security TO OFF; + SELECT * FROM t1; + + SET SESSION AUTHORIZATION rls_regress_user1; + SET row_security TO ON; + 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; + + -- + -- ROLE/GROUP + -- + SET SESSION AUTHORIZATION rls_regress_user0; + + CREATE TABLE z1 (a int, b text) WITH OIDS; + GRANT ALL ON z1 TO PUBLIC; + + COPY z1 FROM STDIN WITH (OIDS); + 101 1 aaa + 102 2 bbb + 103 3 ccc + 104 4 ddd + \. + + CREATE POLICY p1 ON z1 TO rls_regress_group1 + USING (a % 2 = 0); + CREATE POLICY p2 ON z1 TO rls_regress_group2 + USING (a % 2 = 1); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM z1 WHERE f_leak(b); + + SET ROLE rls_regress_group1; + SELECT * FROM z1 WHERE f_leak(b); + + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM z1 WHERE f_leak(b); + + SET ROLE rls_regress_group2; + SELECT * FROM z1 WHERE f_leak(b); + + -- + -- Command specific + -- + SET SESSION AUTHORIZATION rls_regress_user0; + + CREATE TABLE x1 (a int, b text, c text); + GRANT ALL ON x1 TO PUBLIC; + + INSERT INTO x1 VALUES + (1, 'abc', 'rls_regress_user1'), + (2, 'bcd', 'rls_regress_user1'), + (3, 'cde', 'rls_regress_user2'), + (4, 'def', 'rls_regress_user2'), + (5, 'efg', 'rls_regress_user1'), + (6, 'fgh', 'rls_regress_user1'), + (7, 'fgh', 'rls_regress_user2'), + (8, 'fgh', 'rls_regress_user2'); + + CREATE POLICY p0 ON x1 FOR ALL USING (c = current_user); + CREATE POLICY p1 ON x1 FOR SELECT USING (a % 2 = 0); + CREATE POLICY p2 ON x1 FOR INSERT WITH CHECK (a % 2 = 1); + CREATE POLICY p3 ON x1 FOR UPDATE USING (a % 2 = 0); + CREATE POLICY p4 ON x1 FOR DELETE USING (a < 8); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; + UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; + + SET SESSION AUTHORIZATION rls_regress_user2; + SELECT * FROM x1 WHERE f_leak(b) ORDER BY a ASC; + UPDATE x1 SET b = b || '_updt' WHERE f_leak(b) RETURNING *; + DELETE FROM x1 WHERE f_leak(b) RETURNING *; + + -- + -- Duplicate Policy Names + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE TABLE y1 (a int, b text); + CREATE TABLE y2 (a int, b text); + + GRANT ALL ON y1, y2 TO rls_regress_user1; + + CREATE POLICY p1 ON y1 FOR ALL USING (a % 2 = 0); + CREATE POLICY p2 ON y1 FOR SELECT USING (a > 2); + CREATE POLICY p1 ON y1 FOR SELECT USING (a % 2 = 1); --fail + CREATE POLICY p1 ON y2 FOR ALL USING (a % 2 = 0); --OK + + -- + -- Expression structure with SBV + -- + SET SESSION AUTHORIZATION rls_regress_user0; + CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); + GRANT SELECT ON rls_sbv TO rls_regress_user1; + SET SESSION AUTHORIZATION rls_regress_user1; + EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); + + -- + -- Expression structure + -- + SET SESSION AUTHORIZATION rls_regress_user0; + INSERT INTO y2 (SELECT x, md5(x::text) FROM generate_series(0,20) x); + CREATE POLICY p2 ON y2 USING (a % 3 = 0); + CREATE POLICY p3 ON y2 USING (a % 4 = 0); + + SET SESSION AUTHORIZATION rls_regress_user1; + SELECT * FROM y2 WHERE f_leak(b); + EXPLAIN (COSTS OFF) SELECT * FROM y2 WHERE f_leak(b); + + -- + -- Plancache invalidate on user change. + -- + RESET SESSION AUTHORIZATION; + DROP TABLE t1 CASCADE; + CREATE TABLE t1 (a integer); + + GRANT SELECT ON t1 TO rls_regress_user1, rls_regress_user2; + + CREATE POLICY p1 ON t1 TO rls_regress_user1 USING ((a % 2) = 0); + CREATE POLICY p2 ON t1 TO rls_regress_user2 USING ((a % 4) = 0); + + SET ROLE rls_regress_user1; + PREPARE role_inval AS SELECT * FROM t1; + EXPLAIN (COSTS OFF) EXECUTE role_inval; + + SET ROLE rls_regress_user2; + EXPLAIN (COSTS OFF) EXECUTE role_inval; + + -- + -- 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;