diff options
Diffstat (limited to 'src/backend')
33 files changed, 2355 insertions, 55 deletions
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" @@ -344,6 +346,18 @@ static const ObjectPropertyType ObjectProperty[] = false }, { + RowSecurityRelationId, + RowSecurityOidIndexId, + -1, + -1, + Anum_pg_rowsecurity_rsecpolname, + InvalidAttrNumber, + InvalidAttrNumber, + InvalidAttrNumber, + -1, + false + }, + { EventTriggerRelationId, EventTriggerOidIndexId, EVENTTRIGGEROID, @@ -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" @@ -420,6 +421,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; @@ -10615,6 +10629,62 @@ ATExecReplicaIdentity(Relation rel, ReplicaIdentityStmt *stmt, LOCKMODE lockmode } /* + * 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 <name> OPTIONS (...) */ static void 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); @@ -2008,6 +2009,31 @@ _equalAlterTSConfigurationStmt(const AlterTSConfigurationStmt *a, } 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) { COMPARE_SCALAR_FIELD(kind); @@ -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 <str> all_Op MathOp +%type <str> row_security_cmd RowSecurityDefaultForCmd +%type <node> RowSecurityOptionalWithCheck RowSecurityOptionalExpr +%type <list> RowSecurityDefaultToRole RowSecurityOptionalToRole + %type <str> iso_level opt_encoding %type <node> grantee %type <list> 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 <name> ENABLE ROW LEVEL SECURITY */ + | ENABLE_P ROW LEVEL SECURITY + { + AlterTableCmd *n = makeNode(AlterTableCmd); + n->subtype = AT_EnableRowSecurity; + $$ = (Node *)n; + } + /* ALTER TABLE <name> 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); @@ -4497,6 +4522,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 : * CREATE TRIGGER ... * DROP TRIGGER ... @@ -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 = NIL; + for (i = 0; i < rules->numLocks; i++) + { + rule = rules->rules[i]; + if (rule->event != CMD_SELECT) + continue; - locks = lappend(locks, rule); - } + 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 we found any, apply them --- but first check for recursion! + * If the RTE has row-security quals, apply them and recurse into the + * securityQuals. */ - if (locks != NIL) + if (prepend_row_security_policies(parsetree, rte, rt_index)) { - ListCell *l; - + /* + * 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 rules for relation \"%s\"", - RelationGetRelationName(rel)))); - activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs); + 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); - foreach(l, locks) + expression_tree_walker( (Node*) rte->securityQuals, + fireRIRonSubLink, (void*)activeRIRs ); + + activeRIRs = list_delete_first(activeRIRs); + } + + if (parsetree->withCheckOptions != NIL) { - rule = lfirst(l); - - parsetree = ApplyRetrieveRule(parsetree, - rule, - rt_index, - rel, - activeRIRs, - forUpdatePushedDown); + 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 @@ -566,6 +578,17 @@ RevalidateCachedQuery(CachedPlanSource *plansource) } /* + * 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 * to force replan. @@ -583,6 +606,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 * condition that an invalidation message arrives before we get the locks. @@ -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" @@ -401,6 +402,23 @@ static const struct config_enum_entry huge_pages_options[] = { }; /* + * Although only "on", "off", and "force" are documented, we + * accept all the likely variants of "on" and "off". + */ +static const struct config_enum_entry row_security_options[] = { + {"on", ROW_SECURITY_ON, false}, + {"off", ROW_SECURITY_OFF, false}, + {"force", ROW_SECURITY_FORCE, false}, + {"true", ROW_SECURITY_ON, true}, + {"false", ROW_SECURITY_OFF, true}, + {"yes", ROW_SECURITY_ON, true}, + {"no", ROW_SECURITY_OFF, true}, + {"1", ROW_SECURITY_ON, true}, + {"0", ROW_SECURITY_OFF, true}, + {NULL, 0, false} +}; + +/* * Options for enum values stored in other modules */ extern const struct config_enum_entry wal_level_options[]; @@ -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 = '' |
