diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 68f8434352..76d6405061 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -238,6 +238,11 @@ replication slot information + + pg_rowsecurity + table row-level security policies + + pg_seclabel security labels on database objects @@ -1935,6 +1940,15 @@ + + relhasrowsecurity + bool + + True if table has row-security enabled; see + pg_rowsecurity catalog + + + relhassubclass bool @@ -5328,6 +5342,86 @@ + + <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> @@ -9133,6 +9227,12 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx 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 index 5be8fdcc25..70e47aaa3a 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -5429,6 +5429,46 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + row_security (enum) + + row_security configuration parameter + + + + + This variable controls if row security policies are to be applied + to queries which are run against tables that have row security enabled. + 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/event-trigger.sgml b/doc/src/sgml/event-trigger.sgml index 3db8ef1a13..6f71a27855 100644 --- a/doc/src/sgml/event-trigger.sgml +++ b/doc/src/sgml/event-trigger.sgml @@ -195,6 +195,12 @@ X - + + ALTER POLICY + X + X + - + ALTER SCHEMA X @@ -351,6 +357,12 @@ X - + + CREATE POLICY + X + X + - + CREATE RULE X @@ -525,6 +537,12 @@ X X + + DROP POLICY + X + X + X + DROP RULE X diff --git a/doc/src/sgml/keywords.sgml b/doc/src/sgml/keywords.sgml index 1c93b7c148..b0dfd5ff75 100644 --- a/doc/src/sgml/keywords.sgml +++ b/doc/src/sgml/keywords.sgml @@ -3422,6 +3422,13 @@ non-reserved non-reserved + + POLICY + non-reserved + + + + PORTION diff --git a/doc/src/sgml/ref/allfiles.sgml b/doc/src/sgml/ref/allfiles.sgml index b685e16a0f..7aa3128090 100644 --- a/doc/src/sgml/ref/allfiles.sgml +++ b/doc/src/sgml/ref/allfiles.sgml @@ -25,6 +25,7 @@ Complete list of usable sgml source files in this directory. + @@ -69,6 +70,7 @@ Complete list of usable sgml source files in this directory. + @@ -110,6 +112,7 @@ Complete list of usable sgml source files in this directory. + diff --git a/doc/src/sgml/ref/alter_policy.sgml b/doc/src/sgml/ref/alter_policy.sgml new file mode 100644 index 0000000000..37615fcab5 --- /dev/null +++ b/doc/src/sgml/ref/alter_policy.sgml @@ -0,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 index bcd46d5e4d..0471daa1cc 100644 --- a/doc/src/sgml/ref/alter_role.sgml +++ b/doc/src/sgml/ref/alter_role.sgml @@ -32,6 +32,7 @@ ALTER ROLE name [ [ WITH ] connlimit | [ ENCRYPTED | UNENCRYPTED ] PASSWORD 'password' | VALID UNTIL 'timestamp' @@ -142,6 +143,8 @@ ALTER ROLE { name | ALL } [ IN DATA NOLOGIN REPLICATION NOREPLICATION + BYPASSRLS + NOBYPASSRLS CONNECTION LIMIT connlimit PASSWORD password ENCRYPTED diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml index 5bbf4fb359..1b35756c29 100644 --- a/doc/src/sgml/ref/alter_table.sgml +++ b/doc/src/sgml/ref/alter_table.sgml @@ -59,6 +59,8 @@ ALTER TABLE ALL IN TABLESPACE name ENABLE RULE rewrite_rule_name ENABLE REPLICA RULE rewrite_rule_name ENABLE ALWAYS RULE rewrite_rule_name + DISABLE ROW LEVEL SECURITY + ENABLE ROW LEVEL SECURITY CLUSTER ON index_name SET WITHOUT CLUSTER SET WITH OIDS @@ -420,6 +422,21 @@ ALTER TABLE ALL IN TABLESPACE name + + DISABLE/ENABLE ROW LEVEL SECURITY + + + These forms control the application of row security policies belonging + to the table. If enabled and no policies exist for the table, then a + default-deny policy is applied. Note that policies can exist for a table + even if row level security is disabled- in this case, the policies will + NOT be applied and the policies will be ignored. + See also + . + + + + CLUSTER ON diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml new file mode 100644 index 0000000000..c6599eda1c --- /dev/null +++ b/doc/src/sgml/ref/create_policy.sgml @@ -0,0 +1,318 @@ + + + + + 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. Note that row-security must also be enabled on the + table using ALTER TABLE in order for created policies + to be applied. + + + + 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. + + + + Note that while row-security policies will be applied for explicit queries + against tables in the system, they are not applied when the system is + performing internal referential integrity checks or validating constraints. + This means there are indirect ways to determine that a given value exists. + An example of this is attempting to insert a duplicate value + into a column which is the primary key or has a unique constraint. If the + insert fails then the user can infer that the value already exists (this + example assumes that the user is permitted by policy to insert + records which they are not allowed to see). Another example is where a user + is allowed to insert into a table which references another, otherwise hidden + table. Existence can be determined by the user inserting values into the + referencing table, where success would indicate that the value exists in the + referenced table. These issues can be addressed by carefully crafting + policies which prevent users from being able to insert, delete, or update + records at all which might possibly indicate a value they are not otherwise + able to see, or by using generated values (eg: surrogate keys) instead. + + + + Regarding how policy expressions interact with the user: as the expressions + are added to the user's query directly, they will be run with the rights of + the user running the overall query. Therefore, users who are using a given + policy must be able to access any tables or functions referenced in the + expression or they will simply receive a permission denied error when + attempting to query the RLS-enabled table. This does not change how views + work, however. As with normal queries and views, permission checks and + policies for the tables which are referenced by a view will use the view + owner's rights and any policies which apply to the view owner. + + + + + + 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 index 641e3500c9..ea26027511 100644 --- a/doc/src/sgml/ref/create_role.sgml +++ b/doc/src/sgml/ref/create_role.sgml @@ -32,6 +32,7 @@ CREATE ROLE name [ [ WITH ] connlimit | [ ENCRYPTED | UNENCRYPTED ] PASSWORD 'password' | VALID UNTIL 'timestamp' @@ -190,6 +191,25 @@ CREATE ROLE name [ [ WITH ] + + BYPASSRLS + NOBYPASSRLS + + + These clauses determine whether a role is allowed to bypass row-security + policies. A role having the BYPASSRLS attribute will + be allowed to bypass row-security policies by setting + row_security to + OFF. NOBYPASSRLS is the default. + Note that pg_dump will set row_security to + OFF by default, to ensure all contents of a table are + dumped out. If the user running pg_dump does not have appropriate + permissions, an error will be returned. The superuser and owner of the + table being dumped are considered to always have the right to bypass RLS. + + + + CONNECTION LIMIT connlimit diff --git a/doc/src/sgml/ref/drop_policy.sgml b/doc/src/sgml/ref/drop_policy.sgml new file mode 100644 index 0000000000..31ca9db220 --- /dev/null +++ b/doc/src/sgml/ref/drop_policy.sgml @@ -0,0 +1,109 @@ + + + + + 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. Note that if the last policy is removed for a table and + the table still has ROW POLICY enabled via ALTER TABLE, + then the default-deny policy will be used. ALTER TABLE + can be used to disable row security for a table using + DISABLE ROW SECURITY, whether policies for the table + exist or not. + + + + + 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 index 6ec126381c..10c9a6d403 100644 --- a/doc/src/sgml/reference.sgml +++ b/doc/src/sgml/reference.sgml @@ -53,6 +53,7 @@ &alterOperator; &alterOperatorClass; &alterOperatorFamily; + &alterPolicy; &alterRole; &alterRule; &alterSchema; @@ -97,6 +98,7 @@ &createOperator; &createOperatorClass; &createOperatorFamily; + &createPolicy; &createRole; &createRule; &createSchema; @@ -138,6 +140,7 @@ &dropOperatorClass; &dropOperatorFamily; &dropOwned; + &dropPolicy; &dropRole; &dropRule; &dropSchema; diff --git a/src/backend/catalog/Makefile b/src/backend/catalog/Makefile index a974bd5260..b257b02ff5 100644 --- a/src/backend/catalog/Makefile +++ b/src/backend/catalog/Makefile @@ -39,7 +39,7 @@ POSTGRES_BKI_SRCS = $(addprefix $(top_srcdir)/src/include/catalog/,\ 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_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 index d9745cabd2..d30612c4d9 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -5080,6 +5080,25 @@ has_createrole_privilege(Oid roleid) 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 index d41ba49f87..256486c5fd 100644 --- a/src/backend/catalog/dependency.c +++ b/src/backend/catalog/dependency.c @@ -45,6 +45,7 @@ #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,6 +58,7 @@ #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" @@ -1249,6 +1251,10 @@ doDeletion(const ObjectAddress *object, int flags) RemoveEventTriggerById(object->objectId); break; + case OCLASS_ROWSECURITY: + RemovePolicyById(object->objectId); + break; + default: elog(ERROR, "unrecognized object class: %u", object->classId); @@ -2316,6 +2322,9 @@ getObjectClass(const ObjectAddress *object) 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 index c346edac93..8d9eeb9dd7 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -799,6 +799,7 @@ InsertPgClassTuple(Relation pg_class_desc, 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 index d143a4459d..b69b75bcc2 100644 --- a/src/backend/catalog/objectaddress.c +++ b/src/backend/catalog/objectaddress.c @@ -42,6 +42,7 @@ #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,6 +56,7 @@ #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" @@ -343,6 +345,18 @@ static const ObjectPropertyType ObjectProperty[] = -1, false }, + { + RowSecurityRelationId, + RowSecurityOidIndexId, + -1, + -1, + Anum_pg_rowsecurity_rsecpolname, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, { EventTriggerRelationId, EventTriggerOidIndexId, @@ -517,6 +531,7 @@ get_object_address(ObjectType objtype, List *objname, List *objargs, case OBJECT_RULE: case OBJECT_TRIGGER: case OBJECT_CONSTRAINT: + case OBJECT_POLICY: address = get_object_address_relobject(objtype, objname, &relation, missing_ok); break; @@ -982,6 +997,13 @@ get_object_address_relobject(ObjectType objtype, List *objname, 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 */ @@ -1155,6 +1177,7 @@ check_object_ownership(Oid roleid, ObjectType objtype, ObjectAddress address, case OBJECT_COLUMN: case OBJECT_RULE: case OBJECT_TRIGGER: + case OBJECT_POLICY: case OBJECT_CONSTRAINT: if (!pg_class_ownercheck(RelationGetRelid(relation), roleid)) aclcheck_error(ACLCHECK_NOT_OWNER, ACL_KIND_CLASS, @@ -2166,6 +2189,41 @@ getObjectDescription(const ObjectAddress *object) break; } + case OCLASS_ROWSECURITY: + { + Relation rsec_rel; + ScanKeyData skey[1]; + SysScanDesc sscan; + HeapTuple tuple; + Form_pg_rowsecurity form_rsec; + + rsec_rel = heap_open(RowSecurityRelationId, AccessShareLock); + + ScanKeyInit(&skey[0], + 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 index 22663c31fe..f62ed2e17d 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -19,6 +19,7 @@ CREATE VIEW pg_roles AS rolconnlimit, '********'::text as rolpassword, rolvaliduntil, + rolbypassrls, setconfig as rolconfig, pg_authid.oid FROM pg_authid LEFT JOIN pg_db_role_setting s @@ -62,6 +63,34 @@ CREATE VIEW pg_user AS 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, @@ -89,7 +118,8 @@ CREATE VIEW pg_tables AS T.spcname AS tablespace, C.relhasindex AS hasindexes, C.relhasrules AS hasrules, - C.relhastriggers AS hastriggers + 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 index 22f116b78d..b1ac704886 100644 --- a/src/backend/commands/Makefile +++ b/src/backend/commands/Makefile @@ -17,7 +17,7 @@ OBJS = aggregatecmds.o alter.o analyze.o async.o cluster.o comment.o \ 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 \ + 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 index 80c9743a0d..c9a9bafef7 100644 --- a/src/backend/commands/alter.c +++ b/src/backend/commands/alter.c @@ -43,6 +43,7 @@ #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" @@ -338,6 +339,9 @@ ExecRenameStmt(RenameStmt *stmt) 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 index fbd7492a73..6b8357634a 100644 --- a/src/backend/commands/copy.c +++ b/src/backend/commands/copy.c @@ -37,7 +37,9 @@ #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" @@ -784,6 +786,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed) 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()) @@ -837,11 +840,72 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed) rte->selectedCols = bms_add_member(rte->selectedCols, attno); } ExecCheckRTPerms(list_make1(rte), true); + + /* + * Permission check for row security. + * + * check_enable_rls will ereport(ERROR) if the user has requested + * something invalid and will otherwise indicate if we should enable + * RLS (returns RLS_ENABLED) or not for this COPY statement. + * + * If the relation has a row security policy and we are to apply it + * then perform a "query" copy and allow the normal query processing to + * handle the policies. + * + * If RLS is not enabled for this, then just fall through to the + * normal non-filtering relation handling. + */ + if (check_enable_rls(rte->relid, InvalidOid) == RLS_ENABLED) + { + 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; } @@ -861,7 +925,7 @@ DoCopy(const CopyStmt *stmt, const char *queryString, uint64 *processed) } else { - cstate = BeginCopyTo(rel, stmt->query, queryString, + 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/createas.c b/src/backend/commands/createas.c index 52451716f4..e381c06e67 100644 --- a/src/backend/commands/createas.c +++ b/src/backend/commands/createas.c @@ -36,6 +36,7 @@ #include "miscadmin.h" #include "parser/parse_clause.h" #include "rewrite/rewriteHandler.h" +#include "rewrite/rowsecurity.h" #include "storage/smgr.h" #include "tcop/tcopprot.h" #include "utils/builtins.h" @@ -419,6 +420,19 @@ intorel_startup(DestReceiver *self, int operation, TupleDesc typeinfo) ExecCheckRTPerms(list_make1(rte), true); + /* + * Make sure the constructed table does not have RLS enabled. + * + * check_enable_rls() will ereport(ERROR) itself if the user has requested + * something invalid, and otherwise will return RLS_ENABLED if RLS should + * be enabled here. We don't actually support that currently, so throw + * our own ereport(ERROR) if that happens. + */ + if (check_enable_rls(intoRelationId, InvalidOid) == RLS_ENABLED) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + (errmsg("policies not yet implemented for this command")))); + /* * Tentatively mark the target as populated, if it's a matview and we're * going to fill it; otherwise, no change needed. diff --git a/src/backend/commands/dropcmds.c b/src/backend/commands/dropcmds.c index e64ad8027e..858358166d 100644 --- a/src/backend/commands/dropcmds.c +++ b/src/backend/commands/dropcmds.c @@ -371,6 +371,15 @@ does_not_exist_skipping(ObjectType objtype, List *objname, List *objargs) list_length(objname) - 1)); } break; + case OBJECT_POLICY: + if (!owningrel_does_not_exist_skipping(objname, &msg, &name)) + { + msg = gettext_noop("policy \"%s\" for relation \"%s\" does not exist, skipping"); + name = strVal(llast(objname)); + args = NameListToString(list_truncate(list_copy(objname), + list_length(objname) - 1)); + } + break; case OBJECT_EVENT_TRIGGER: msg = gettext_noop("event trigger \"%s\" does not exist, skipping"); name = NameListToString(objname); diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c index 754264eb3e..1b8c94bcfd 100644 --- a/src/backend/commands/event_trigger.c +++ b/src/backend/commands/event_trigger.c @@ -85,6 +85,7 @@ static event_trigger_support_data event_trigger_support[] = { {"OPERATOR", true}, {"OPERATOR CLASS", true}, {"OPERATOR FAMILY", true}, + {"POLICY", true}, {"ROLE", false}, {"RULE", true}, {"SCHEMA", true}, @@ -936,6 +937,7 @@ EventTriggerSupportsObjectType(ObjectType obtype) case OBJECT_OPCLASS: case OBJECT_OPERATOR: case OBJECT_OPFAMILY: + case OBJECT_POLICY: case OBJECT_RULE: case OBJECT_SCHEMA: case OBJECT_SEQUENCE: @@ -995,6 +997,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass) 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 0000000000..2f4df48902 --- /dev/null +++ b/src/backend/commands/policy.c @@ -0,0 +1,988 @@ +/*------------------------------------------------------------------------- + * + * 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/pg_rowsecurity.h" +#include "catalog/pg_type.h" +#include "commands/policy.h" +#include "miscadmin.h" +#include "nodes/makefuncs.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* rls_role_list_to_array(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; +} + +/* + * rls_role_list_to_array + * 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_ID_PUBLIC is + * used as the role id. + * + * roles - the list of role names to convert. + */ +static ArrayType * +rls_role_list_to_array(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(); + { + /* + * Set up our memory context- we will always set up some kind of + * policy here. If no explicit policies are found then an implicit + * default-deny policy is created. + */ + rscxt = AllocSetContextCreate(CacheMemoryContext, + "Row-security descriptor", + ALLOCSET_SMALL_MINSIZE, + ALLOCSET_SMALL_INITSIZE, + ALLOCSET_SMALL_MAXSIZE); + rsdesc = MemoryContextAllocZero(rscxt, sizeof(RowSecurityDesc)); + rsdesc->rscxt = rscxt; + + /* + * 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; + + 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) || + contain_subplans((Node *) with_check_qual); + + rsdesc->policies = lcons(policy, rsdesc->policies); + + MemoryContextSwitchTo(oldcxt); + + if (qual_expr != NULL) + pfree(qual_expr); + + if (with_check_qual != NULL) + pfree(with_check_qual); + } + + /* + * Check if no policies were added + * + * If no policies exist in pg_rowsecurity for this relation, then we + * need to create a single default-deny policy. We use InvalidOid for + * the Oid to indicate that this is the default-deny policy (we may + * decide to ignore the default policy if an extension adds policies). + */ + if (rsdesc->policies == NIL) + { + RowSecurityPolicy *policy = NULL; + Datum role; + + oldcxt = MemoryContextSwitchTo(rscxt); + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy = palloc0(sizeof(RowSecurityPolicy)); + policy->policy_name = pstrdup("default-deny policy"); + policy->rsecid = InvalidOid; + policy->cmd = '\0'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, + 'i'); + policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid, + sizeof(bool), BoolGetDatum(false), + false, true); + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + rsdesc->policies = lcons(policy, rsdesc->policies); + + MemoryContextSwitchTo(oldcxt); + } + } + 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; + SysScanDesc sscan; + ScanKeyData skey[1]; + HeapTuple tuple; + Oid relid; + Relation rel; + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* + * Find the policy to delete. + */ + ScanKeyInit(&skey[0], + 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); + + /* + * Open and exclusive-lock the relation the policy belong to. + */ + relid = ((Form_pg_rowsecurity) GETSTRUCT(tuple))->rsecrelid; + + rel = heap_open(relid, AccessExclusiveLock); + if (rel->rd_rel->relkind != RELKIND_RELATION) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not a table", + RelationGetRelationName(rel)))); + + if (!allowSystemTableMods && IsSystemRelation(rel)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + RelationGetRelationName(rel)))); + + simple_heap_delete(pg_rowsecurity_rel, &tuple->t_self); + + systable_endscan(sscan); + heap_close(rel, AccessExclusiveLock); + + /* + * Note that, unlike some of the other flags in pg_class, relhasrowsecurity + * is not just an indication of if policies exist. When relhasrowsecurity + * is set (which can be done directly by the user or indirectly by creating + * a policy on the table), then all access to the relation must be through + * a policy. If no policy is defined for the relation then a default-deny + * policy is created and all records are filtered (except for queries from + * the owner). + */ + + CacheInvalidateRelcache(rel); + + /* Clean up */ + 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 skey[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 = rls_role_list_to_array(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_WHERE, + "POLICY"); + + with_check_qual = transformWhereClause(with_check_pstate, + copyObject(stmt->with_check), + EXPR_KIND_WHERE, + "POLICY"); + + /* Open pg_rowsecurity catalog */ + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, RowExclusiveLock); + + /* Set key - row security relation id. */ + ScanKeyInit(&skey[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security policy name. */ + ScanKeyInit(&skey[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skey); + + 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); + + /* 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 skey[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 = rls_role_list_to_array(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_WHERE, + "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_WHERE, + "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(&skey[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Set key - row security policy name. */ + ScanKeyInit(&skey[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skey); + + 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 skey[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(&skey[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skey[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->newname)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skey); + + 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(&skey[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(table_id)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skey[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(stmt->subname)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skey); + + 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; +} + +/* + * 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 skey[2]; + SysScanDesc sscan; + HeapTuple rsec_tuple; + Oid policy_oid; + + pg_rowsecurity_rel = heap_open(RowSecurityRelationId, AccessShareLock); + + /* Add key - row security relation id. */ + ScanKeyInit(&skey[0], + Anum_pg_rowsecurity_rsecrelid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(relid)); + + /* Add key - row security policy name. */ + ScanKeyInit(&skey[1], + Anum_pg_rowsecurity_rsecpolname, + BTEqualStrategyNumber, F_NAMEEQ, + CStringGetDatum(policy_name)); + + sscan = systable_beginscan(pg_rowsecurity_rel, + RowSecurityRelidPolnameIndexId, true, NULL, 2, + skey); + + rsec_tuple = systable_getnext(sscan); + + if (!HeapTupleIsValid(rsec_tuple)) + { + if (!missing_ok) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("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, AccessShareLock); + + return policy_oid; +} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index 7bc579bf4c..0385404c57 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -36,6 +36,7 @@ #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,6 +46,7 @@ #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" @@ -408,6 +410,8 @@ static void ATExecAddOf(Relation rel, const TypeName *ofTypename, LOCKMODE lockm 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); static void copy_relation_data(SMgrRelation rel, SMgrRelation dst, ForkNumber forkNum, char relpersistence); @@ -2872,6 +2876,8 @@ AlterTableGetLockLevel(List *cmds) case AT_AddIndexConstraint: case AT_ReplicaIdentity: case AT_SetNotNull: + case AT_EnableRowSecurity: + case AT_DisableRowSecurity: cmd_lockmode = AccessExclusiveLock; break; @@ -3280,6 +3286,8 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd, 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 */ @@ -3571,6 +3579,12 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab, Relation rel, case AT_ReplicaIdentity: ATExecReplicaIdentity(rel, (ReplicaIdentityStmt *) cmd->def, lockmode); break; + case AT_EnableRowSecurity: + ATExecEnableRowSecurity(rel); + break; + case AT_DisableRowSecurity: + ATExecDisableRowSecurity(rel); + break; case AT_GenericOptions: ATExecGenericOptions(rel, (List *) cmd->def); break; @@ -10614,6 +10628,62 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode index_close(indexRel, NoLock); } +/* + * 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) +{ + Relation pg_class; + Oid relid; + HeapTuple tuple; + + relid = RelationGetRelid(rel); + + /* 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); + + heap_close(pg_class, RowExclusiveLock); + heap_freetuple(tuple); +} + /* * ALTER FOREIGN TABLE OPTIONS (...) */ diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 91b6fa5c17..1a73fd8558 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -87,6 +87,7 @@ CreateRole(CreateRoleStmt *stmt) 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 */ @@ -106,6 +107,7 @@ CreateRole(CreateRoleStmt *stmt) 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) @@ -232,6 +234,14 @@ CreateRole(CreateRoleStmt *stmt) 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); @@ -267,6 +277,8 @@ CreateRole(CreateRoleStmt *stmt) adminmembers = (List *) dadminmembers->arg; if (dvalidUntil) validUntil = strVal(dvalidUntil->arg); + if (dbypassRLS) + bypassrls = intVal(dbypassRLS->arg) != 0; /* Check some permissions first */ if (issuper) @@ -283,6 +295,13 @@ CreateRole(CreateRoleStmt *stmt) (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()) @@ -375,6 +394,8 @@ CreateRole(CreateRoleStmt *stmt) 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); /* @@ -474,6 +495,7 @@ AlterRole(AlterRoleStmt *stmt) 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; @@ -484,6 +506,7 @@ AlterRole(AlterRoleStmt *stmt) DefElem *dconnlimit = NULL; DefElem *drolemembers = NULL; DefElem *dvalidUntil = NULL; + DefElem *dbypassRLS = NULL; Oid roleid; /* Extract options from the statement node tree */ @@ -578,6 +601,14 @@ AlterRole(AlterRoleStmt *stmt) 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); @@ -609,6 +640,8 @@ AlterRole(AlterRoleStmt *stmt) 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. @@ -642,6 +675,13 @@ AlterRole(AlterRoleStmt *stmt) (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 && @@ -775,6 +815,12 @@ AlterRole(AlterRoleStmt *stmt) 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/executor/execMain.c b/src/backend/executor/execMain.c index 01eda70f05..a546292da6 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -501,6 +501,12 @@ ExecutorRewind(QueryDesc *queryDesc) * * Returns true if permissions are adequate. Otherwise, throws an appropriate * error if ereport_on_violation is true, or simply returns false otherwise. + * + * Note that this does NOT address row-level security policies (aka: RLS). If + * rows will be returned to the user as a result of this permission check + * passing, then RLS also needs to be consulted (and check_enable_rls()). + * + * See rewrite/rowsecurity.c. */ bool ExecCheckRTPerms(List *rangeTable, bool ereport_on_violation) @@ -1660,15 +1666,17 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo, /* * WITH CHECK OPTION checks are intended to ensure that the new tuple - * is visible in the view. If the view's qual evaluates to NULL, then - * the new tuple won't be included in the view. Therefore we need to - * tell ExecQual to return FALSE for NULL (the opposite of what we do - * above for CHECK constraints). + * is visible (in the case of a view) or that it passes the + * 'with-check' policy (in the case of row security). + * If the qual evaluates to NULL or FALSE, then the new tuple won't be + * included in the view or doesn't pass the 'with-check' policy for the + * table. We need ExecQual to return FALSE for NULL to handle the view + * case (the opposite of what we do above for CHECK constraints). */ if (!ExecQual((List *) wcoExpr, econtext, false)) ereport(ERROR, (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION), - errmsg("new row violates WITH CHECK OPTION for view \"%s\"", + errmsg("new row violates WITH CHECK OPTION for \"%s\"", wco->viewname), errdetail("Failing row contains %s.", ExecBuildSlotValueDescription(slot, diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index aa053a0f15..8d842d4689 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -2488,6 +2488,7 @@ _copyQuery(const Query *from) 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); @@ -3849,6 +3850,35 @@ _copyAlterTSConfigurationStmt(const AlterTSConfigurationStmt *from) 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; +} + /* **************************************************************** * pg_list.h copy functions * **************************************************************** @@ -4561,7 +4591,12 @@ copyObject(const void *from) case T_AlterTSConfigurationStmt: retval = _copyAlterTSConfigurationStmt(from); break; - + case T_CreatePolicyStmt: + retval = _copyCreatePolicyStmt(from); + break; + case T_AlterPolicyStmt: + retval = _copyAlterPolicyStmt(from); + break; case T_A_Expr: retval = _copyAExpr(from); break; diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 719923e02e..7a291505d6 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -857,6 +857,7 @@ _equalQuery(const Query *a, const Query *b) 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); @@ -2007,6 +2008,31 @@ _equalAlterTSConfigurationStmt(const AlterTSConfigurationStmt *a, return true; } +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 _equalAExpr(const A_Expr *a, const A_Expr *b) { @@ -3025,7 +3051,12 @@ equal(const void *a, const void *b) 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_A_Expr: retval = _equalAExpr(a, b); break; diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index e686a6c199..1ff78ebddd 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -2263,6 +2263,7 @@ _outQuery(StringInfo str, const Query *node) 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 index 69d9989484..a324100ed7 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -208,6 +208,7 @@ _readQuery(void) 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 index e1480cda24..a509edd3af 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -177,6 +177,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) glob->lastPHId = 0; glob->lastRowMarkId = 0; glob->transientPlan = false; + glob->has_rls = false; /* Determine what fraction of the plan is likely to be scanned */ if (cursorOptions & CURSOR_OPT_FAST_PLAN) @@ -254,6 +255,7 @@ standard_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) result->relationOids = glob->relationOids; result->invalItems = glob->invalItems; result->nParamExec = glob->nParamExec; + result->has_rls = glob->has_rls; return result; } @@ -1206,6 +1208,7 @@ grouping_planner(PlannerInfo *root, double tuple_fraction) * 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 index 4d717df191..5bf84c1a21 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -2081,7 +2081,8 @@ record_plan_function_dependency(PlannerInfo *root, Oid funcid) void extract_query_dependencies(Node *query, List **relationOids, - List **invalItems) + List **invalItems, + bool *hasRowSecurity) { PlannerGlobal glob; PlannerInfo root; @@ -2091,6 +2092,7 @@ extract_query_dependencies(Node *query, glob.type = T_PlannerGlobal; glob.relationOids = NIL; glob.invalItems = NIL; + glob.has_rls = false; MemSet(&root, 0, sizeof(root)); root.type = T_PlannerInfo; @@ -2100,6 +2102,7 @@ extract_query_dependencies(Node *query, *relationOids = glob.relationOids; *invalItems = glob.invalItems; + *hasRowSecurity = glob.has_rls; } static bool @@ -2115,6 +2118,9 @@ extract_query_dependencies_walker(Node *node, PlannerInfo *context) Query *query = (Query *) node; ListCell *lc; + /* Collect row-security information */ + context->glob->has_rls = query->hasRowSecurity; + if (query->commandType == CMD_UTILITY) { /* diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index b46dd7b008..77d2f29fc7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -231,7 +231,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); AlterObjectSchemaStmt AlterOwnerStmt AlterSeqStmt AlterSystemStmt AlterTableStmt AlterTblSpcStmt AlterExtensionStmt AlterExtensionContentsStmt AlterForeignTableStmt AlterCompositeTypeStmt AlterUserStmt AlterUserMappingStmt AlterUserSetStmt - AlterRoleStmt AlterRoleSetStmt + AlterRoleStmt AlterRoleSetStmt AlterPolicyStmt AlterDefaultPrivilegesStmt DefACLAction AnalyzeStmt ClosePortalStmt ClusterStmt CommentStmt ConstraintsSetStmt CopyStmt CreateAsStmt CreateCastStmt @@ -240,11 +240,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); CreateSchemaStmt CreateSeqStmt CreateStmt CreateTableSpaceStmt CreateFdwStmt CreateForeignServerStmt CreateForeignTableStmt CreateAssertStmt CreateTrigStmt CreateEventTrigStmt - CreateUserStmt CreateUserMappingStmt CreateRoleStmt + CreateUserStmt CreateUserMappingStmt CreateRoleStmt CreatePolicyStmt CreatedbStmt DeclareCursorStmt DefineStmt DeleteStmt DiscardStmt DoStmt DropGroupStmt DropOpClassStmt DropOpFamilyStmt DropPLangStmt DropStmt DropAssertStmt DropTrigStmt DropRuleStmt DropCastStmt DropRoleStmt - DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt + DropPolicyStmt DropUserStmt DropdbStmt DropTableSpaceStmt DropFdwStmt DropForeignServerStmt DropUserMappingStmt ExplainStmt FetchStmt GrantStmt GrantRoleStmt ImportForeignSchemaStmt IndexStmt InsertStmt ListenStmt LoadStmt LockStmt NotifyStmt ExplainableStmt PreparableStmt @@ -319,6 +319,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %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 @@ -589,7 +593,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); 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 + PARSER PARTIAL PARTITION PASSING PASSWORD PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROGRAM @@ -740,6 +744,7 @@ stmt : | AlterGroupStmt | AlterObjectSchemaStmt | AlterOwnerStmt + | AlterPolicyStmt | AlterSeqStmt | AlterSystemStmt | AlterTableStmt @@ -774,6 +779,7 @@ stmt : | CreateOpClassStmt | CreateOpFamilyStmt | AlterOpFamilyStmt + | CreatePolicyStmt | CreatePLangStmt | CreateSchemaStmt | CreateSeqStmt @@ -799,6 +805,7 @@ stmt : | DropOpClassStmt | DropOpFamilyStmt | DropOwnedStmt + | DropPolicyStmt | DropPLangStmt | DropRuleStmt | DropStmt @@ -957,6 +964,10 @@ AlterOptRoleElem: $$ = 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) { /* @@ -2302,6 +2313,20 @@ alter_table_cmd: 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 + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_DisableRowSecurity; + $$ = (Node *)n; + } | alter_generic_options { AlterTableCmd *n = makeNode(AlterTableCmd); @@ -4495,6 +4520,105 @@ AlterUserMappingStmt: ALTER USER MAPPING FOR auth_ident SERVER name alter_generi } ; +/***************************************************************************** + * + * QUERIES: + * CREATE POLICY name ON table FOR cmd TO role USING (qual) + * WITH CHECK (with_check) + * ALTER POLICY name ON table FOR cmd TO role USING (qual) + * WITH CHECK (with_check) + * DROP POLICY name ON table + * + *****************************************************************************/ + +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 any_name opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + n->removeType = OBJECT_POLICY; + n->objects = list_make1(lappend($5, makeString($3))); + n->arguments = NIL; + n->behavior = $6; + n->missing_ok = false; + n->concurrent = false; + $$ = (Node *) n; + } + | DROP POLICY IF_P EXISTS name ON any_name opt_drop_behavior + { + DropStmt *n = makeNode(DropStmt); + n->removeType = OBJECT_POLICY; + n->objects = list_make1(lappend($7, makeString($5))); + n->arguments = NIL; + n->behavior = $8; + n->missing_ok = true; + n->concurrent = false; + $$ = (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 : @@ -7240,6 +7364,26 @@ RenameStmt: ALTER AGGREGATE func_name aggr_args RENAME TO name 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); @@ -13036,6 +13180,7 @@ unreserved_keyword: | PASSING | PASSWORD | PLANS + | POLICY | PRECEDING | PREPARE | PREPARED diff --git a/src/backend/rewrite/Makefile b/src/backend/rewrite/Makefile index 9ff56c75ad..25423d39e6 100644 --- a/src/backend/rewrite/Makefile +++ b/src/backend/rewrite/Makefile @@ -13,6 +13,7 @@ top_builddir = ../../.. include $(top_builddir)/src/Makefile.global OBJS = rewriteRemove.o rewriteDefine.o \ - rewriteHandler.o rewriteManip.o rewriteSupport.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 index cb65c0502e..e640c1eaa5 100644 --- a/src/backend/rewrite/rewriteHandler.c +++ b/src/backend/rewrite/rewriteHandler.c @@ -25,6 +25,7 @@ #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" @@ -1670,48 +1671,91 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown) * Collect the RIR rules that we must apply */ rules = rel->rd_rules; - if (rules == NULL) + 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) + locks = NIL; + for (i = 0; i < rules->numLocks; i++) { - rule = lfirst(l); + rule = rules->rules[i]; + if (rule->event != CMD_SELECT) + continue; - parsetree = ApplyRetrieveRule(parsetree, - rule, - rt_index, - rel, - activeRIRs, - forUpdatePushedDown); + 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)))); + + /* + * Make sure we check for recursion in either securityQuals or + * WITH CHECK quals. + */ + if (rte->securityQuals != NIL) + { + activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); + + expression_tree_walker( (Node*) rte->securityQuals, + fireRIRonSubLink, (void*)activeRIRs ); + + activeRIRs = list_delete_first(activeRIRs); + } + + if (parsetree->withCheckOptions != NIL) + { + WithCheckOption *wco; + List *quals = NIL; + + wco = (WithCheckOption *) makeNode(WithCheckOption); + quals = lcons(wco->qual, quals); + + activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); + + expression_tree_walker( (Node*) quals, fireRIRonSubLink, + (void*)activeRIRs); } - activeRIRs = list_delete_first(activeRIRs); } heap_close(rel, NoLock); diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c new file mode 100644 index 0000000000..e1ccd1295e --- /dev/null +++ b/src/backend/rewrite/rowsecurity.c @@ -0,0 +1,557 @@ +/* + * rewrite/rowsecurity.c + * Routines to support policies for row-level security. + * + * Policies in PostgreSQL provide a mechanism to limit what records are + * returned to a user and what records a user is permitted to add to a table. + * + * Policies can be defined for specific roles, specific commands, or provided + * by an extension. Row security can also be enabled for a table without any + * policies being explicitly defined, in which case a default-deny policy is + * applied. + * + * Any part of the system which is returning records back to the user, or + * which is accepting records from the user to add to a table, needs to + * consider the policies associated with the table (if any). For normal + * queries, this is handled by calling prepend_row_security_policies() during + * rewrite, which looks at each RTE and adds the expressions defined by the + * policies to the securityQuals list for the RTE. For queries which modify + * the relation, any WITH CHECK policies are added to the list of + * WithCheckOptions for the Query and checked against each row which is being + * added to the table. Other parts of the system (eg: COPY) simply construct + * a normal query and use that, if RLS is to be applied. + * + * The check to see if RLS should be enabled is provided through + * check_enable_rls(), which returns an enum (defined in rowsecurity.h) to + * indicate if RLS should be enabled (RLS_ENABLED), or bypassed (RLS_NONE or + * RLS_NONE_ENV). RLS_NONE_ENV indicates that RLS should be bypassed + * in the current environment, but that may change if the row_security GUC or + * the current role changes. + * + * 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 List *pull_row_security_policies(CmdType cmd, Relation relation, + Oid user_id); +static void process_policies(List *policies, int rt_index, + Expr **final_qual, + Expr **final_with_check_qual, + bool *hassublinks); +static bool check_role_for_policy(RowSecurityPolicy *policy, Oid user_id); + +/* + * hook to allow extensions to apply their own security policy + * + * See below where the hook is called in prepend_row_security_policies for + * insight into how to use this hook. + */ +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) +{ + Expr *rowsec_expr = NULL; + Expr *rowsec_with_check_expr = NULL; + Expr *hook_expr = NULL; + Expr *hook_with_check_expr = NULL; + + List *rowsec_policies; + List *hook_policies = NIL; + + Relation rel; + Oid user_id; + int sec_context; + int rls_status; + bool defaultDeny = true; + bool hassublinks = false; + + /* This is just to get the security context */ + GetUserIdAndSecContext(&user_id, &sec_context); + + /* Switch to checkAsUser if it's set */ + user_id = rte->checkAsUser ? rte->checkAsUser : GetUserId(); + + /* + * If this is not a normal relation, or we have been told + * to explicitly skip RLS (perhaps because this is an FK check) + * then just return immediately. + */ + if (rte->relid < FirstNormalObjectId + || rte->relkind != RELKIND_RELATION + || (sec_context & SECURITY_ROW_LEVEL_DISABLED)) + return false; + + /* Determine the state of RLS for this, pass checkAsUser explicitly */ + rls_status = check_enable_rls(rte->relid, rte->checkAsUser); + + /* If there is no RLS on this table at all, nothing to do */ + if (rls_status == RLS_NONE) + return false; + + /* + * RLS_NONE_ENV means we are not doing any RLS now, but that may change + * with changes to the environment, so we mark it as hasRowSecurity to + * force a re-plan when the environment changes. + */ + if (rls_status == RLS_NONE_ENV) + { + /* + * Indicate that this query may involve RLS and must therefore + * be replanned if the environment changes (GUCs, role), but we + * are not adding anything here. + */ + root->hasRowSecurity = true; + + return false; + } + + /* + * We may end up getting called multiple times for the same RTE, so check + * to make sure we aren't doing double-work. + */ + if (rte->securityQuals != NIL) + return false; + + /* Grab the built-in policies which should be applied to this relation. */ + rel = heap_open(rte->relid, NoLock); + + rowsec_policies = pull_row_security_policies(root->commandType, rel, + user_id); + + /* + * Check if this is only the default-deny policy. + * + * Normally, if the table has row-security enabled but there are + * no policies, we use a default-deny policy and not allow anything. + * However, when an extension uses the hook to add their own + * policies, we don't want to include the default deny policy or + * there won't be any way for a user to use an extension exclusively + * for the policies to be used. + */ + if (((RowSecurityPolicy *) linitial(rowsec_policies))->rsecid + == InvalidOid) + defaultDeny = true; + + /* Now that we have our policies, build the expressions from them. */ + process_policies(rowsec_policies, rt_index, &rowsec_expr, + &rowsec_with_check_expr, &hassublinks); + + /* + * Also, allow extensions to add their own policies. + * + * Note that, as with the internal policies, if multiple policies are + * returned then they will be combined into a single expression with + * all of them OR'd together. However, to avoid the situation of an + * extension granting more access to a table than the internal policies + * would allow, the extension's policies are AND'd with the internal + * policies. In other words- extensions can only provide further + * filtering of the result set (or further reduce the set of records + * allowed to be added). + * + * If only a USING policy is returned by the extension then it will be + * used for WITH CHECK as well, similar to how internal policies are + * handled. + * + * The only caveat to this is that if there are NO internal policies + * defined, there ARE policies returned by the extension, and RLS is + * enabled on the table, then we will ignore the internally-generated + * default-deny policy and use only the policies returned by the + * extension. + */ + if (row_security_policy_hook) + { + hook_policies = (*row_security_policy_hook)(root->commandType, rel); + + /* Build the expression from any policies returned. */ + process_policies(hook_policies, rt_index, &hook_expr, + &hook_with_check_expr, &hassublinks); + } + + /* + * If the only built-in policy is the default-deny one, and hook + * policies exist, then use the hook policies only and do not apply + * the default-deny policy. Otherwise, apply both sets (AND'd + * together). + */ + if (defaultDeny && hook_policies != NIL) + rowsec_expr = NULL; + + /* + * For INSERT or UPDATE, we need to add the WITH CHECK quals to + * Query's withCheckOptions to verify that any new records pass the + * WITH CHECK policy (this will be a copy of the USING policy, if no + * explicit WITH CHECK policy exists). + */ + if (root->commandType == CMD_INSERT || root->commandType == CMD_UPDATE) + { + /* + * WITH CHECK OPTIONS wants a WCO node which wraps each Expr, so + * create them as necessary. + */ + if (rowsec_with_check_expr) + { + WithCheckOption *wco; + + wco = (WithCheckOption *) makeNode(WithCheckOption); + wco->viewname = RelationGetRelationName(rel); + wco->qual = (Node *) rowsec_with_check_expr; + wco->cascaded = false; + root->withCheckOptions = lcons(wco, root->withCheckOptions); + } + + /* + * Ditto for the expression, if any, returned from the extension. + */ + if (hook_with_check_expr) + { + WithCheckOption *wco; + + wco = (WithCheckOption *) makeNode(WithCheckOption); + wco->viewname = RelationGetRelationName(rel); + wco->qual = (Node *) hook_with_check_expr; + wco->cascaded = false; + root->withCheckOptions = lcons(wco, root->withCheckOptions); + } + } + + /* For SELECT, UPDATE, and DELETE, set the security quals */ + if (root->commandType == CMD_SELECT + || root->commandType == CMD_UPDATE + || root->commandType == CMD_DELETE) + { + if (rowsec_expr) + rte->securityQuals = lcons(rowsec_expr, rte->securityQuals); + + if (hook_expr) + rte->securityQuals = lcons(hook_expr, + 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 = true; + + /* + * If we have sublinks added because of the policies being added to the + * query, then set hasSubLinks on the Query to force subLinks to be + * properly expanded. + */ + if (hassublinks) + root->hasSubLinks = hassublinks; + + /* If we got this far, we must have added quals */ + return true; +} + +/* + * pull_row_security_policies + * + * Returns the list of policies to be added for this relation, based on the + * type of command and the roles to which it applies, from the relation cache. + * + */ +static List * +pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id) +{ + List *policies = NIL; + ListCell *item; + RowSecurityPolicy *policy; + + /* + * Row security is enabled for the relation and the row security GUC is + * either 'on' or 'force' here, so find the policies to apply to the table. + * There must always be at least one policy defined (may be the simple + * 'default-deny' policy, if none are explicitly defined on the table). + */ + foreach(item, relation->rsdesc->policies) + { + policy = (RowSecurityPolicy *) lfirst(item); + + /* Always add ALL policies, if they exist. */ + if (policy->cmd == '\0' && check_role_for_policy(policy, user_id)) + 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, user_id)) + 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, user_id)) + policies = lcons(policy, policies); + break; + case CMD_UPDATE: + if (policy->cmd == ACL_UPDATE_CHR + && check_role_for_policy(policy, user_id)) + policies = lcons(policy, policies); + break; + case CMD_DELETE: + if (policy->cmd == ACL_DELETE_CHR + && check_role_for_policy(policy, user_id)) + policies = lcons(policy, policies); + break; + default: + elog(ERROR, "unrecognized command type."); + break; + } + } + + /* + * There should always be a policy applied. If there are none found then + * create a simply defauly-deny policy (might be that policies exist but + * that none of them apply to the role which is querying the table). + */ + if (policies == NIL) + { + RowSecurityPolicy *policy = NULL; + Datum role; + + role = ObjectIdGetDatum(ACL_ID_PUBLIC); + + policy = palloc0(sizeof(RowSecurityPolicy)); + policy->policy_name = pstrdup("default-deny policy"); + policy->rsecid = InvalidOid; + policy->cmd = '\0'; + policy->roles = construct_array(&role, 1, OIDOID, sizeof(Oid), true, + 'i'); + policy->qual = (Expr *) makeConst(BOOLOID, -1, InvalidOid, + sizeof(bool), BoolGetDatum(false), + false, true); + policy->with_check_qual = copyObject(policy->qual); + policy->hassublinks = false; + + policies = list_make1(policy); + } + + Assert(policies != NIL); + + return policies; +} + +/* + * process_policies + * + * This will step through the policies which are passed in (which would come + * from either the built-in ones created on a table, or from policies provided + * by an extension through the hook provided), work out how to combine them, + * rewrite them as necessary, and produce an Expr for the normal security + * quals and an Expr for the with check quals. + * + * qual_eval, with_check_eval, and hassublinks are output variables + */ +static void +process_policies(List *policies, int rt_index, Expr **qual_eval, + Expr **with_check_eval, bool *hassublinks) +{ + ListCell *item; + List *quals = NIL; + List *with_check_quals = NIL; + + /* + * Extract the USING and WITH CHECK quals from each of the policies + * and add them to our lists. + */ + foreach(item, policies) + { + RowSecurityPolicy *policy = (RowSecurityPolicy *) lfirst(item); + + if (policy->qual != NULL) + quals = lcons(copyObject(policy->qual), quals); + + if (policy->with_check_qual != NULL) + with_check_quals = lcons(copyObject(policy->with_check_qual), + with_check_quals); + + if (policy->hassublinks) + *hassublinks = true; + } + + /* + * If we end up without any normal quals (perhaps the only policy matched + * was for INSERT), then create a single all-false one. + */ + if (quals == NIL) + quals = lcons(makeConst(BOOLOID, -1, InvalidOid, sizeof(bool), + BoolGetDatum(false), false, true), quals); + + /* + * If we end up with only USING quals, then use those as + * WITH CHECK quals also. + */ + if (with_check_quals == NIL) + with_check_quals = copyObject(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. + */ + ChangeVarNodes((Node *) 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(quals) > 1) + *qual_eval = makeBoolExpr(OR_EXPR, quals, -1); + else + *qual_eval = (Expr*) linitial(quals); + + /* + * 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_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1); + else + *with_check_eval = (Expr*) linitial(with_check_quals); + + return; +} + +/* + * check_enable_rls + * + * Determine, based on the relation, row_security setting, and current role, + * if RLS is applicable to this query. RLS_NONE_ENV indicates that, while + * RLS is not to be added for this query, a change in the environment may change + * that. RLS_NONE means that RLS is not on the relation at all and therefore + * we don't need to worry about it. RLS_ENABLED means RLS should be implemented + * for the table and the plan cache needs to be invalidated if the environment + * changes. + * + * Handle checking as another role via checkAsUser (for views, etc). + */ +int +check_enable_rls(Oid relid, Oid checkAsUser) +{ + HeapTuple tuple; + Form_pg_class classform; + bool relhasrowsecurity; + Oid user_id = checkAsUser ? checkAsUser : GetUserId(); + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid)); + if (!HeapTupleIsValid(tuple)) + return RLS_NONE; + + classform = (Form_pg_class) GETSTRUCT(tuple); + + relhasrowsecurity = classform->relhasrowsecurity; + + ReleaseSysCache(tuple); + + /* Nothing to do if the relation does not have RLS */ + if (!relhasrowsecurity) + return RLS_NONE; + + /* + * 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, then we bypass RLS unless row_security + * is set to 'force'. Note that superuser is always considered an owner. + * + * Return RLS_NONE_ENV to indicate that this decision depends on the + * environment (in this case, what the current values of user_id and + * row_security are). + */ + if (row_security != ROW_SECURITY_FORCE + && (pg_class_ownercheck(relid, user_id))) + return RLS_NONE_ENV; + + /* + * 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. + * + * Note that row_security is always considered 'on' when querying + * through a view or other cases where checkAsUser is true, so skip this + * if checkAsUser is in use. + */ + if (!checkAsUser && row_security == ROW_SECURITY_OFF) + { + if (has_bypassrls_privilege(user_id)) + /* OK to bypass */ + return RLS_NONE_ENV; + else + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("insufficient privilege to bypass row security."))); + } + + /* RLS should be fully enabled for this relation. */ + return RLS_ENABLED; +} + +/* + * check_role_for_policy - + * determines if the policy should be applied for the current role + */ +bool +check_role_for_policy(RowSecurityPolicy *policy, Oid user_id) +{ + 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(user_id, roles[i])) + return true; + } + + return false; +} diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c index e2c2d3d558..24aa2b3dc8 100644 --- a/src/backend/tcop/utility.c +++ b/src/backend/tcop/utility.c @@ -39,6 +39,7 @@ #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" @@ -1320,6 +1321,14 @@ ProcessUtilitySlow(Node *parsetree, ExecAlterDefaultPrivilegesStmt((AlterDefaultPrivilegesStmt *) parsetree); break; + case T_CreatePolicyStmt: /* CREATE POLICY */ + CreatePolicy((CreatePolicyStmt *) parsetree); + break; + + case T_AlterPolicyStmt: /* ALTER POLICY */ + AlterPolicy((AlterPolicyStmt *) parsetree); + break; + default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(parsetree)); @@ -1623,6 +1632,9 @@ AlterObjectTypeCommandTag(ObjectType objtype) case OBJECT_OPFAMILY: tag = "ALTER OPERATOR FAMILY"; break; + case OBJECT_POLICY: + tag = "ALTER POLICY"; + break; case OBJECT_ROLE: tag = "ALTER ROLE"; break; @@ -1944,6 +1956,9 @@ CreateCommandTag(Node *parsetree) case OBJECT_OPFAMILY: tag = "DROP OPERATOR FAMILY"; break; + case OBJECT_POLICY: + tag = "DROP POLICY"; + break; default: tag = "???"; } @@ -2287,6 +2302,14 @@ CreateCommandTag(Node *parsetree) tag = "ALTER TEXT SEARCH CONFIGURATION"; break; + case T_CreatePolicyStmt: + tag = "CREATE POLICY"; + break; + + case T_AlterPolicyStmt: + tag = "ALTER POLICY"; + break; + case T_PrepareStmt: tag = "PREPARE"; break; @@ -2831,6 +2854,14 @@ GetCommandLogLevel(Node *parsetree) lev = LOGSTMT_DDL; break; + case T_CreatePolicyStmt: + lev = LOGSTMT_DDL; + break; + + case T_AlterPolicyStmt: + lev = LOGSTMT_DDL; + break; + case T_AlterTSDictionaryStmt: lev = LOGSTMT_DDL; break; diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 38cd5b89c9..dc6eb2c8aa 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -117,7 +117,6 @@ static AclMode convert_role_priv_string(text *priv_type_text); 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); /* @@ -5126,7 +5125,7 @@ get_role_oid(const char *rolname, bool missing_ok) * get_role_oid_or_public - As above, but return ACL_ID_PUBLIC if the * role name is "public". */ -static Oid +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 index e4d7b2c34b..ed4a3769e4 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -2303,6 +2303,18 @@ RI_Initial_Check(Trigger *trigger, Relation fk_rel, Relation pk_rel) if (!ExecCheckRTPerms(list_make2(fkrte, pkrte), false)) return false; + /* + * Also punt if RLS is enabled on either table unless this role has the + * bypassrls right or is the table owner of the table(s) involved which + * have RLS enabled. + */ + if (!has_bypassrls_privilege(GetUserId()) && + ((pk_rel->rd_rel->relhasrowsecurity && + !pg_class_ownercheck(pkrte->relid, GetUserId())) || + (fk_rel->rd_rel->relhasrowsecurity && + !pg_class_ownercheck(fkrte->relid, GetUserId())))) + return false; + /*---------- * The query string built is: * SELECT fk.keycols FROM ONLY relname fk @@ -2956,6 +2968,7 @@ ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, 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 @@ -2968,8 +2981,22 @@ ri_PlanCheck(const char *querystr, int nargs, Oid *argtypes, /* 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, - save_sec_context | SECURITY_LOCAL_USERID_CHANGE); + 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 index d03d3b3cdf..26ef2fa6ff 100644 --- a/src/backend/utils/cache/plancache.c +++ b/src/backend/utils/cache/plancache.c @@ -53,12 +53,14 @@ #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" @@ -151,6 +153,8 @@ CreateCachedPlan(Node *raw_parse_tree, CachedPlanSource *plansource; MemoryContext source_context; MemoryContext oldcxt; + Oid user_id; + int security_context; Assert(query_string != NULL); /* required as of 8.4 */ @@ -173,6 +177,8 @@ CreateCachedPlan(Node *raw_parse_tree, */ 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); @@ -201,6 +207,11 @@ CreateCachedPlan(Node *raw_parse_tree, plansource->generic_cost = -1; plansource->total_custom_cost = 0; plansource->num_custom_plans = 0; + plansource->has_rls = false; + plansource->rowSecurityDisabled + = (security_context & SECURITY_ROW_LEVEL_DISABLED) != 0; + plansource->row_security_env = row_security; + plansource->planUserId = InvalidOid; MemoryContextSwitchTo(oldcxt); @@ -371,7 +382,8 @@ CompleteCachedPlan(CachedPlanSource *plansource, */ extract_query_dependencies((Node *) querytree_list, &plansource->relationOids, - &plansource->invalItems); + &plansource->invalItems, + &plansource->has_rls); /* * Also save the current search_path in the query_context. (This @@ -565,6 +577,17 @@ RevalidateCachedQuery(CachedPlanSource *plansource) return NIL; } + /* + * If this is a new cached plan, then set the user id it was planned by + * and under what row security settings; these are needed to determine + * plan invalidation when RLS is involved. + */ + if (!OidIsValid(plansource->planUserId)) + { + plansource->planUserId = GetUserId(); + plansource->row_security_env = row_security; + } + /* * 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 @@ -582,6 +605,23 @@ RevalidateCachedQuery(CachedPlanSource *plansource) } } + /* + * Check if row security is enabled for this query and things have changed + * such that we need to invalidate this plan and rebuild it. Note that if + * row security was explicitly disabled (eg: this is a FK check plan) then + * we don't invalidate due to RLS. + * + * Otherwise, if the plan has a possible RLS dependency, force a replan if + * either the role under which the plan was planned or the row_security + * setting has been changed. + */ + if (plansource->is_valid + && !plansource->rowSecurityDisabled + && plansource->has_rls + && (plansource->planUserId != GetUserId() + || plansource->row_security_env != row_security)) + plansource->is_valid = false; + /* * 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 @@ -723,7 +763,8 @@ RevalidateCachedQuery(CachedPlanSource *plansource) */ extract_query_dependencies((Node *) qlist, &plansource->relationOids, - &plansource->invalItems); + &plansource->invalItems, + &plansource->has_rls); /* * 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 index b6f03df6a4..e7f7129bd9 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -55,6 +55,7 @@ #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" @@ -966,6 +967,11 @@ RelationBuildDesc(Oid targetRelId, bool insertIt) else relation->trigdesc = NULL; + if (relation->rd_rel->relhasrowsecurity) + RelationBuildRowSecurity(relation); + else + relation->rsdesc = NULL; + /* * if it's an index, initialize index-related information */ @@ -1936,6 +1942,8 @@ RelationDestroyRelation(Relation relation, bool remember_tupdesc) 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); @@ -3242,8 +3250,8 @@ RelationCacheInitializePhase3(void) * wrong in the results from formrdesc or the relcache cache file. If we * faked up relcache entries using formrdesc, then read the real pg_class * rows and replace the fake entries with them. Also, if any of the - * relcache entries have rules or triggers, load that info the hard way - * since it isn't recorded in the cache file. + * relcache entries have rules, triggers, or security policies, load that + * info the hard way since it isn't recorded in the cache file. * * Whenever we access the catalogs to read data, there is a possibility of * a shared-inval cache flush causing relcache entries to be removed. @@ -3334,6 +3342,21 @@ RelationCacheInitializePhase3(void) restart = true; } + /* + * Re-load the row security policies if the relation has them, since + * they are not preserved in the cache. Note that we can never NOT + * have a policy while relhasrowsecurity is true- + * RelationBuildRowSecurity will create a single default-deny policy + * if there is no policy defined in pg_rowsecurity. + */ + if (relation->rd_rel->relhasrowsecurity && relation->rsdesc == NULL) + { + RelationBuildRowSecurity(relation); + + Assert (relation->rsdesc != NULL); + restart = true; + } + /* Release hold on the relation */ RelationDecrementReferenceCount(relation); @@ -4706,6 +4729,7 @@ load_relcache_init_file(bool shared) 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 index b87bfb3ff0..d208314258 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -61,6 +61,7 @@ #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" @@ -400,6 +401,23 @@ static const struct config_enum_entry huge_pages_options[] = { {NULL, 0, false} }; +/* + * 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 */ @@ -456,6 +474,8 @@ int tcp_keepalives_idle; 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. @@ -3517,6 +3537,16 @@ static struct config_enum ConfigureNamesEnum[] = 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 index 1b5f39fa6c..485d5d4b5c 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -90,6 +90,7 @@ #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 index 94e9147b13..2f855cf706 100644 --- a/src/bin/pg_dump/common.c +++ b/src/bin/pg_dump/common.c @@ -244,6 +244,10 @@ getSchemaData(Archive *fout, int *numTablesPtr) 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 index 25780cfc1a..921bc1ba36 100644 --- a/src/bin/pg_dump/pg_backup.h +++ b/src/bin/pg_dump/pg_backup.h @@ -150,6 +150,7 @@ typedef struct _restoreOptions 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 index ded9135c36..5476a1e7e2 100644 --- a/src/bin/pg_dump/pg_backup_archiver.c +++ b/src/bin/pg_dump/pg_backup_archiver.c @@ -373,6 +373,14 @@ RestoreArchive(Archive *AHX) ahprintf(AH, "BEGIN;\n\n"); } + /* + * 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. */ @@ -3242,6 +3250,7 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, RestoreOptions *ropt, bool isDat 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 index c084ee9d9e..29153294e2 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -137,6 +137,7 @@ static int no_security_labels = 0; 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); @@ -247,6 +248,7 @@ static char *myFormatType(const char *typname, int32 typmod); 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); @@ -345,6 +347,7 @@ main(int argc, char **argv) {"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}, @@ -825,6 +828,7 @@ main(int argc, char **argv) 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; @@ -897,6 +901,7 @@ help(const char *progname) 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")); @@ -1050,6 +1055,14 @@ setup_connection(Archive *AH, const char *dumpencoding, char *use_role) 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 @@ -2757,6 +2770,240 @@ dumpBlobs(Archive *fout, void *arg) 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_rsecwithcheck; + int i, j, ntups; + + if (fout->remoteVersion < 90500) + return; + + for (i = 0; i < numTables; i++) + { + TableInfo *tbinfo = &tblinfo[i]; + + /* Ignore row-security on tables not to be dumped */ + if (!tbinfo->dobj.dump) + continue; + + if (g_verbose) + write_msg(NULL, "reading row-security enabled for table \"%s\"", + tbinfo->dobj.name); + + /* + * Get row-security enabled information for the table. + * We represent RLS enabled on a table by creating RowSecurityInfo + * object with an empty policy. + */ + if (tbinfo->hasrowsec) + { + /* + * Note: use tableoid 0 so that this object won't be mistaken for + * something that pg_depend entries apply to. + */ + rsinfo = pg_malloc(sizeof(RowSecurityInfo)); + rsinfo->dobj.objType = DO_ROW_SECURITY; + rsinfo->dobj.catId.tableoid = 0; + rsinfo->dobj.catId.oid = tbinfo->dobj.catId.oid; + AssignDumpId(&rsinfo->dobj); + rsinfo->dobj.namespace = tbinfo->dobj.namespace; + rsinfo->dobj.name = pg_strdup(tbinfo->dobj.name); + rsinfo->rstable = tbinfo; + rsinfo->rsecpolname = NULL; + rsinfo->rseccmd = NULL; + rsinfo->rsecroles = NULL; + rsinfo->rsecqual = NULL; + rsinfo->rsecwithcheck = NULL; + } + + if (g_verbose) + write_msg(NULL, "reading row-security policies 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); + + /* Get the policies for the table. */ + appendPQExpBuffer(query, + "SELECT oid, tableoid, s.rsecpolname, s.rseccmd, " + "CASE WHEN s.rsecroles = '{0}' THEN 'PUBLIC' ELSE " + " array_to_string(ARRAY(SELECT rolname from pg_roles WHERE oid = ANY(s.rsecroles)), ', ') END AS rsecroles, " + "pg_get_expr(s.rsecqual, s.rsecrelid) AS rsecqual, " + "pg_get_expr(s.rsecwithcheck, s.rsecrelid) AS rsecwithcheck " + "FROM pg_catalog.pg_rowsecurity s " + "WHERE rsecrelid = '%u'", + tbinfo->dobj.catId.oid); + res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK); + + ntups = PQntuples(res); + + if (ntups == 0) + { + /* + * No explicit policies to handle (only the default-deny policy, + * which is handled as part of the table definition. Clean up and + * return. + */ + PQclear(res); + continue; + } + + 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"); + i_rsecwithcheck = PQfnumber(res, "rsecwithcheck"); + + rsinfo = pg_malloc(ntups * sizeof(RowSecurityInfo)); + + for (j = 0; j < ntups; j++) + { + 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); + rsinfo[j].dobj.namespace = tbinfo->dobj.namespace; + rsinfo[j].rstable = tbinfo; + rsinfo[j].rsecpolname = pg_strdup(PQgetvalue(res, j, + i_rsecpolname)); + + rsinfo[j].dobj.name = pg_strdup(rsinfo[j].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)); + + if (PQgetisnull(res, j, i_rsecqual)) + rsinfo[j].rsecqual = NULL; + else + rsinfo[j].rsecqual = pg_strdup(PQgetvalue(res, j, i_rsecqual)); + + if (PQgetisnull(res, j, i_rsecwithcheck)) + rsinfo[j].rsecwithcheck = NULL; + else + rsinfo[j].rsecwithcheck + = pg_strdup(PQgetvalue(res, j, i_rsecwithcheck)); + } + 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) + return; + + /* + * If rsecpolname is NULL, then this record is just indicating that ROW + * LEVEL SECURITY is enabled for the table. + * Dump as ALTER TABLE ENABLE ROW LEVEL SECURITY. + */ + if (rsinfo->rsecpolname == NULL) + { + query = createPQExpBuffer(); + + appendPQExpBuffer(query, "ALTER TABLE %s ENABLE ROW LEVEL SECURITY;", + fmtId(rsinfo->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_NONE, + query->data, "", NULL, + NULL, 0, + NULL, NULL); + + destroyPQExpBuffer(query); + 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 (rsinfo->rsecroles != NULL) + appendPQExpBuffer(query, " TO %s", rsinfo->rsecroles); + + if (rsinfo->rsecqual != NULL) + appendPQExpBuffer(query, " USING %s", rsinfo->rsecqual); + + if (rsinfo->rsecwithcheck != NULL) + appendPQExpBuffer(query, " WITH CHECK %s", rsinfo->rsecwithcheck); + + appendPQExpBuffer(query, ";\n"); + + 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); + destroyPQExpBuffer(delqry); +} + static void binary_upgrade_set_type_oids_by_type_oid(Archive *fout, PQExpBuffer upgrade_buffer, @@ -4287,6 +4534,7 @@ getTables(Archive *fout, int *numTables) int i_relhastriggers; int i_relhasindex; int i_relhasrules; + int i_relhasrowsec; int i_relhasoids; int i_relfrozenxid; int i_relminmxid; @@ -4328,7 +4576,7 @@ getTables(Archive *fout, int *numTables) * we cannot correctly identify inherited columns, owned sequences, etc. */ - if (fout->remoteVersion >= 90400) + if (fout->remoteVersion >= 90500) { /* * Left join to pick up dependency info linking sequences to their @@ -4340,6 +4588,48 @@ getTables(Archive *fout, int *numTables) "(%s c.relowner) AS rolname, " "c.relchecks, c.relhastriggers, " "c.relhasindex, c.relhasrules, c.relhasoids, " + "c.relhasrowsecurity, " + "c.relfrozenxid, c.relminmxid, tc.oid AS toid, " + "tc.relfrozenxid AS tfrozenxid, " + "tc.relminmxid AS tminmxid, " + "c.relpersistence, c.relispopulated, " + "c.relreplident, c.relpages, " + "CASE WHEN c.reloftype <> 0 THEN c.reloftype::pg_catalog.regtype ELSE NULL END AS reloftype, " + "d.refobjid AS owning_tab, " + "d.refobjsubid AS owning_col, " + "(SELECT spcname FROM pg_tablespace t WHERE t.oid = c.reltablespace) AS reltablespace, " + "array_to_string(array_remove(array_remove(c.reloptions,'check_option=local'),'check_option=cascaded'), ', ') AS reloptions, " + "CASE WHEN 'check_option=local' = ANY (c.reloptions) THEN 'LOCAL'::text " + "WHEN 'check_option=cascaded' = ANY (c.reloptions) THEN 'CASCADED'::text ELSE NULL END AS checkoption, " + "array_to_string(array(SELECT 'toast.' || x FROM unnest(tc.reloptions) x), ', ') AS toast_reloptions " + "FROM pg_class c " + "LEFT JOIN pg_depend d ON " + "(c.relkind = '%c' AND " + "d.classid = c.tableoid AND d.objid = c.oid AND " + "d.objsubid = 0 AND " + "d.refclassid = c.tableoid AND d.deptype = 'a') " + "LEFT JOIN pg_class tc ON (c.reltoastrelid = tc.oid) " + "WHERE c.relkind in ('%c', '%c', '%c', '%c', '%c', '%c') " + "ORDER BY c.oid", + username_subquery, + RELKIND_SEQUENCE, + RELKIND_RELATION, RELKIND_SEQUENCE, + RELKIND_VIEW, RELKIND_COMPOSITE_TYPE, + RELKIND_MATVIEW, RELKIND_FOREIGN_TABLE); + } + else if (fout->remoteVersion >= 90400) + { + /* + * Left join to pick up dependency info linking sequences to their + * owning column, if any (note this dependency is AUTO as of 8.2) + */ + appendPQExpBuffer(query, + "SELECT c.tableoid, c.oid, c.relname, " + "c.relacl, c.relkind, c.relnamespace, " + "(%s c.relowner) AS rolname, " + "c.relchecks, c.relhastriggers, " + "c.relhasindex, c.relhasrules, c.relhasoids, " + "'f'::bool AS relhasrowsecurity, " "c.relfrozenxid, c.relminmxid, tc.oid AS toid, " "tc.relfrozenxid AS tfrozenxid, " "tc.relminmxid AS tminmxid, " @@ -4380,6 +4670,7 @@ getTables(Archive *fout, int *numTables) "(%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, " @@ -4420,6 +4711,7 @@ getTables(Archive *fout, int *numTables) "(%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, " @@ -4458,6 +4750,7 @@ getTables(Archive *fout, int *numTables) "(%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, " @@ -4495,6 +4788,7 @@ getTables(Archive *fout, int *numTables) "(%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, " @@ -4532,6 +4826,7 @@ getTables(Archive *fout, int *numTables) "(%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, " @@ -4569,6 +4864,7 @@ getTables(Archive *fout, int *numTables) "(%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," @@ -4605,6 +4901,7 @@ getTables(Archive *fout, int *numTables) "(%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," @@ -4637,6 +4934,7 @@ getTables(Archive *fout, int *numTables) "(%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," @@ -4664,6 +4962,7 @@ getTables(Archive *fout, int *numTables) "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," @@ -4701,6 +5000,7 @@ getTables(Archive *fout, int *numTables) "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," @@ -4748,6 +5048,7 @@ getTables(Archive *fout, int *numTables) 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"); @@ -4799,6 +5100,7 @@ getTables(Archive *fout, int *numTables) 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)); @@ -7930,6 +8232,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj) 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 */ @@ -15332,6 +15637,7 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs, 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 index d184187580..b5d820e761 100644 --- a/src/bin/pg_dump/pg_dump.h +++ b/src/bin/pg_dump/pg_dump.h @@ -111,7 +111,8 @@ typedef enum DO_PRE_DATA_BOUNDARY, DO_POST_DATA_BOUNDARY, DO_EVENT_TRIGGER, - DO_REFRESH_MATVIEW + DO_REFRESH_MATVIEW, + DO_ROW_SECURITY } DumpableObjectType; typedef struct _dumpableObject @@ -245,6 +246,7 @@ typedef struct _tableInfo 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 */ @@ -486,6 +488,23 @@ typedef struct _blobInfo char *blobacl; } BlobInfo; +/* + * The RowSecurityInfo struct is used to represent row policies on a table and + * to indicate if a table has RLS enabled (ENABLE ROW SECURITY). If + * rsecpolname is NULL, then the record indicates ENABLE ROW SECURITY, while if + * it's non-NULL then this is a regular policy definition. + */ +typedef struct _rowSecurityInfo +{ + DumpableObject dobj; + TableInfo *rstable; + char *rsecpolname; /* null indicates RLS is enabled on rel */ + char *rseccmd; + char *rsecroles; + char *rsecqual; + char *rsecwithcheck; +} RowSecurityInfo; + /* global decls */ extern bool force_quotes; /* double-quotes for identifiers flag */ extern bool g_verbose; /* verbose flag */ @@ -577,5 +596,6 @@ extern DefaultACLInfo *getDefaultACLs(Archive *fout, int *numDefaultACLs); 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 index f0caa6b659..90aedee7d2 100644 --- a/src/bin/pg_dump/pg_dump_sort.c +++ b/src/bin/pg_dump/pg_dump_sort.c @@ -70,7 +70,8 @@ static const int oldObjectTypePriority[] = 10, /* DO_PRE_DATA_BOUNDARY */ 13, /* DO_POST_DATA_BOUNDARY */ 20, /* DO_EVENT_TRIGGER */ - 15 /* DO_REFRESH_MATVIEW */ + 15, /* DO_REFRESH_MATVIEW */ + 21 /* DO_ROW_SECURITY */ }; /* @@ -118,7 +119,8 @@ static const int newObjectTypePriority[] = 22, /* DO_PRE_DATA_BOUNDARY */ 25, /* DO_POST_DATA_BOUNDARY */ 32, /* DO_EVENT_TRIGGER */ - 33 /* DO_REFRESH_MATVIEW */ + 33, /* DO_REFRESH_MATVIEW */ + 34 /* DO_ROW_SECURITY */ }; static DumpId preDataBoundId; @@ -1434,6 +1436,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize) "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 index b2b3e6feb7..c25ea851d3 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -663,17 +663,29 @@ dumpRoles(PGconn *conn) i_rolpassword, i_rolvaliduntil, i_rolreplication, + i_rolbypassrls, i_rolcomment, i_is_current_user; int i; /* note: rolconfig is dumped later */ - if (server_version >= 90100) + 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 " @@ -684,6 +696,7 @@ dumpRoles(PGconn *conn) "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 " @@ -694,6 +707,7 @@ dumpRoles(PGconn *conn) "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 " @@ -724,6 +738,7 @@ dumpRoles(PGconn *conn) "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 " @@ -743,6 +758,7 @@ dumpRoles(PGconn *conn) 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"); @@ -810,6 +826,11 @@ dumpRoles(PGconn *conn) 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 index fdfdc19c3f..1c1b80f137 100644 --- a/src/bin/pg_dump/pg_restore.c +++ b/src/bin/pg_dump/pg_restore.c @@ -70,6 +70,7 @@ main(int argc, char **argv) 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; @@ -111,6 +112,7 @@ main(int argc, char **argv) * 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}, @@ -333,6 +335,7 @@ main(int argc, char **argv) } 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; @@ -460,6 +463,7 @@ usage(const char *progname) 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 index 282cd432a2..97dc2dded2 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -742,7 +742,7 @@ permissionsList(const char *pattern) PQExpBufferData buf; PGresult *res; printQueryOpt myopt = pset.popt; - static const bool translate_columns[] = {false, false, true, false, false}; + static const bool translate_columns[] = {false, false, true, false, false, false}; initPQExpBuffer(&buf); @@ -778,7 +778,38 @@ permissionsList(const char *pattern) " 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")); + 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" @@ -1173,6 +1204,7 @@ describeOneTableDetails(const char *schemaname, bool hasindex; bool hasrules; bool hastriggers; + bool hasrowsecurity; bool hasoids; Oid tablespace; char *reloptions; @@ -1194,11 +1226,28 @@ describeOneTableDetails(const char *schemaname, initPQExpBuffer(&tmpbuf); /* Get general table info */ - if (pset.sversion >= 90400) + if (pset.sversion >= 90500) { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " - "c.relhastriggers, c.relhasoids, " + "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" @@ -1215,7 +1264,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " - "c.relhastriggers, c.relhasoids, " + "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" @@ -1232,7 +1281,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " - "c.relhastriggers, c.relhasoids, " + "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 " @@ -1248,7 +1297,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, " - "c.relhastriggers, c.relhasoids, " + "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" @@ -1263,7 +1312,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " - "reltriggers <> 0, relhasoids, " + "reltriggers <> 0, false, relhasoids, " "%s, reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", (verbose ? @@ -1274,7 +1323,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " - "reltriggers <> 0, relhasoids, " + "reltriggers <> 0, false, relhasoids, " "'', reltablespace\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); @@ -1283,7 +1332,7 @@ describeOneTableDetails(const char *schemaname, { printfPQExpBuffer(&buf, "SELECT relchecks, relkind, relhasindex, relhasrules, " - "reltriggers <> 0, relhasoids, " + "reltriggers <> 0, false, relhasoids, " "'', ''\n" "FROM pg_catalog.pg_class WHERE oid = '%s';", oid); @@ -1306,18 +1355,19 @@ describeOneTableDetails(const char *schemaname, 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.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, 6)) : NULL; + pg_strdup(PQgetvalue(res, 0, 7)) : NULL; tableinfo.tablespace = (pset.sversion >= 80000) ? - atooid(PQgetvalue(res, 0, 7)) : 0; + atooid(PQgetvalue(res, 0, 8)) : 0; tableinfo.reloftype = (pset.sversion >= 90000 && - strcmp(PQgetvalue(res, 0, 8), "") != 0) ? - pg_strdup(PQgetvalue(res, 0, 8)) : NULL; + strcmp(PQgetvalue(res, 0, 9), "") != 0) ? + pg_strdup(PQgetvalue(res, 0, 9)) : NULL; tableinfo.relpersistence = (pset.sversion >= 90100) ? - *(PQgetvalue(res, 0, 9)) : 0; + *(PQgetvalue(res, 0, 10)) : 0; tableinfo.relreplident = (pset.sversion >= 90400) ? - *(PQgetvalue(res, 0, 10)) : 'd'; + *(PQgetvalue(res, 0, 11)) : 'd'; PQclear(res); res = NULL; @@ -1948,6 +1998,67 @@ describeOneTableDetails(const char *schemaname, 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_roles where oid = any (rs.rsecroles) order by 1) END,\n" + "pg_catalog.pg_get_expr(rs.rsecqual, rs.rsecrelid),\n" + "pg_catalog.pg_get_expr(rs.rsecwithcheck, 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++) + { + printfPQExpBuffer(&buf, " POLICY \"%s\"", + PQgetvalue(result, i, 0)); + + if (!PQgetisnull(result, i, 4)) + appendPQExpBuffer(&buf, " (%s)", + PQgetvalue(result, i, 4)); + + if (!PQgetisnull(result, i, 2)) + appendPQExpBuffer(&buf, " EXPRESSION %s", + PQgetvalue(result, i, 2)); + + if (!PQgetisnull(result, i, 3)) + appendPQExpBuffer(&buf, " WITH CHECK %s", + PQgetvalue(result, i, 3)); + + 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') { @@ -2529,6 +2640,11 @@ describeRoles(const char *pattern, bool verbose) 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/bin/psql/tab-complete.c b/src/bin/psql/tab-complete.c index b80fe13168..a4594b6783 100644 --- a/src/bin/psql/tab-complete.c +++ b/src/bin/psql/tab-complete.c @@ -782,6 +782,7 @@ static const pgsql_thing_t words_after_create[] = { * good idea. */ {"OWNED", NULL, NULL, THING_NO_CREATE}, /* for DROP OWNED BY ... */ {"PARSER", Query_for_list_of_ts_parsers, NULL, THING_NO_SHOW}, + {"POLICY", NULL, NULL}, {"ROLE", Query_for_list_of_roles}, {"RULE", "SELECT pg_catalog.quote_ident(rulename) FROM pg_catalog.pg_rules WHERE substring(pg_catalog.quote_ident(rulename),1,%d)='%s'"}, {"SCHEMA", Query_for_list_of_schemas}, @@ -971,7 +972,7 @@ psql_completion(const char *text, int start, int end) {"AGGREGATE", "COLLATION", "CONVERSION", "DATABASE", "DEFAULT PRIVILEGES", "DOMAIN", "EVENT TRIGGER", "EXTENSION", "FOREIGN DATA WRAPPER", "FOREIGN TABLE", "FUNCTION", "GROUP", "INDEX", "LANGUAGE", "LARGE OBJECT", "MATERIALIZED VIEW", "OPERATOR", - "ROLE", "RULE", "SCHEMA", "SERVER", "SEQUENCE", "SYSTEM", "TABLE", + "POLICY", "ROLE", "RULE", "SCHEMA", "SERVER", "SEQUENCE", "SYSTEM", "TABLE", "TABLESPACE", "TEXT SEARCH", "TRIGGER", "TYPE", "USER", "USER MAPPING FOR", "VIEW", NULL}; @@ -1398,6 +1399,44 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_LIST(list_ALTERMATVIEW); } + /* ALTER POLICY ON */ + else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 && + pg_strcasecmp(prev2_wd, "POLICY") == 0) + COMPLETE_WITH_CONST("ON"); + /* ALTER POLICY ON
*/ + else if (pg_strcasecmp(prev4_wd, "ALTER") == 0 && + pg_strcasecmp(prev3_wd, "POLICY") == 0 && + pg_strcasecmp(prev_wd, "ON") == 0) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + /* ALTER POLICY ON
- show options */ + else if (pg_strcasecmp(prev5_wd, "ALTER") == 0 && + pg_strcasecmp(prev4_wd, "POLICY") == 0 && + pg_strcasecmp(prev2_wd, "ON") == 0) + { + static const char *const list_ALTERPOLICY[] = + {"RENAME TO", "TO", "USING", "WITH CHECK", NULL}; + + COMPLETE_WITH_LIST(list_ALTERPOLICY); + } + /* ALTER POLICY ON
TO */ + else if (pg_strcasecmp(prev6_wd, "ALTER") == 0 && + pg_strcasecmp(prev5_wd, "POLICY") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "TO") == 0) + COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles); + /* ALTER POLICY ON
USING ( */ + else if (pg_strcasecmp(prev6_wd, "ALTER") == 0 && + pg_strcasecmp(prev5_wd, "POLICY") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "USING") == 0) + COMPLETE_WITH_CONST("("); + /* ALTER POLICY ON
WITH CHECK ( */ + else if (pg_strcasecmp(prev6_wd, "POLICY") == 0 && + pg_strcasecmp(prev4_wd, "ON") == 0 && + pg_strcasecmp(prev2_wd, "WITH") == 0 && + pg_strcasecmp(prev_wd, "CHECK") == 0) + COMPLETE_WITH_CONST("("); + /* ALTER RULE , add ON */ else if (pg_strcasecmp(prev3_wd, "ALTER") == 0 && pg_strcasecmp(prev2_wd, "RULE") == 0) @@ -1462,7 +1501,7 @@ psql_completion(const char *text, int start, int end) pg_strcasecmp(prev_wd, "ENABLE") == 0) { static const char *const list_ALTERENABLE[] = - {"ALWAYS", "REPLICA", "RULE", "TRIGGER", NULL}; + {"ALWAYS", "REPLICA", "ROW LEVEL SECURITY", "RULE", "TRIGGER", NULL}; COMPLETE_WITH_LIST(list_ALTERENABLE); } @@ -1529,7 +1568,7 @@ psql_completion(const char *text, int start, int end) pg_strcasecmp(prev_wd, "DISABLE") == 0) { static const char *const list_ALTERDISABLE[] = - {"RULE", "TRIGGER", NULL}; + { "ROW LEVEL SECURITY", "RULE", "TRIGGER", NULL}; COMPLETE_WITH_LIST(list_ALTERDISABLE); } @@ -1549,6 +1588,16 @@ psql_completion(const char *text, int start, int end) completion_info_charp = prev3_wd; COMPLETE_WITH_QUERY(Query_for_trigger_of_table); } + else if (pg_strcasecmp(prev4_wd, "DISABLE") == 0 && + pg_strcasecmp(prev3_wd, "ROW") == 0 && + pg_strcasecmp(prev2_wd, "LEVEL") == 0 && + pg_strcasecmp(prev_wd, "SECURITY") == 0) + { + static const char *const list_DISABLERLS[] = + { "CASCADE", NULL}; + + COMPLETE_WITH_LIST(list_DISABLERLS); + } /* ALTER TABLE xxx ALTER */ else if (pg_strcasecmp(prev4_wd, "ALTER") == 0 && @@ -2251,12 +2300,103 @@ psql_completion(const char *text, int start, int end) pg_strcasecmp(prev_wd, "(") == 0) COMPLETE_WITH_ATTR(prev4_wd, ""); /* Complete USING with an index method */ - else if (pg_strcasecmp(prev_wd, "USING") == 0) + else if ((pg_strcasecmp(prev6_wd, "INDEX") == 0 || + pg_strcasecmp(prev5_wd, "INDEX") == 0 || + pg_strcasecmp(prev4_wd, "INDEX") == 0) && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "USING") == 0) COMPLETE_WITH_QUERY(Query_for_list_of_access_methods); else if (pg_strcasecmp(prev4_wd, "ON") == 0 && + (!(pg_strcasecmp(prev6_wd, "POLICY") == 0) && + !(pg_strcasecmp(prev4_wd, "FOR") == 0)) && pg_strcasecmp(prev2_wd, "USING") == 0) COMPLETE_WITH_CONST("("); + /* CREATE POLICY */ + /* Complete "CREATE POLICY ON" */ + else if (pg_strcasecmp(prev3_wd, "CREATE") == 0 && + pg_strcasecmp(prev2_wd, "POLICY") == 0) + COMPLETE_WITH_CONST("ON"); + /* Complete "CREATE POLICY ON
" */ + else if (pg_strcasecmp(prev4_wd, "CREATE") == 0 && + pg_strcasecmp(prev3_wd, "POLICY") == 0 && + pg_strcasecmp(prev_wd, "ON") == 0) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + /* Complete "CREATE POLICY ON
FOR|TO|USING|WITH CHECK" */ + else if (pg_strcasecmp(prev5_wd, "CREATE") == 0 && + pg_strcasecmp(prev4_wd, "POLICY") == 0 && + pg_strcasecmp(prev2_wd, "ON") == 0) + { + static const char *const list_POLICYOPTIONS[] = + {"FOR", "TO", "USING", "WITH CHECK", NULL}; + + COMPLETE_WITH_LIST(list_POLICYOPTIONS); + } + /* Complete "CREATE POLICY ON
FOR ALL|SELECT|INSERT|UPDATE|DELETE" */ + else if (pg_strcasecmp(prev6_wd, "CREATE") == 0 && + pg_strcasecmp(prev5_wd, "POLICY") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "FOR") == 0) + { + static const char *const list_POLICYCMDS[] = + {"ALL", "SELECT", "INSERT", "UPDATE", "DELETE", NULL}; + + COMPLETE_WITH_LIST(list_POLICYCMDS); + } + /* Complete "CREATE POLICY ON
FOR INSERT TO|WITH CHECK" */ + else if (pg_strcasecmp(prev6_wd, "POLICY") == 0 && + pg_strcasecmp(prev4_wd, "ON") == 0 && + pg_strcasecmp(prev2_wd, "FOR") == 0 && + pg_strcasecmp(prev_wd, "INSERT") == 0) + { + static const char *const list_POLICYOPTIONS[] = + {"TO", "WITH CHECK", NULL}; + + COMPLETE_WITH_LIST(list_POLICYOPTIONS); + } + /* + * Complete "CREATE POLICY ON
FOR SELECT TO|USING" + * Complete "CREATE POLICY ON
FOR DELETE TO|USING" + */ + else if (pg_strcasecmp(prev6_wd, "POLICY") == 0 && + pg_strcasecmp(prev4_wd, "ON") == 0 && + pg_strcasecmp(prev2_wd, "FOR") == 0 && + (pg_strcasecmp(prev_wd, "SELECT") == 0 || + pg_strcasecmp(prev_wd, "DELETE") == 0)) + { + static const char *const list_POLICYOPTIONS[] = + {"TO", "USING", NULL}; + + COMPLETE_WITH_LIST(list_POLICYOPTIONS); + } + /* + * Complete "CREATE POLICY ON
FOR ALL TO|USING|WITH CHECK" + * Complete "CREATE POLICY ON
FOR UPDATE TO|USING|WITH CHECK" + */ + else if (pg_strcasecmp(prev6_wd, "POLICY") == 0 && + pg_strcasecmp(prev4_wd, "ON") == 0 && + pg_strcasecmp(prev2_wd, "FOR") == 0 && + (pg_strcasecmp(prev_wd, "ALL") == 0 || + pg_strcasecmp(prev_wd, "UPDATE") == 0)) + { + static const char *const list_POLICYOPTIONS[] = + {"TO", "USING", "WITH CHECK", NULL}; + + COMPLETE_WITH_LIST(list_POLICYOPTIONS); + } + /* Complete "CREATE POLICY ON
TO " */ + else if (pg_strcasecmp(prev6_wd, "CREATE") == 0 && + pg_strcasecmp(prev5_wd, "POLICY") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "TO") == 0) + COMPLETE_WITH_QUERY(Query_for_list_of_grant_roles); + /* Complete "CREATE POLICY ON
USING (" */ + else if (pg_strcasecmp(prev6_wd, "CREATE") == 0 && + pg_strcasecmp(prev5_wd, "POLICY") == 0 && + pg_strcasecmp(prev3_wd, "ON") == 0 && + pg_strcasecmp(prev_wd, "USING") == 0) + COMPLETE_WITH_CONST("("); + /* CREATE RULE */ /* Complete "CREATE RULE " with "AS" */ else if (pg_strcasecmp(prev3_wd, "CREATE") == 0 && @@ -2726,6 +2866,16 @@ psql_completion(const char *text, int start, int end) COMPLETE_WITH_QUERY(Query_for_list_of_event_triggers); } + /* DROP POLICY ON */ + else if (pg_strcasecmp(prev3_wd, "DROP") == 0 && + pg_strcasecmp(prev2_wd, "POLICY") == 0) + COMPLETE_WITH_CONST("ON"); + /* DROP POLICY ON
*/ + else if (pg_strcasecmp(prev4_wd, "DROP") == 0 && + pg_strcasecmp(prev3_wd, "POLICY") == 0 && + pg_strcasecmp(prev_wd, "ON") == 0) + COMPLETE_WITH_SCHEMA_QUERY(Query_for_list_of_tables, NULL); + /* DROP RULE */ else if (pg_strcasecmp(prev3_wd, "DROP") == 0 && pg_strcasecmp(prev2_wd, "RULE") == 0) diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index a36dbeb80b..af0475e831 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201409162 +#define CATALOG_VERSION_NO 201409191 #endif diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h index 8ed259283a..6a4913a66e 100644 --- a/src/include/catalog/dependency.h +++ b/src/include/catalog/dependency.h @@ -147,6 +147,7 @@ typedef enum ObjectClass 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 index 59576fd751..870692cf54 100644 --- a/src/include/catalog/indexing.h +++ b/src/include/catalog/indexing.h @@ -299,6 +299,12 @@ DECLARE_UNIQUE_INDEX(pg_extension_name_index, 3081, on pg_extension using btree( 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 index e7c32c9fa6..3b63d2bb9e 100644 --- a/src/include/catalog/pg_authid.h +++ b/src/include/catalog/pg_authid.h @@ -52,6 +52,7 @@ CATALOG(pg_authid,1260) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2842) BKI_SCHEMA_MAC 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! */ @@ -73,7 +74,7 @@ typedef FormData_pg_authid *Form_pg_authid; * compiler constants for pg_authid * ---------------- */ -#define Natts_pg_authid 11 +#define Natts_pg_authid 12 #define Anum_pg_authid_rolname 1 #define Anum_pg_authid_rolsuper 2 #define Anum_pg_authid_rolinherit 3 @@ -82,9 +83,10 @@ typedef FormData_pg_authid *Form_pg_authid; #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 +#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 @@ -93,7 +95,7 @@ typedef FormData_pg_authid *Form_pg_authid; * user choices. * ---------------- */ -DATA(insert OID = 10 ( "POSTGRES" t t t t t t t -1 _null_ _null_ )); +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 index f2fb317e5f..f6353514ca 100644 --- a/src/include/catalog/pg_class.h +++ b/src/include/catalog/pg_class.h @@ -65,6 +65,7 @@ CATALOG(pg_class,1259) BKI_BOOTSTRAP BKI_ROWTYPE_OID(83) BKI_SCHEMA_MACRO 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 */ @@ -94,7 +95,7 @@ typedef FormData_pg_class *Form_pg_class; * ---------------- */ -#define Natts_pg_class 29 +#define Natts_pg_class 30 #define Anum_pg_class_relname 1 #define Anum_pg_class_relnamespace 2 #define Anum_pg_class_reltype 3 @@ -118,12 +119,13 @@ typedef FormData_pg_class *Form_pg_class; #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 +#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 @@ -138,13 +140,13 @@ typedef FormData_pg_class *Form_pg_class; * 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_ )); +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 t n 3 1 _null_ _null_ )); +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 t n 3 1 _null_ _null_ )); +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 29 0 t f f f f t n 3 1 _null_ _null_ )); +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 0000000000..2638d5e684 --- /dev/null +++ b/src/include/catalog/pg_rowsecurity.h @@ -0,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 \0 for all */ + +#ifdef CATALOG_VARLEN + Oid rsecroles[1] /* Roles associated with policy, not-NULL */ + 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 0000000000..95d8a6d117 --- /dev/null +++ b/src/include/commands/policy.h @@ -0,0 +1,33 @@ +/*------------------------------------------------------------------------- + * + * 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); + +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 index 3807955c8e..2ba98856ff 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -272,6 +272,7 @@ extern int trace_recovery(int trace_level); /* 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 index a031b88b87..154d943d58 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -366,6 +366,8 @@ typedef enum NodeTag T_RefreshMatViewStmt, T_ReplicaIdentityStmt, T_AlterSystemStmt, + T_CreatePolicyStmt, + T_AlterPolicyStmt, /* * TAGS FOR PARSE TREE NODES (parsenodes.h) diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index d2c0b29c0d..f3aa69e4a1 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -120,6 +120,7 @@ typedef struct Query 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) */ @@ -1224,6 +1225,7 @@ typedef enum ObjectType OBJECT_OPCLASS, OBJECT_OPERATOR, OBJECT_OPFAMILY, + OBJECT_POLICY, OBJECT_ROLE, OBJECT_RULE, OBJECT_SCHEMA, @@ -1333,6 +1335,8 @@ typedef enum AlterTableType 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; @@ -1855,6 +1859,35 @@ typedef struct ImportForeignSchemaStmt 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; + /* ---------------------- * Create TRIGGER Statement * ---------------------- diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 3b9c683829..18394946f8 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -67,6 +67,9 @@ typedef struct PlannedStmt List *invalItems; /* other dependencies, as PlanInvalItems */ int nParamExec; /* number of PARAM_EXEC Params used */ + + bool has_rls; /* row-security applied? */ + } 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 index dacbe9cc0b..f1a0504c0d 100644 --- a/src/include/nodes/relation.h +++ b/src/include/nodes/relation.h @@ -99,6 +99,9 @@ typedef struct PlannerGlobal Index lastRowMarkId; /* highest PlanRowMark ID assigned */ bool transientPlan; /* redo plan when TransactionXmin changes? */ + + bool has_rls; /* row-security is applied? */ + } 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 index 450425046a..3fdc2cba0e 100644 --- a/src/include/optimizer/planmain.h +++ b/src/include/optimizer/planmain.h @@ -135,6 +135,7 @@ extern void set_sa_opfuncid(ScalarArrayOpExpr *opexpr); extern void record_plan_function_dependency(PlannerInfo *root, Oid funcid); extern void extract_query_dependencies(Node *query, List **relationOids, - List **invalItems); + List **invalItems, + bool *hasRowSecurity); #endif /* PLANMAIN_H */ diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 17888ad0ec..3c8c1b9e25 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -284,6 +284,7 @@ PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD) 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/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h new file mode 100644 index 0000000000..245005cae2 --- /dev/null +++ b/src/include/rewrite/rowsecurity.h @@ -0,0 +1,80 @@ +/* ------------------------------------------------------------------------- + * + * 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; + +/* + * Used by callers of check_enable_rls. + * + * RLS could be completely disabled on the tables involved in the query, + * which is the simple case, or it may depend on the current environment + * (the role which is running the query or the value of the row_security + * GUC- on, off, or force), or it might be simply enabled as usual. + * + * If RLS isn't on the table involved then RLS_NONE is returned to indicate + * that we don't need to worry about invalidating the query plan for RLS + * reasons. If RLS is on the table, but we are bypassing it for now, then + * we return RLS_NONE_ENV to indicate that, if the environment changes, + * we need to invalidate and replan. Finally, if RLS should be turned on + * for the query, then we return RLS_ENABLED, which means we also need to + * invalidate if the environment changes. + */ +enum CheckEnableRlsResult +{ + RLS_NONE, + RLS_NONE_ENV, + RLS_ENABLED +}; + +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); + +extern int check_enable_rls(Oid relid, Oid checkAsUser); + +#endif /* ROWSECURITY_H */ diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 9430baa4a0..a8e3164659 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -228,6 +228,7 @@ extern bool is_member_of_role_nosuper(Oid member, Oid role); 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, @@ -326,5 +327,6 @@ extern bool pg_foreign_server_ownercheck(Oid srv_oid, Oid roleid); 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 index cfbfaa26cc..56bf4bb7af 100644 --- a/src/include/utils/plancache.h +++ b/src/include/utils/plancache.h @@ -93,6 +93,7 @@ typedef struct CachedPlanSource 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 */ @@ -108,6 +109,9 @@ typedef struct CachedPlanSource 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? */ + int row_security_env; /* row security setting when planned */ + bool rowSecurityDisabled; /* is row-security disabled? */ } CachedPlanSource; /* diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index 37b6cbbb4d..198b98f2f8 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -21,6 +21,7 @@ #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" @@ -105,6 +106,7 @@ typedef struct RelationData 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 index bcff8ddc17..09b02127f9 100644 --- a/src/test/regress/expected/dependency.out +++ b/src/test/regress/expected/dependency.out @@ -63,21 +63,21 @@ CREATE TABLE deptest (a serial primary key, b text); 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 | + 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 access privileges ---------+----------+-------+-------------------------------------------+-------------------------- - public | deptest1 | table | regression_user0=arwdDxt/regression_user0 | + 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 index 1675075f68..5359dd8536 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -1425,39 +1425,39 @@ grant select on dep_priv_test to regressuser4 with grant option; 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 | + 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 access privileges ---------+---------------+-------+-----------------------------------+-------------------------- - public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| - | | | regressuser2=r*/regressuser1 +| - | | | regressuser3=r*/regressuser1 +| - | | | regressuser4=r*/regressuser3 +| - | | | regressuser5=r/regressuser4 | + 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 access privileges ---------+---------------+-------+-----------------------------------+-------------------------- - public | dep_priv_test | table | regressuser1=arwdDxt/regressuser1+| - | | | regressuser2=r*/regressuser1 +| - | | | regressuser3=r*/regressuser1 | + 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 0000000000..007afc606b --- /dev/null +++ b/src/test/regress/expected/rowsecurity.out @@ -0,0 +1,2236 @@ +-- +-- 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'); +ALTER TABLE document ENABLE ROW LEVEL SECURITY; +-- 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 + 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); +ALTER TABLE category ENABLE ROW LEVEL SECURITY; +-- 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 +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE t2 ENABLE ROW LEVEL SECURITY; +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)); +ALTER TABLE rec1 ENABLE ROW LEVEL SECURITY; +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)); +ALTER TABLE rec2 ENABLE ROW LEVEL SECURITY; +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_user1; +CREATE VIEW rec1v AS SELECT * FROM rec1; +CREATE VIEW rec2v AS SELECT * FROM rec2; +SET SESSION AUTHORIZATION rls_regress_user0; +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_user1; +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; +SET SESSION AUTHORIZATION rls_regress_user0; +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); +GRANT SELECT ON s1, s2 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%')); +CREATE POLICY p3 ON s1 FOR INSERT WITH CHECK (a = (SELECT a FROM s1)); +ALTER TABLE s1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE s2 ENABLE ROW LEVEL SECURITY; +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; +SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) +ERROR: infinite recursion detected in row-security policy for relation "s1" +INSERT INTO s1 VALUES (1, 'foo'); -- fail (infinite recursion) +ERROR: infinite recursion detected in row-security policy for relation "s1" +SET SESSION AUTHORIZATION rls_regress_user0; +DROP POLICY p3 on s1; +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); +GRANT SELECT ON z1 TO rls_regress_group1, rls_regress_group2, + rls_regress_user1, rls_regress_user2; +INSERT INTO z1 VALUES + (1, 'aaa'), + (2, 'bbb'), + (3, 'ccc'), + (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); +ALTER TABLE z1 ENABLE ROW LEVEL SECURITY; +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) + +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 0) +(4 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) + +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 0) +(4 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) + +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 1) +(4 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) + +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 1) +(4 rows) + +-- +-- Views should follow policy for view owner. +-- +-- View and Table owner are the same. +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +GRANT SELECT ON rls_view TO rls_regress_user1; +-- Query as role that is not owner of view or table. Should return all records. +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM rls_view; +NOTICE: f_leak => aaa +NOTICE: f_leak => bbb +NOTICE: f_leak => ccc +NOTICE: f_leak => ddd + a | b +---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd +(4 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + QUERY PLAN +--------------------- + Seq Scan on z1 + Filter: f_leak(b) +(2 rows) + +-- Query as view/table owner. Should return all records. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM rls_view; +NOTICE: f_leak => aaa +NOTICE: f_leak => bbb +NOTICE: f_leak => ccc +NOTICE: f_leak => ddd + a | b +---+----- + 1 | aaa + 2 | bbb + 3 | ccc + 4 | ddd +(4 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + QUERY PLAN +--------------------- + Seq Scan on z1 + Filter: f_leak(b) +(2 rows) + +DROP VIEW rls_view; +-- View and Table owners are different. +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +GRANT SELECT ON rls_view TO rls_regress_user0; +-- Query as role that is not owner of view but is owner of table. +-- Should return records based on view owner policies. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM rls_view; +NOTICE: f_leak => bbb +NOTICE: f_leak => ddd + a | b +---+----- + 2 | bbb + 4 | ddd +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 0) +(4 rows) + +-- Query as role that is not owner of table but is owner of view. +-- Should return records based on view owner policies. +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM rls_view; +NOTICE: f_leak => bbb +NOTICE: f_leak => ddd + a | b +---+----- + 2 | bbb + 4 | ddd +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 0) +(4 rows) + +-- Query as role that is not the owner of the table or view without permissions. +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM rls_view; --fail - permission denied. +ERROR: permission denied for relation rls_view +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied. +ERROR: permission denied for relation rls_view +-- Query as role that is not the owner of the table or view with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +GRANT SELECT ON rls_view TO rls_regress_user2; +SELECT * FROM rls_view; +NOTICE: f_leak => bbb +NOTICE: f_leak => ddd + a | b +---+----- + 2 | bbb + 4 | ddd +(2 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + QUERY PLAN +------------------------------- + Subquery Scan on z1 + Filter: f_leak(z1.b) + -> Seq Scan on z1 z1_1 + Filter: ((a % 2) = 0) +(4 rows) + +SET SESSION AUTHORIZATION rls_regress_user1; +DROP VIEW rls_view; +-- +-- 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); +ALTER TABLE x1 ENABLE ROW LEVEL SECURITY; +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 +ALTER TABLE y1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE y2 ENABLE ROW LEVEL SECURITY; +-- +-- Expression structure with SBV +-- +-- Create view as table owner. RLS should NOT be applied. +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); + QUERY PLAN +----------------------------------- + Seq Scan on y1 + Filter: (f_leak(b) AND (a = 1)) +(2 rows) + +DROP VIEW rls_sbv; +-- Create view as role that does not own table. RLS should be applied. +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); +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) + +DROP VIEW rls_sbv; +-- +-- 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); +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; +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) + +-- +-- CTE and RLS +-- +RESET SESSION AUTHORIZATION; +DROP TABLE t1 CASCADE; +CREATE TABLE t1 (a integer, b text); +CREATE POLICY p1 ON t1 USING (a % 2 = 0); +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; +GRANT ALL ON t1 TO rls_regress_user1; +INSERT INTO t1 (SELECT x, md5(x::text) FROM generate_series(0,20) x); +SET SESSION AUTHORIZATION rls_regress_user1; +WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; +NOTICE: f_leak => cfcd208495d565ef66e7dff9f98764da +NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c +NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c +NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc +NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d +NOTICE: f_leak => d3d9446802a44259755d38e6d163e820 +NOTICE: f_leak => c20ad4d76fe97759aa27a0c99bff6710 +NOTICE: f_leak => aab3238922bcc25a6f606eb525ffdc56 +NOTICE: f_leak => c74d97b01eae257e44aa9d5bade97baf +NOTICE: f_leak => 6f4922f45568161a8cdf4ad2299f6d23 +NOTICE: f_leak => 98f13708210194c475687be6106a3b84 + a | b +----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 +(11 rows) + +EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; + QUERY PLAN +--------------------------------------- + CTE Scan on cte1 + CTE cte1 + -> Subquery Scan on t1 + Filter: f_leak(t1.b) + -> Seq Scan on t1 t1_1 + Filter: ((a % 2) = 0) +(6 rows) + +WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail +ERROR: new row violates WITH CHECK OPTION for "t1" +DETAIL: Failing row contains (1, cfcd208495d565ef66e7dff9f98764da). +WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok + a | b +----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 +(11 rows) + +WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail +ERROR: new row violates WITH CHECK OPTION for "t1" +DETAIL: Failing row contains (21, Fail). +WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok + a | b +----+--------- + 20 | Success +(1 row) + +-- +-- Rename Policy +-- +RESET SESSION AUTHORIZATION; +ALTER POLICY p1 ON t1 RENAME TO p1; --fail +ERROR: row-policy "p1" for table "t1" already exists +SELECT rsecpolname, relname + FROM pg_rowsecurity rs + JOIN pg_class pc ON (pc.oid = rs.rsecrelid) + WHERE relname = 't1'; + rsecpolname | relname +-------------+--------- + p1 | t1 +(1 row) + +ALTER POLICY p1 ON t1 RENAME TO p2; --ok +SELECT rsecpolname, relname + FROM pg_rowsecurity rs + JOIN pg_class pc ON (pc.oid = rs.rsecrelid) + WHERE relname = 't1'; + rsecpolname | relname +-------------+--------- + p2 | t1 +(1 row) + +-- +-- Check INSERT SELECT +-- +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE TABLE t2 (a integer, b text); +INSERT INTO t2 (SELECT * FROM t1); +EXPLAIN (COSTS OFF) INSERT INTO t2 (SELECT * FROM t1); + QUERY PLAN +------------------------------- + Insert on t2 + -> Seq Scan on t1 + Filter: ((a % 2) = 0) +(3 rows) + +SELECT * FROM t2; + a | b +----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + 20 | Success +(12 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t2; + QUERY PLAN +---------------- + Seq Scan on t2 +(1 row) + +CREATE TABLE t3 AS SELECT * FROM t1; +SELECT * FROM t3; + a | b +----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + 20 | Success +(12 rows) + +SELECT * INTO t4 FROM t1; +SELECT * FROM t4; + a | b +----+---------------------------------- + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + 20 | Success +(12 rows) + +-- +-- RLS with JOIN +-- +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE blog (id integer, author text, post text); +CREATE TABLE comment (blog_id integer, message text); +GRANT ALL ON blog, comment TO rls_regress_user1; +CREATE POLICY blog_1 ON blog USING (id % 2 = 0); +ALTER TABLE blog ENABLE ROW LEVEL SECURITY; +INSERT INTO blog VALUES + (1, 'alice', 'blog #1'), + (2, 'bob', 'blog #1'), + (3, 'alice', 'blog #2'), + (4, 'alice', 'blog #3'), + (5, 'john', 'blog #1'); +INSERT INTO comment VALUES + (1, 'cool blog'), + (1, 'fun blog'), + (3, 'crazy blog'), + (5, 'what?'), + (4, 'insane!'), + (2, 'who did it?'); +SET SESSION AUTHORIZATION rls_regress_user1; +-- Check RLS JOIN with Non-RLS. +SELECT id, author, message FROM blog JOIN comment ON id = blog_id; + id | author | message +----+--------+------------- + 4 | alice | insane! + 2 | bob | who did it? +(2 rows) + +-- Check Non-RLS JOIN with RLS. +SELECT id, author, message FROM comment JOIN blog ON id = blog_id; + id | author | message +----+--------+------------- + 4 | alice | insane! + 2 | bob | who did it? +(2 rows) + +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE POLICY comment_1 ON comment USING (blog_id < 4); +ALTER TABLE comment ENABLE ROW LEVEL SECURITY; +SET SESSION AUTHORIZATION rls_regress_user1; +-- Check RLS JOIN RLS +SELECT id, author, message FROM blog JOIN comment ON id = blog_id; + id | author | message +----+--------+------------- + 2 | bob | who did it? +(1 row) + +SELECT id, author, message FROM comment JOIN blog ON id = blog_id; + id | author | message +----+--------+------------- + 2 | bob | who did it? +(1 row) + +SET SESSION AUTHORIZATION rls_regress_user0; +DROP TABLE blog, comment; +-- +-- Default Deny Policy +-- +RESET SESSION AUTHORIZATION; +DROP POLICY p2 ON t1; +ALTER TABLE t1 OWNER TO rls_regress_user0; +-- Check that default deny does not apply to superuser. +RESET SESSION AUTHORIZATION; +SELECT * FROM t1; + a | b +----+---------------------------------- + 1 | c4ca4238a0b923820dcc509a6f75849b + 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 5 | e4da3b7fbbce2345d7772b0674a318d5 + 7 | 8f14e45fceea167a5a36dedd4bea2543 + 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 11 | 6512bd43d9caa6e02c990b0a82652dca + 13 | c51ce410c124a10e0db5e4b97fc2af39 + 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 + 17 | 70efdf2ec9b086079795c442636b55fb + 19 | 1f0e3dad99908345f7439f8ffabdffc4 + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + 20 | Success +(22 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +---------------- + Seq Scan on t1 +(1 row) + +-- Check that default deny does not apply to table owner. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM t1; + a | b +----+---------------------------------- + 1 | c4ca4238a0b923820dcc509a6f75849b + 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3 + 5 | e4da3b7fbbce2345d7772b0674a318d5 + 7 | 8f14e45fceea167a5a36dedd4bea2543 + 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26 + 11 | 6512bd43d9caa6e02c990b0a82652dca + 13 | c51ce410c124a10e0db5e4b97fc2af39 + 15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3 + 17 | 70efdf2ec9b086079795c442636b55fb + 19 | 1f0e3dad99908345f7439f8ffabdffc4 + 0 | cfcd208495d565ef66e7dff9f98764da + 2 | c81e728d9d4c2f636f067f89cc14862c + 4 | a87ff679a2f3e71d9181a67b7542122c + 6 | 1679091c5a880faf6fb5e6087eb1b2dc + 8 | c9f0f895fb98ab9159f51fd0297e236d + 10 | d3d9446802a44259755d38e6d163e820 + 12 | c20ad4d76fe97759aa27a0c99bff6710 + 14 | aab3238922bcc25a6f606eb525ffdc56 + 16 | c74d97b01eae257e44aa9d5bade97baf + 18 | 6f4922f45568161a8cdf4ad2299f6d23 + 20 | 98f13708210194c475687be6106a3b84 + 20 | Success +(22 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +---------------- + Seq Scan on t1 +(1 row) + +-- Check that default deny does apply to superuser when RLS force. +SET row_security TO FORCE; +RESET SESSION AUTHORIZATION; +SELECT * FROM t1; + a | b +---+--- +(0 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +-- Check that default deny does apply to table owner when RLS force. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM t1; + a | b +---+--- +(0 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +-- Check that default deny applies to non-owner/non-superuser when RLS on. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO ON; +SELECT * FROM t1; + a | b +---+--- +(0 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM t1; + a | b +---+--- +(0 rows) + +EXPLAIN (COSTS OFF) SELECT * FROM t1; + QUERY PLAN +-------------------------- + Result + One-Time Filter: false +(2 rows) + +-- +-- Event Triggers +-- +RESET SESSION AUTHORIZATION; +CREATE TABLE event_trigger_test (a integer, b text); +CREATE OR REPLACE FUNCTION start_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - ddl_command_start', tg_tag; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION end_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - ddl_command_end', tg_tag; +END; +$$ LANGUAGE plpgsql; +CREATE OR REPLACE FUNCTION drop_sql_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - sql_drop', tg_tag; +END; +$$ LANGUAGE plpgsql; +CREATE EVENT TRIGGER start_rls_command ON ddl_command_start + WHEN TAG IN ('CREATE POLICY', 'ALTER POLICY', 'DROP POLICY') EXECUTE PROCEDURE start_command(); +CREATE EVENT TRIGGER end_rls_command ON ddl_command_end + WHEN TAG IN ('CREATE POLICY', 'ALTER POLICY', 'DROP POLICY') EXECUTE PROCEDURE end_command(); +CREATE EVENT TRIGGER sql_drop_command ON sql_drop + WHEN TAG IN ('DROP POLICY') EXECUTE PROCEDURE drop_sql_command(); +CREATE POLICY p1 ON event_trigger_test USING (FALSE); +NOTICE: CREATE POLICY - ddl_command_start +NOTICE: CREATE POLICY - ddl_command_end +ALTER POLICY p1 ON event_trigger_test USING (TRUE); +NOTICE: ALTER POLICY - ddl_command_start +NOTICE: ALTER POLICY - ddl_command_end +ALTER POLICY p1 ON event_trigger_test RENAME TO p2; +NOTICE: ALTER POLICY - ddl_command_start +NOTICE: ALTER POLICY - ddl_command_end +DROP POLICY p2 ON event_trigger_test; +NOTICE: DROP POLICY - ddl_command_start +NOTICE: DROP POLICY - sql_drop +NOTICE: DROP POLICY - ddl_command_end +DROP EVENT TRIGGER start_rls_command; +DROP EVENT TRIGGER end_rls_command; +DROP EVENT TRIGGER sql_drop_command; +-- +-- COPY TO/FROM +-- +RESET SESSION AUTHORIZATION; +DROP TABLE copy_t CASCADE; +ERROR: table "copy_t" does not exist +CREATE TABLE copy_t (a integer, b text); +CREATE POLICY p1 ON copy_t USING (a % 2 = 0); +ALTER TABLE copy_t ENABLE ROW LEVEL SECURITY; +GRANT ALL ON copy_t TO rls_regress_user1, rls_regress_exempt_user; +INSERT INTO copy_t (SELECT x, md5(x::text) FROM generate_series(0,10) x); +-- Check COPY TO as Superuser/owner. +RESET SESSION AUTHORIZATION; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; +0,cfcd208495d565ef66e7dff9f98764da +1,c4ca4238a0b923820dcc509a6f75849b +2,c81e728d9d4c2f636f067f89cc14862c +3,eccbc87e4b5ce2fe28308fd9f2a7baf3 +4,a87ff679a2f3e71d9181a67b7542122c +5,e4da3b7fbbce2345d7772b0674a318d5 +6,1679091c5a880faf6fb5e6087eb1b2dc +7,8f14e45fceea167a5a36dedd4bea2543 +8,c9f0f895fb98ab9159f51fd0297e236d +9,45c48cce2e2d7fbdea1afc51c7c6ad26 +10,d3d9446802a44259755d38e6d163e820 +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; +0,cfcd208495d565ef66e7dff9f98764da +1,c4ca4238a0b923820dcc509a6f75849b +2,c81e728d9d4c2f636f067f89cc14862c +3,eccbc87e4b5ce2fe28308fd9f2a7baf3 +4,a87ff679a2f3e71d9181a67b7542122c +5,e4da3b7fbbce2345d7772b0674a318d5 +6,1679091c5a880faf6fb5e6087eb1b2dc +7,8f14e45fceea167a5a36dedd4bea2543 +8,c9f0f895fb98ab9159f51fd0297e236d +9,45c48cce2e2d7fbdea1afc51c7c6ad26 +10,d3d9446802a44259755d38e6d163e820 +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; +0,cfcd208495d565ef66e7dff9f98764da +2,c81e728d9d4c2f636f067f89cc14862c +4,a87ff679a2f3e71d9181a67b7542122c +6,1679091c5a880faf6fb5e6087eb1b2dc +8,c9f0f895fb98ab9159f51fd0297e236d +10,d3d9446802a44259755d38e6d163e820 +-- Check COPY TO as user with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls +ERROR: insufficient privilege to bypass row security. +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +0,cfcd208495d565ef66e7dff9f98764da +2,c81e728d9d4c2f636f067f89cc14862c +4,a87ff679a2f3e71d9181a67b7542122c +6,1679091c5a880faf6fb5e6087eb1b2dc +8,c9f0f895fb98ab9159f51fd0297e236d +10,d3d9446802a44259755d38e6d163e820 +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +0,cfcd208495d565ef66e7dff9f98764da +2,c81e728d9d4c2f636f067f89cc14862c +4,a87ff679a2f3e71d9181a67b7542122c +6,1679091c5a880faf6fb5e6087eb1b2dc +8,c9f0f895fb98ab9159f51fd0297e236d +10,d3d9446802a44259755d38e6d163e820 +-- Check COPY TO as user with permissions and BYPASSRLS +SET SESSION AUTHORIZATION rls_regress_exempt_user; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +0,cfcd208495d565ef66e7dff9f98764da +1,c4ca4238a0b923820dcc509a6f75849b +2,c81e728d9d4c2f636f067f89cc14862c +3,eccbc87e4b5ce2fe28308fd9f2a7baf3 +4,a87ff679a2f3e71d9181a67b7542122c +5,e4da3b7fbbce2345d7772b0674a318d5 +6,1679091c5a880faf6fb5e6087eb1b2dc +7,8f14e45fceea167a5a36dedd4bea2543 +8,c9f0f895fb98ab9159f51fd0297e236d +9,45c48cce2e2d7fbdea1afc51c7c6ad26 +10,d3d9446802a44259755d38e6d163e820 +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +0,cfcd208495d565ef66e7dff9f98764da +2,c81e728d9d4c2f636f067f89cc14862c +4,a87ff679a2f3e71d9181a67b7542122c +6,1679091c5a880faf6fb5e6087eb1b2dc +8,c9f0f895fb98ab9159f51fd0297e236d +10,d3d9446802a44259755d38e6d163e820 +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +0,cfcd208495d565ef66e7dff9f98764da +2,c81e728d9d4c2f636f067f89cc14862c +4,a87ff679a2f3e71d9181a67b7542122c +6,1679091c5a880faf6fb5e6087eb1b2dc +8,c9f0f895fb98ab9159f51fd0297e236d +10,d3d9446802a44259755d38e6d163e820 +-- Check COPY TO as user without permissions.SET row_security TO OFF; +SET SESSION AUTHORIZATION rls_regress_user2; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls +ERROR: insufficient privilege to bypass row security. +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied +ERROR: permission denied for relation copy_t +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied +ERROR: permission denied for relation copy_t +-- Check COPY FROM as Superuser/owner. +RESET SESSION AUTHORIZATION; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --ok +SET row_security TO ON; +COPY copy_t FROM STDIN; --ok +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +ERROR: COPY FROM not supported with row security. +HINT: Use direct INSERT statements instead. +-- Check COPY FROM as user with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --fail - insufficient privilege to bypass rls. +ERROR: insufficient privilege to bypass row security. +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +ERROR: COPY FROM not supported with row security. +HINT: Use direct INSERT statements instead. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +ERROR: COPY FROM not supported with row security. +HINT: Use direct INSERT statements instead. +-- Check COPY TO as user with permissions and BYPASSRLS +SET SESSION AUTHORIZATION rls_regress_exempt_user; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --ok +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +ERROR: COPY FROM not supported with row security. +HINT: Use direct INSERT statements instead. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +ERROR: COPY FROM not supported with row security. +HINT: Use direct INSERT statements instead. +-- Check COPY FROM as user without permissions. +SET SESSION AUTHORIZATION rls_regress_user2; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --fail - permission denied. +ERROR: permission denied for relation copy_t +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - permission denied. +ERROR: permission denied for relation copy_t +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - permission denied. +ERROR: permission denied for relation copy_t +RESET SESSION AUTHORIZATION; +DROP TABLE copy_t; +-- +-- Clean up objects +-- +RESET SESSION AUTHORIZATION; +DROP SCHEMA rls_regress_schema CASCADE; +NOTICE: drop cascades to 24 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 table t1 +drop cascades to table t2 +drop cascades to table t3 +drop cascades to table t4 +drop cascades to table event_trigger_test +drop cascades to function start_command() +drop cascades to function end_command() +drop cascades to function drop_sql_command() +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 index ca56b47618..889bcd201f 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1353,6 +1353,32 @@ pg_matviews| SELECT n.nspname AS schemaname, 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, @@ -1389,6 +1415,7 @@ pg_roles| SELECT pg_authid.rolname, pg_authid.rolconnlimit, '********'::text AS rolpassword, pg_authid.rolvaliduntil, + pg_authid.rolbypassrls, s.setconfig AS rolconfig, pg_authid.oid FROM (pg_authid @@ -2018,7 +2045,8 @@ pg_tables| SELECT n.nspname AS schemaname, t.spcname AS tablespace, c.relhasindex AS hasindexes, c.relhasrules AS hasrules, - c.relhastriggers AS hastriggers + 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 index 111d24ca6b..2c8ec118a7 100644 --- a/src/test/regress/expected/sanity_check.out +++ b/src/test/regress/expected/sanity_check.out @@ -121,6 +121,7 @@ pg_pltemplate|t 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/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out index 6576c47451..6a35925169 100644 --- a/src/test/regress/expected/updatable_views.out +++ b/src/test/regress/expected/updatable_views.out @@ -1395,18 +1395,18 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view1'; INSERT INTO rw_view1 VALUES(3,4); -- ok INSERT INTO rw_view1 VALUES(4,3); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (4, 3). INSERT INTO rw_view1 VALUES(5,null); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (5, null). UPDATE rw_view1 SET b = 5 WHERE a = 3; -- ok UPDATE rw_view1 SET b = -5 WHERE a = 3; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (3, -5). INSERT INTO rw_view1(a) VALUES (9); -- ok INSERT INTO rw_view1(a) VALUES (10); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (10, 10). SELECT * FROM base_tbl; a | b @@ -1445,11 +1445,11 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; (1 row) INSERT INTO rw_view2 VALUES (-5); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (-5). INSERT INTO rw_view2 VALUES (5); -- ok INSERT INTO rw_view2 VALUES (15); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (15). SELECT * FROM base_tbl; a @@ -1458,10 +1458,10 @@ SELECT * FROM base_tbl; (1 row) UPDATE rw_view2 SET a = a - 10; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (-5). UPDATE rw_view2 SET a = a + 10; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (15). CREATE OR REPLACE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a < 10 WITH LOCAL CHECK OPTION; @@ -1486,7 +1486,7 @@ SELECT * FROM information_schema.views WHERE table_name = 'rw_view2'; INSERT INTO rw_view2 VALUES (-10); -- ok, but not in view INSERT INTO rw_view2 VALUES (20); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (20). SELECT * FROM base_tbl; a @@ -1500,10 +1500,10 @@ ERROR: invalid value for "check_option" option DETAIL: Valid values are "local" and "cascaded". ALTER VIEW rw_view1 SET (check_option=local); INSERT INTO rw_view2 VALUES (-20); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (-20). INSERT INTO rw_view2 VALUES (30); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (30). ALTER VIEW rw_view2 RESET (check_option); \d+ rw_view2 @@ -1559,7 +1559,7 @@ INSERT INTO rw_view1 VALUES (1); -- ok INSERT INTO rw_view2 VALUES (-2); -- ok, but not in view INSERT INTO rw_view2 VALUES (2); -- ok INSERT INTO rw_view3 VALUES (-3); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (-3). INSERT INTO rw_view3 VALUES (3); -- ok DROP TABLE base_tbl CASCADE; @@ -1577,11 +1577,11 @@ CREATE VIEW rw_view1 AS WITH CHECK OPTION; INSERT INTO rw_view1 VALUES (5); -- ok INSERT INTO rw_view1 VALUES (15); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (15). UPDATE rw_view1 SET a = a + 5; -- ok UPDATE rw_view1 SET a = a + 5; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (15). EXPLAIN (costs off) INSERT INTO rw_view1 VALUES (5); QUERY PLAN @@ -1629,10 +1629,10 @@ CREATE TRIGGER base_tbl_trig BEFORE INSERT OR UPDATE ON base_tbl CREATE VIEW rw_view1 AS SELECT * FROM base_tbl WHERE a < b WITH CHECK OPTION; INSERT INTO rw_view1 VALUES (5,0); -- ok INSERT INTO rw_view1 VALUES (15, 20); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (15, 10). UPDATE rw_view1 SET a = 20, b = 30; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view1" +ERROR: new row violates WITH CHECK OPTION for "rw_view1" DETAIL: Failing row contains (20, 10). DROP TABLE base_tbl CASCADE; NOTICE: drop cascades to view rw_view1 @@ -1663,12 +1663,12 @@ CREATE TRIGGER rw_view1_trig CREATE VIEW rw_view2 AS SELECT * FROM rw_view1 WHERE a > 0 WITH LOCAL CHECK OPTION; INSERT INTO rw_view2 VALUES (-5); -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (-5). INSERT INTO rw_view2 VALUES (5); -- ok INSERT INTO rw_view2 VALUES (50); -- ok, but not in view UPDATE rw_view2 SET a = a - 10; -- should fail -ERROR: new row violates WITH CHECK OPTION for view "rw_view2" +ERROR: new row violates WITH CHECK OPTION for "rw_view2" DETAIL: Failing row contains (-5). SELECT * FROM base_tbl; a | b diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index c0416f4fb9..ab6c4e2cee 100644 --- a/src/test/regress/parallel_schedule +++ b/src/test/regress/parallel_schedule @@ -83,7 +83,7 @@ test: select_into select_distinct select_distinct_on select_implicit select_havi # ---------- # Another group of parallel tests # ---------- -test: privileges security_label collate matview lock replica_identity +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 index 16a190507d..5ed2bf0ffb 100644 --- a/src/test/regress/serial_schedule +++ b/src/test/regress/serial_schedule @@ -101,6 +101,7 @@ test: collate 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 0000000000..5409bb055a --- /dev/null +++ b/src/test/regress/sql/rowsecurity.sql @@ -0,0 +1,921 @@ +-- +-- 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'); + +ALTER TABLE document ENABLE ROW LEVEL SECURITY; + +-- 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 + 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); + +ALTER TABLE category ENABLE ROW LEVEL SECURITY; + +-- 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 + +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE t2 ENABLE ROW LEVEL SECURITY; + +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)); +ALTER TABLE rec1 ENABLE ROW LEVEL SECURITY; +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)); +ALTER TABLE rec2 ENABLE ROW LEVEL SECURITY; + +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM rec1; -- fail, mutual recursion + +-- +-- Mutual recursion via views +-- +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW rec1v AS SELECT * FROM rec1; +CREATE VIEW rec2v AS SELECT * FROM rec2; +SET SESSION AUTHORIZATION rls_regress_user0; +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_user1; +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; +SET SESSION AUTHORIZATION rls_regress_user0; +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); + +GRANT SELECT ON s1, s2 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%')); +CREATE POLICY p3 ON s1 FOR INSERT WITH CHECK (a = (SELECT a FROM s1)); + +ALTER TABLE s1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE s2 ENABLE ROW LEVEL SECURITY; + +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW v2 AS SELECT * FROM s2 WHERE y like '%af%'; +SELECT * FROM s1 WHERE f_leak(b); -- fail (infinite recursion) + +INSERT INTO s1 VALUES (1, 'foo'); -- fail (infinite recursion) + +SET SESSION AUTHORIZATION rls_regress_user0; +DROP POLICY p3 on s1; +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); + +GRANT SELECT ON z1 TO rls_regress_group1, rls_regress_group2, + rls_regress_user1, rls_regress_user2; + +INSERT INTO z1 VALUES + (1, 'aaa'), + (2, 'bbb'), + (3, 'ccc'), + (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); + +ALTER TABLE z1 ENABLE ROW LEVEL SECURITY; + +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM z1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + +SET ROLE rls_regress_group1; +SELECT * FROM z1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM z1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + +SET ROLE rls_regress_group2; +SELECT * FROM z1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM z1 WHERE f_leak(b); + +-- +-- Views should follow policy for view owner. +-- +-- View and Table owner are the same. +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +GRANT SELECT ON rls_view TO rls_regress_user1; + +-- Query as role that is not owner of view or table. Should return all records. +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM rls_view; +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + +-- Query as view/table owner. Should return all records. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM rls_view; +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; +DROP VIEW rls_view; + +-- View and Table owners are different. +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW rls_view AS SELECT * FROM z1 WHERE f_leak(b); +GRANT SELECT ON rls_view TO rls_regress_user0; + +-- Query as role that is not owner of view but is owner of table. +-- Should return records based on view owner policies. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM rls_view; +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + +-- Query as role that is not owner of table but is owner of view. +-- Should return records based on view owner policies. +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM rls_view; +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + +-- Query as role that is not the owner of the table or view without permissions. +SET SESSION AUTHORIZATION rls_regress_user2; +SELECT * FROM rls_view; --fail - permission denied. +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; --fail - permission denied. + +-- Query as role that is not the owner of the table or view with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +GRANT SELECT ON rls_view TO rls_regress_user2; +SELECT * FROM rls_view; +EXPLAIN (COSTS OFF) SELECT * FROM rls_view; + +SET SESSION AUTHORIZATION rls_regress_user1; +DROP VIEW rls_view; + +-- +-- 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); + +ALTER TABLE x1 ENABLE ROW LEVEL SECURITY; + +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 + +ALTER TABLE y1 ENABLE ROW LEVEL SECURITY; +ALTER TABLE y2 ENABLE ROW LEVEL SECURITY; + +-- +-- Expression structure with SBV +-- +-- Create view as table owner. RLS should NOT be applied. +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); +DROP VIEW rls_sbv; + +-- Create view as role that does not own table. RLS should be applied. +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE VIEW rls_sbv WITH (security_barrier) AS + SELECT * FROM y1 WHERE f_leak(b); +EXPLAIN (COSTS OFF) SELECT * FROM rls_sbv WHERE (a = 1); +DROP VIEW rls_sbv; + +-- +-- 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); + +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; + +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; + +-- +-- CTE and RLS +-- +RESET SESSION AUTHORIZATION; +DROP TABLE t1 CASCADE; +CREATE TABLE t1 (a integer, b text); +CREATE POLICY p1 ON t1 USING (a % 2 = 0); + +ALTER TABLE t1 ENABLE ROW LEVEL SECURITY; + +GRANT ALL ON t1 TO rls_regress_user1; + +INSERT INTO t1 (SELECT x, md5(x::text) FROM generate_series(0,20) x); + +SET SESSION AUTHORIZATION rls_regress_user1; + +WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; +EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FROM cte1; + +WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail +WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok + +WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail +WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok + +-- +-- Rename Policy +-- +RESET SESSION AUTHORIZATION; +ALTER POLICY p1 ON t1 RENAME TO p1; --fail + +SELECT rsecpolname, relname + FROM pg_rowsecurity rs + JOIN pg_class pc ON (pc.oid = rs.rsecrelid) + WHERE relname = 't1'; + +ALTER POLICY p1 ON t1 RENAME TO p2; --ok + +SELECT rsecpolname, relname + FROM pg_rowsecurity rs + JOIN pg_class pc ON (pc.oid = rs.rsecrelid) + WHERE relname = 't1'; + +-- +-- Check INSERT SELECT +-- +SET SESSION AUTHORIZATION rls_regress_user1; +CREATE TABLE t2 (a integer, b text); +INSERT INTO t2 (SELECT * FROM t1); +EXPLAIN (COSTS OFF) INSERT INTO t2 (SELECT * FROM t1); +SELECT * FROM t2; +EXPLAIN (COSTS OFF) SELECT * FROM t2; +CREATE TABLE t3 AS SELECT * FROM t1; +SELECT * FROM t3; +SELECT * INTO t4 FROM t1; +SELECT * FROM t4; + +-- +-- RLS with JOIN +-- +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE TABLE blog (id integer, author text, post text); +CREATE TABLE comment (blog_id integer, message text); + +GRANT ALL ON blog, comment TO rls_regress_user1; + +CREATE POLICY blog_1 ON blog USING (id % 2 = 0); + +ALTER TABLE blog ENABLE ROW LEVEL SECURITY; + +INSERT INTO blog VALUES + (1, 'alice', 'blog #1'), + (2, 'bob', 'blog #1'), + (3, 'alice', 'blog #2'), + (4, 'alice', 'blog #3'), + (5, 'john', 'blog #1'); + +INSERT INTO comment VALUES + (1, 'cool blog'), + (1, 'fun blog'), + (3, 'crazy blog'), + (5, 'what?'), + (4, 'insane!'), + (2, 'who did it?'); + +SET SESSION AUTHORIZATION rls_regress_user1; +-- Check RLS JOIN with Non-RLS. +SELECT id, author, message FROM blog JOIN comment ON id = blog_id; +-- Check Non-RLS JOIN with RLS. +SELECT id, author, message FROM comment JOIN blog ON id = blog_id; + +SET SESSION AUTHORIZATION rls_regress_user0; +CREATE POLICY comment_1 ON comment USING (blog_id < 4); + +ALTER TABLE comment ENABLE ROW LEVEL SECURITY; + +SET SESSION AUTHORIZATION rls_regress_user1; +-- Check RLS JOIN RLS +SELECT id, author, message FROM blog JOIN comment ON id = blog_id; +SELECT id, author, message FROM comment JOIN blog ON id = blog_id; + +SET SESSION AUTHORIZATION rls_regress_user0; +DROP TABLE blog, comment; + +-- +-- Default Deny Policy +-- +RESET SESSION AUTHORIZATION; +DROP POLICY p2 ON t1; +ALTER TABLE t1 OWNER TO rls_regress_user0; + +-- Check that default deny does not apply to superuser. +RESET SESSION AUTHORIZATION; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; + +-- Check that default deny does not apply to table owner. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; + +-- Check that default deny does apply to superuser when RLS force. +SET row_security TO FORCE; +RESET SESSION AUTHORIZATION; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; + +-- Check that default deny does apply to table owner when RLS force. +SET SESSION AUTHORIZATION rls_regress_user0; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; + +-- Check that default deny applies to non-owner/non-superuser when RLS on. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO ON; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; +SET SESSION AUTHORIZATION rls_regress_user1; +SELECT * FROM t1; +EXPLAIN (COSTS OFF) SELECT * FROM t1; + +-- +-- Event Triggers +-- +RESET SESSION AUTHORIZATION; +CREATE TABLE event_trigger_test (a integer, b text); + +CREATE OR REPLACE FUNCTION start_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - ddl_command_start', tg_tag; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION end_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - ddl_command_end', tg_tag; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION drop_sql_command() +RETURNS event_trigger AS $$ +BEGIN +RAISE NOTICE '% - sql_drop', tg_tag; +END; +$$ LANGUAGE plpgsql; + +CREATE EVENT TRIGGER start_rls_command ON ddl_command_start + WHEN TAG IN ('CREATE POLICY', 'ALTER POLICY', 'DROP POLICY') EXECUTE PROCEDURE start_command(); + +CREATE EVENT TRIGGER end_rls_command ON ddl_command_end + WHEN TAG IN ('CREATE POLICY', 'ALTER POLICY', 'DROP POLICY') EXECUTE PROCEDURE end_command(); + +CREATE EVENT TRIGGER sql_drop_command ON sql_drop + WHEN TAG IN ('DROP POLICY') EXECUTE PROCEDURE drop_sql_command(); + +CREATE POLICY p1 ON event_trigger_test USING (FALSE); +ALTER POLICY p1 ON event_trigger_test USING (TRUE); +ALTER POLICY p1 ON event_trigger_test RENAME TO p2; +DROP POLICY p2 ON event_trigger_test; + +DROP EVENT TRIGGER start_rls_command; +DROP EVENT TRIGGER end_rls_command; +DROP EVENT TRIGGER sql_drop_command; + +-- +-- COPY TO/FROM +-- + +RESET SESSION AUTHORIZATION; +DROP TABLE copy_t CASCADE; +CREATE TABLE copy_t (a integer, b text); +CREATE POLICY p1 ON copy_t USING (a % 2 = 0); + +ALTER TABLE copy_t ENABLE ROW LEVEL SECURITY; + +GRANT ALL ON copy_t TO rls_regress_user1, rls_regress_exempt_user; + +INSERT INTO copy_t (SELECT x, md5(x::text) FROM generate_series(0,10) x); + +-- Check COPY TO as Superuser/owner. +RESET SESSION AUTHORIZATION; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; + +-- Check COPY TO as user with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok + +-- Check COPY TO as user with permissions and BYPASSRLS +SET SESSION AUTHORIZATION rls_regress_exempt_user; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --ok + +-- Check COPY TO as user without permissions.SET row_security TO OFF; +SET SESSION AUTHORIZATION rls_regress_user2; +SET row_security TO OFF; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - insufficient to bypass rls +SET row_security TO ON; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied +SET row_security TO FORCE; +COPY (SELECT * FROM copy_t ORDER BY a ASC) TO STDOUT WITH DELIMITER ','; --fail - permission denied + +-- Check COPY FROM as Superuser/owner. +RESET SESSION AUTHORIZATION; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --ok +1 abc +2 bcd +3 cde +4 def +\. +SET row_security TO ON; +COPY copy_t FROM STDIN; --ok +1 abc +2 bcd +3 cde +4 def +\. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. + +-- Check COPY FROM as user with permissions. +SET SESSION AUTHORIZATION rls_regress_user1; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --fail - insufficient privilege to bypass rls. +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. + +-- Check COPY TO as user with permissions and BYPASSRLS +SET SESSION AUTHORIZATION rls_regress_exempt_user; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --ok +1 abc +2 bcd +3 cde +4 def +\. +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - COPY FROM not supported by RLS. + +-- Check COPY FROM as user without permissions. +SET SESSION AUTHORIZATION rls_regress_user2; +SET row_security TO OFF; +COPY copy_t FROM STDIN; --fail - permission denied. +SET row_security TO ON; +COPY copy_t FROM STDIN; --fail - permission denied. +SET row_security TO FORCE; +COPY copy_t FROM STDIN; --fail - permission denied. + +RESET SESSION AUTHORIZATION; +DROP TABLE copy_t; + +-- +-- 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;