mirror of https://github.com/citusdata/citus.git
541 lines
14 KiB
C
541 lines
14 KiB
C
/*
|
|
* citus_depended_object.c
|
|
*
|
|
* Implements exposed functions related to hiding citus depended objects.
|
|
*
|
|
* Copyright (c) Citus Data, Inc.
|
|
*/
|
|
|
|
#include "postgres.h"
|
|
|
|
#include "miscadmin.h"
|
|
|
|
#include "catalog/namespace.h"
|
|
#include "catalog/pg_aggregate.h"
|
|
#include "catalog/pg_am.h"
|
|
#include "catalog/pg_amop.h"
|
|
#include "catalog/pg_amproc.h"
|
|
#include "catalog/pg_attrdef.h"
|
|
#include "catalog/pg_attribute.h"
|
|
#include "catalog/pg_class.h"
|
|
#include "catalog/pg_constraint.h"
|
|
#include "catalog/pg_depend.h"
|
|
#include "catalog/pg_enum.h"
|
|
#include "catalog/pg_event_trigger.h"
|
|
#include "catalog/pg_language.h"
|
|
#include "catalog/pg_namespace.h"
|
|
#include "catalog/pg_opclass.h"
|
|
#include "catalog/pg_operator.h"
|
|
#include "catalog/pg_opfamily.h"
|
|
#include "catalog/pg_proc.h"
|
|
#include "catalog/pg_rewrite.h"
|
|
#include "catalog/pg_sequence.h"
|
|
#include "catalog/pg_statistic.h"
|
|
#include "catalog/pg_trigger.h"
|
|
#include "catalog/pg_ts_config.h"
|
|
#include "catalog/pg_ts_dict.h"
|
|
#include "catalog/pg_ts_template.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "nodes/makefuncs.h"
|
|
#include "nodes/nodeFuncs.h"
|
|
#include "nodes/parsenodes.h"
|
|
#include "parser/parse_type.h"
|
|
#include "storage/large_object.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/syscache.h"
|
|
|
|
#include "distributed/citus_depended_object.h"
|
|
#include "distributed/commands.h"
|
|
#include "distributed/listutils.h"
|
|
#include "distributed/log_utils.h"
|
|
#include "distributed/metadata_cache.h"
|
|
#include "distributed/shared_library_init.h"
|
|
|
|
/*
|
|
* GUC hides any objects, which depends on citus extension, from pg meta class queries,
|
|
* it is intended to be used in vanilla tests to not break postgres test logs
|
|
*/
|
|
bool HideCitusDependentObjects = false;
|
|
|
|
static Node * CreateCitusDependentObjectExpr(int pgMetaTableVarno, int pgMetaTableOid);
|
|
static List * GetCitusDependedObjectArgs(int pgMetaTableVarno, int pgMetaTableOid);
|
|
static bool AlterRoleSetStatementContainsAll(Node *node);
|
|
static bool HasDropCommandViolatesOwnership(Node *node);
|
|
static bool AnyObjectViolatesOwnership(DropStmt *dropStmt);
|
|
|
|
/*
|
|
* IsPgLocksTable returns true if RTE is pg_locks table.
|
|
*/
|
|
bool
|
|
IsPgLocksTable(RangeTblEntry *rte)
|
|
{
|
|
Oid pgLocksId = get_relname_relid("pg_locks", get_namespace_oid("pg_catalog", false));
|
|
return rte->relid == pgLocksId;
|
|
}
|
|
|
|
|
|
/*
|
|
* SetLocalHideCitusDependentObjectsDisabledWhenAlreadyEnabled disables the GUC HideCitusDependentObjects
|
|
* if only it is enabled for local transaction.
|
|
*/
|
|
void
|
|
SetLocalHideCitusDependentObjectsDisabledWhenAlreadyEnabled(void)
|
|
{
|
|
if (!HideCitusDependentObjects)
|
|
{
|
|
return;
|
|
}
|
|
|
|
set_config_option("citus.hide_citus_dependent_objects", "false",
|
|
(superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION,
|
|
GUC_ACTION_LOCAL, true, 0, false);
|
|
}
|
|
|
|
|
|
/*
|
|
* SetLocalClientMinMessagesIfRunningPGTests sets client_min_message locally to the given value
|
|
* if EnableUnsupportedFeatureMessages is set to false.
|
|
*/
|
|
void
|
|
SetLocalClientMinMessagesIfRunningPGTests(int clientMinMessageLevel)
|
|
{
|
|
if (EnableUnsupportedFeatureMessages)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const char *clientMinMessageLevelName = GetClientMinMessageLevelNameForValue(
|
|
clientMinMessageLevel);
|
|
|
|
set_config_option("client_min_messages", clientMinMessageLevelName,
|
|
(superuser() ? PGC_SUSET : PGC_USERSET), PGC_S_SESSION,
|
|
GUC_ACTION_LOCAL, true, 0, false);
|
|
}
|
|
|
|
|
|
/*
|
|
* HideCitusDependentObjectsOnQueriesOfPgMetaTables adds a NOT is_citus_depended_object(oid, oid) expr
|
|
* to the quals of meta class RTEs that we are interested in.
|
|
*/
|
|
bool
|
|
HideCitusDependentObjectsOnQueriesOfPgMetaTables(Node *node, void *context)
|
|
{
|
|
if (!CitusHasBeenLoaded() || !HideCitusDependentObjects || node == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (IsA(node, Query))
|
|
{
|
|
Query *query = (Query *) node;
|
|
MemoryContext queryContext = GetMemoryChunkContext(query);
|
|
|
|
/*
|
|
* We process the whole rtable rather than visiting individual RangeTblEntry's
|
|
* in the walker, since we need to know the varno to generate the right
|
|
* filter.
|
|
*/
|
|
int varno = 0;
|
|
RangeTblEntry *rangeTableEntry = NULL;
|
|
|
|
foreach_ptr(rangeTableEntry, query->rtable)
|
|
{
|
|
varno++;
|
|
|
|
if (rangeTableEntry->rtekind == RTE_RELATION)
|
|
{
|
|
/* make sure the expression is in the right memory context */
|
|
MemoryContext originalContext = MemoryContextSwitchTo(queryContext);
|
|
|
|
Oid metaTableOid = InvalidOid;
|
|
|
|
/*
|
|
* add NOT is_citus_depended_object(oid, oid) to the quals
|
|
* of the RTE if it is a pg meta table that we are interested in.
|
|
*/
|
|
switch (rangeTableEntry->relid)
|
|
{
|
|
/* pg_class */
|
|
case RelationRelationId:
|
|
|
|
/* pg_proc */
|
|
case ProcedureRelationId:
|
|
|
|
/* pg_am */
|
|
case AccessMethodRelationId:
|
|
|
|
/* pg_type */
|
|
case TypeRelationId:
|
|
|
|
/* pg_enum */
|
|
case EnumRelationId:
|
|
|
|
/* pg_event_trigger */
|
|
case EventTriggerRelationId:
|
|
|
|
/* pg_trigger */
|
|
case TriggerRelationId:
|
|
|
|
/* pg_rewrite */
|
|
case RewriteRelationId:
|
|
|
|
/* pg_attrdef */
|
|
case AttrDefaultRelationId:
|
|
|
|
/* pg_constraint */
|
|
case ConstraintRelationId:
|
|
|
|
/* pg_ts_config */
|
|
case TSConfigRelationId:
|
|
|
|
/* pg_ts_template */
|
|
case TSTemplateRelationId:
|
|
|
|
/* pg_ts_dict */
|
|
case TSDictionaryRelationId:
|
|
|
|
/* pg_language */
|
|
case LanguageRelationId:
|
|
|
|
/* pg_namespace */
|
|
case NamespaceRelationId:
|
|
|
|
/* pg_sequence */
|
|
case SequenceRelationId:
|
|
|
|
/* pg_statistic */
|
|
case StatisticRelationId:
|
|
|
|
/* pg_attribute */
|
|
case AttributeRelationId:
|
|
|
|
/* pg_index */
|
|
case IndexRelationId:
|
|
|
|
/* pg_operator */
|
|
case OperatorRelationId:
|
|
|
|
/* pg_opclass */
|
|
case OperatorClassRelationId:
|
|
|
|
/* pg_opfamily */
|
|
case OperatorFamilyRelationId:
|
|
|
|
/* pg_amop */
|
|
case AccessMethodOperatorRelationId:
|
|
|
|
/* pg_amproc */
|
|
case AccessMethodProcedureRelationId:
|
|
|
|
/* pg_aggregate */
|
|
case AggregateRelationId:
|
|
{
|
|
metaTableOid = rangeTableEntry->relid;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
metaTableOid = InvalidOid;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (OidIsValid(metaTableOid))
|
|
{
|
|
/*
|
|
* We found a valid pg meta class in query,
|
|
* so we assert below conditions.
|
|
*/
|
|
Assert(query->jointree != NULL);
|
|
Assert(query->jointree->fromlist != NULL);
|
|
|
|
Node *citusDependentObjExpr =
|
|
CreateCitusDependentObjectExpr(varno, metaTableOid);
|
|
|
|
/*
|
|
* We do not use security quals because a postgres vanilla test fails
|
|
* with a change of order for its result.
|
|
*/
|
|
query->jointree->quals = make_and_qual(
|
|
query->jointree->quals, citusDependentObjExpr);
|
|
}
|
|
|
|
MemoryContextSwitchTo(originalContext);
|
|
}
|
|
}
|
|
|
|
return query_tree_walker((Query *) node,
|
|
HideCitusDependentObjectsOnQueriesOfPgMetaTables,
|
|
context, 0);
|
|
}
|
|
|
|
return expression_tree_walker(node, HideCitusDependentObjectsOnQueriesOfPgMetaTables,
|
|
context);
|
|
}
|
|
|
|
|
|
/*
|
|
* CreateCitusDependentObjectExpr constructs an expression of the form:
|
|
* NOT pg_catalog.is_citus_depended_object(oid, oid)
|
|
*/
|
|
static Node *
|
|
CreateCitusDependentObjectExpr(int pgMetaTableVarno, int pgMetaTableOid)
|
|
{
|
|
/* build the call to read_intermediate_result */
|
|
FuncExpr *funcExpr = makeNode(FuncExpr);
|
|
funcExpr->funcid = CitusDependentObjectFuncId();
|
|
funcExpr->funcretset = false;
|
|
funcExpr->funcvariadic = false;
|
|
funcExpr->funcformat = 0;
|
|
funcExpr->funccollid = 0;
|
|
funcExpr->inputcollid = 0;
|
|
funcExpr->location = -1;
|
|
funcExpr->args = GetCitusDependedObjectArgs(pgMetaTableVarno, pgMetaTableOid);
|
|
|
|
BoolExpr *notExpr = makeNode(BoolExpr);
|
|
notExpr->boolop = NOT_EXPR;
|
|
notExpr->args = list_make1(funcExpr);
|
|
notExpr->location = -1;
|
|
|
|
return (Node *) notExpr;
|
|
}
|
|
|
|
|
|
/*
|
|
* GetCitusDependedObjectArgs returns func arguments for pg_catalog.is_citus_depended_object
|
|
*/
|
|
static List *
|
|
GetCitusDependedObjectArgs(int pgMetaTableVarno, int pgMetaTableOid)
|
|
{
|
|
/*
|
|
* set attribute number for the oid, which we are insterest in, inside pg meta tables.
|
|
* We are accessing the 1. col(their own oid or their relation's oid) to get the related
|
|
* object's oid for all of the pg meta tables except pg_enum and pg_index. For pg_enum,
|
|
* class, we access its 2. col(its type's oid) to see if its type depends on citus,
|
|
* so it does. For pg_index, we access its 2. col (its relation's oid) to see if its relation
|
|
* depends on citus, so it does.
|
|
*/
|
|
AttrNumber oidAttNum = 1;
|
|
if (pgMetaTableOid == EnumRelationId || pgMetaTableOid == IndexRelationId)
|
|
{
|
|
oidAttNum = 2;
|
|
}
|
|
|
|
/* create const for meta table oid */
|
|
Const *metaTableOidConst = makeConst(OIDOID, -1, InvalidOid, sizeof(Oid),
|
|
ObjectIdGetDatum(pgMetaTableOid),
|
|
false, true);
|
|
|
|
/*
|
|
* create a var for the oid that we are interested in,
|
|
* col type should be regproc for pg_aggregate table; else oid
|
|
*/
|
|
Oid varType = (pgMetaTableOid == AggregateRelationId) ? REGPROCOID : OIDOID;
|
|
Var *oidVar = makeVar(pgMetaTableVarno, oidAttNum,
|
|
varType, -1, InvalidOid, 0);
|
|
|
|
return list_make2((Node *) metaTableOidConst, (Node *) oidVar);
|
|
}
|
|
|
|
|
|
/*
|
|
* DistOpsValidityState returns validation state for given dist ops.
|
|
*/
|
|
DistOpsValidationState
|
|
DistOpsValidityState(Node *node, const DistributeObjectOps *ops)
|
|
{
|
|
if (ops && ops->operationType == DIST_OPS_CREATE)
|
|
{
|
|
/*
|
|
* We should not validate CREATE statements because no address exists
|
|
* here yet.
|
|
*/
|
|
return NoAddressResolutionRequired;
|
|
}
|
|
else if (AlterRoleSetStatementContainsAll(node))
|
|
{
|
|
/*
|
|
* We should not validate 'ALTER ROLE ALL [SET|UNSET] because for the role ALL
|
|
* AlterRoleSetStmtObjectAddress returns an invalid address even though it should not.
|
|
*/
|
|
return NoAddressResolutionRequired;
|
|
}
|
|
else if (HasDropCommandViolatesOwnership(node))
|
|
{
|
|
/*
|
|
* found object with an invalid ownership, PG will complain if there is any object
|
|
* with an invalid ownership.
|
|
*/
|
|
return HasObjectWithInvalidOwnership;
|
|
}
|
|
|
|
if (ops && ops->address)
|
|
{
|
|
bool missingOk = true;
|
|
bool isPostprocess = false;
|
|
List *objectAddresses = ops->address(node, missingOk, isPostprocess);
|
|
ObjectAddress *objectAddress = NULL;
|
|
foreach_ptr(objectAddress, objectAddresses)
|
|
{
|
|
if (OidIsValid(objectAddress->objectId))
|
|
{
|
|
/* found one valid object */
|
|
return HasAtLeastOneValidObject;
|
|
}
|
|
}
|
|
|
|
/* no valid objects */
|
|
return HasNoneValidObject;
|
|
}
|
|
else
|
|
{
|
|
/* if the object doesn't have address defined, we donot validate */
|
|
return NoAddressResolutionRequired;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* DistOpsInValidState returns true if given state is valid to execute
|
|
* preprocess and qualify steps.
|
|
*/
|
|
bool
|
|
DistOpsInValidState(DistOpsValidationState distOpsValidationState)
|
|
{
|
|
return distOpsValidationState == HasAtLeastOneValidObject || distOpsValidationState ==
|
|
NoAddressResolutionRequired;
|
|
}
|
|
|
|
|
|
/*
|
|
* AlterRoleSetStatementContainsAll returns true if the statement is a
|
|
* ALTER ROLE ALL (SET / RESET).
|
|
*/
|
|
static bool
|
|
AlterRoleSetStatementContainsAll(Node *node)
|
|
{
|
|
if (node == NULL)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (nodeTag(node) == T_AlterRoleSetStmt)
|
|
{
|
|
/* rolespec is null for the role 'ALL' */
|
|
AlterRoleSetStmt *alterRoleSetStmt = castNode(AlterRoleSetStmt, node);
|
|
|
|
return alterRoleSetStmt->role == NULL;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* HasDropCommandViolatesOwnership returns true if any object in the given
|
|
* statement violates object ownership.
|
|
*
|
|
* Currently there is only one test which fails due to object ownership.
|
|
* The command that is failing is DROP. If in the future we hit other
|
|
* commands like this, we should expand this function.
|
|
*/
|
|
static bool
|
|
HasDropCommandViolatesOwnership(Node *node)
|
|
{
|
|
if (!IsA(node, DropStmt))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DropStmt *dropStmt = castNode(DropStmt, node);
|
|
if (AnyObjectViolatesOwnership(dropStmt))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
* AnyObjectViolatesOwnership return true if given object in stmt violates ownership.
|
|
*/
|
|
static bool
|
|
AnyObjectViolatesOwnership(DropStmt *dropStmt)
|
|
{
|
|
bool hasOwnershipViolation = false;
|
|
ObjectAddress objectAddress = { 0 };
|
|
volatile Relation relation = NULL;
|
|
ObjectType objectType = dropStmt->removeType;
|
|
bool missingOk = dropStmt->missing_ok;
|
|
|
|
MemoryContext savedContext = CurrentMemoryContext;
|
|
ResourceOwner savedOwner = CurrentResourceOwner;
|
|
BeginInternalSubTransaction(NULL);
|
|
MemoryContextSwitchTo(savedContext);
|
|
|
|
PG_TRY();
|
|
{
|
|
Node *object = NULL;
|
|
foreach_ptr(object, dropStmt->objects)
|
|
{
|
|
Relation rel = NULL;
|
|
objectAddress = get_object_address(objectType, object,
|
|
&rel, AccessShareLock, missingOk);
|
|
|
|
/*
|
|
* The object relation is qualified with volatile and its value is obtained from
|
|
* get_object_address(). Unless we can qualify the corresponding parameter of
|
|
* get_object_address() with volatile (this is a function defined in PostgreSQL),
|
|
* we cannot get rid of this assignment.
|
|
*/
|
|
relation = rel;
|
|
|
|
if (OidIsValid(objectAddress.objectId))
|
|
{
|
|
/*
|
|
* if object violates ownership, check_object_ownership will throw error.
|
|
*/
|
|
check_object_ownership(GetUserId(),
|
|
objectType,
|
|
objectAddress,
|
|
object, relation);
|
|
}
|
|
|
|
if (relation != NULL)
|
|
{
|
|
relation_close(relation, NoLock);
|
|
relation = NULL;
|
|
}
|
|
}
|
|
|
|
ReleaseCurrentSubTransaction();
|
|
MemoryContextSwitchTo(savedContext);
|
|
CurrentResourceOwner = savedOwner;
|
|
}
|
|
PG_CATCH();
|
|
{
|
|
MemoryContextSwitchTo(savedContext);
|
|
ErrorData *edata = CopyErrorData();
|
|
FlushErrorState();
|
|
|
|
hasOwnershipViolation = true;
|
|
if (relation != NULL)
|
|
{
|
|
relation_close(relation, NoLock);
|
|
relation = NULL;
|
|
}
|
|
RollbackAndReleaseCurrentSubTransaction();
|
|
MemoryContextSwitchTo(savedContext);
|
|
CurrentResourceOwner = savedOwner;
|
|
|
|
/* Rethrow error with LOG_SERVER_ONLY to prevent log to be sent to client */
|
|
edata->elevel = LOG_SERVER_ONLY;
|
|
ThrowErrorData(edata);
|
|
}
|
|
PG_END_TRY();
|
|
|
|
return hasOwnershipViolation;
|
|
}
|