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_seclabelsecurity labels on database objects
@@ -1935,6 +1940,15 @@
+
+ relhasrowsecurity
+ bool
+
+ True if table has row-security enabled; see
+ pg_rowsecurity catalog
+
+
+
relhassubclassbool
@@ -5328,6 +5342,86 @@
+
+ pg_rowsecurity
+
+
+ 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.
+
+
+
+
+ pg_rowsecurity 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.
+
+
+
+ pg_seclabel
@@ -9133,6 +9227,12 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
pg_class.relhastriggersTrue 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 SCHEMAX
@@ -351,6 +357,12 @@
X-
+
+ CREATE POLICY
+ X
+ X
+ -
+ CREATE RULEX
@@ -525,6 +537,12 @@
XX
+
+ DROP POLICY
+ X
+ X
+ X
+ DROP RULEX
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-reservednon-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
NOLOGINREPLICATIONNOREPLICATION
+ BYPASSRLS
+ NOBYPASSRLSCONNECTION LIMITconnlimitPASSWORD> passwordENCRYPTED>
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 LIMITconnlimit
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