citus/src/backend/distributed/commands/role.c

823 lines
22 KiB
C

/*-------------------------------------------------------------------------
*
* role.c
* Commands for ALTER ROLE statements.
*
* Copyright (c) Citus Data, Inc.
*
*-------------------------------------------------------------------------
*/
#include "postgres.h"
#include "distributed/pg_version_constants.h"
#include "access/heapam.h"
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
#include "catalog/pg_auth_members.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_db_role_setting.h"
#include "catalog/pg_type.h"
#include "catalog/objectaddress.h"
#include "commands/dbcommands.h"
#include "distributed/citus_ruleutils.h"
#include "distributed/citus_safe_lib.h"
#include "distributed/commands.h"
#include "distributed/commands/utility_hook.h"
#include "distributed/deparser.h"
#include "distributed/listutils.h"
#include "distributed/coordinator_protocol.h"
#include "distributed/metadata/distobject.h"
#include "distributed/metadata_sync.h"
#include "distributed/version_compat.h"
#include "distributed/worker_transaction.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/parsenodes.h"
#include "nodes/pg_list.h"
#include "parser/scansup.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/guc_tables.h"
#include "utils/guc.h"
#include "utils/rel.h"
#include "utils/varlena.h"
#include "utils/syscache.h"
static const char * ExtractEncryptedPassword(Oid roleOid);
static const char * CreateAlterRoleIfExistsCommand(AlterRoleStmt *stmt);
static const char * CreateAlterRoleSetIfExistsCommand(AlterRoleSetStmt *stmt);
static char * CreateCreateOrAlterRoleCommand(const char *roleName,
CreateRoleStmt *createRoleStmt,
AlterRoleStmt *alterRoleStmt);
static DefElem * makeDefElemInt(char *name, int value);
static List * GenerateRoleOptionsList(HeapTuple tuple);
static char * GetRoleNameFromDbRoleSetting(HeapTuple tuple,
TupleDesc DbRoleSettingDescription);
static char * GetDatabaseNameFromDbRoleSetting(HeapTuple tuple,
TupleDesc DbRoleSettingDescription);
static Node * makeStringConst(char *str, int location);
static Node * makeIntConst(int val, int location);
static Node * makeFloatConst(char *str, int location);
static const char * WrapQueryInAlterRoleIfExistsCall(const char *query, RoleSpec *role);
static VariableSetStmt * MakeVariableSetStmt(const char *config);
static int ConfigGenericNameCompare(const void *lhs, const void *rhs);
static ObjectAddress RoleSpecToObjectAddress(RoleSpec *role, bool missing_ok);
/* controlled via GUC */
bool EnableAlterRolePropagation = true;
bool EnableAlterRoleSetPropagation = true;
/*
* AlterRoleStmtObjectAddress returns the ObjectAddress of the role in the
* AlterRoleStmt. If missing_ok is set to false an error will be raised if postgres
* was unable to find the role that was the target of the statement.
*/
ObjectAddress
AlterRoleStmtObjectAddress(Node *node, bool missing_ok)
{
AlterRoleStmt *stmt = castNode(AlterRoleStmt, node);
return RoleSpecToObjectAddress(stmt->role, missing_ok);
}
/*
* AlterRoleSetStmtObjectAddress returns the ObjectAddress of the role in the
* AlterRoleSetStmt. If missing_ok is set to false an error will be raised if postgres
* was unable to find the role that was the target of the statement.
*/
ObjectAddress
AlterRoleSetStmtObjectAddress(Node *node, bool missing_ok)
{
AlterRoleSetStmt *stmt = castNode(AlterRoleSetStmt, node);
return RoleSpecToObjectAddress(stmt->role, missing_ok);
}
/*
* RoleSpecToObjectAddress returns the ObjectAddress of a Role associated with a
* RoleSpec. If missing_ok is set to false an error will be raised by postgres
* explaining the Role could not be found.
*/
static ObjectAddress
RoleSpecToObjectAddress(RoleSpec *role, bool missing_ok)
{
ObjectAddress address = { 0 };
if (role != NULL)
{
/* roles can be NULL for statements on ALL roles eg. ALTER ROLE ALL SET ... */
Oid roleOid = get_rolespec_oid(role, missing_ok);
ObjectAddressSet(address, AuthIdRelationId, roleOid);
}
return address;
}
/*
* PostprocessAlterRoleStmt actually creates the plan we need to execute for alter
* role statement. We need to do it this way because we need to use the encrypted
* password, which is, in some cases, created at standardProcessUtility.
*/
List *
PostprocessAlterRoleStmt(Node *node, const char *queryString)
{
ObjectAddress address = GetObjectAddressFromParseTree(node, false);
if (!ShouldPropagateObject(&address))
{
return NIL;
}
if (!EnableAlterRolePropagation || !IsCoordinator())
{
return NIL;
}
AlterRoleStmt *stmt = castNode(AlterRoleStmt, node);
DefElem *option = NULL;
foreach_ptr(option, stmt->options)
{
if (strcasecmp(option->defname, "password") == 0)
{
Oid roleOid = get_rolespec_oid(stmt->role, true);
const char *encryptedPassword = ExtractEncryptedPassword(roleOid);
if (encryptedPassword != NULL)
{
String *encryptedPasswordValue = makeString((char *) encryptedPassword);
option->arg = (Node *) encryptedPasswordValue;
}
else
{
option->arg = NULL;
}
break;
}
}
List *commands = list_make1((void *) CreateAlterRoleIfExistsCommand(stmt));
return NodeDDLTaskList(NON_COORDINATOR_NODES, commands);
}
/*
* PreprocessAlterRoleSetStmt actually creates the plan we need to execute for alter
* role set statement.
*/
List *
PreprocessAlterRoleSetStmt(Node *node, const char *queryString,
ProcessUtilityContext processUtilityContext)
{
if (!ShouldPropagate())
{
return NIL;
}
if (!EnableAlterRoleSetPropagation)
{
return NIL;
}
AlterRoleSetStmt *stmt = castNode(AlterRoleSetStmt, node);
/* don't propagate if the statement is scoped to another database */
if (stmt->database != NULL &&
strcmp(stmt->database, get_database_name(MyDatabaseId)) != 0)
{
return NIL;
}
ObjectAddress address = GetObjectAddressFromParseTree(node, false);
/*
* stmt->role could be NULL when the statement is on 'ALL' roles, we do propagate for
* ALL roles. If it is not NULL the role is for a specific role. If that role is not
* distributed we will not propagate the statement
*/
if (stmt->role != NULL && !IsObjectDistributed(&address))
{
return NIL;
}
/*
* Since roles need to be handled manually on community, we need to support such queries
* by handling them locally on worker nodes
*/
if (!IsCoordinator())
{
return NIL;
}
QualifyTreeNode((Node *) stmt);
const char *sql = DeparseTreeNode((Node *) stmt);
List *commandList = list_make3(DISABLE_DDL_PROPAGATION,
(void *) sql,
ENABLE_DDL_PROPAGATION);
return NodeDDLTaskList(NON_COORDINATOR_NODES, commandList);
}
/*
* CreateAlterRoleIfExistsCommand creates ALTER ROLE command, from the alter role node
* using the alter_role_if_exists() UDF.
*/
static const char *
CreateAlterRoleIfExistsCommand(AlterRoleStmt *stmt)
{
const char *alterRoleQuery = DeparseTreeNode((Node *) stmt);
return WrapQueryInAlterRoleIfExistsCall(alterRoleQuery, stmt->role);
}
/*
* CreateAlterRoleSetIfExistsCommand creates ALTER ROLE .. SET command, from the
* AlterRoleSetStmt node.
*
* If the statement affects a single user, the query is wrapped in a
* alter_role_if_exists() to make sure that it is run on workers that has a user
* with the same name. If the query is a ALTER ROLE ALL .. SET query, the query
* is sent to the workers as is.
*/
static const char *
CreateAlterRoleSetIfExistsCommand(AlterRoleSetStmt *stmt)
{
char *alterRoleSetQuery = DeparseTreeNode((Node *) stmt);
/* ALTER ROLE ALL .. SET queries should not be wrapped in a alter_role_if_exists() call */
if (stmt->role == NULL)
{
return alterRoleSetQuery;
}
else
{
return WrapQueryInAlterRoleIfExistsCall(alterRoleSetQuery, stmt->role);
}
}
/*
* WrapQueryInAlterRoleIfExistsCall wraps a given query in a alter_role_if_exists()
* UDF.
*/
static const char *
WrapQueryInAlterRoleIfExistsCall(const char *query, RoleSpec *role)
{
StringInfoData buffer = { 0 };
const char *roleName = RoleSpecString(role, false);
initStringInfo(&buffer);
appendStringInfo(&buffer,
"SELECT alter_role_if_exists(%s, %s)",
quote_literal_cstr(roleName),
quote_literal_cstr(query));
return buffer.data;
}
/*
* CreateCreateOrAlterRoleCommand creates ALTER ROLE command, from the alter role node
* using the alter_role_if_exists() UDF.
*/
static char *
CreateCreateOrAlterRoleCommand(const char *roleName,
CreateRoleStmt *createRoleStmt,
AlterRoleStmt *alterRoleStmt)
{
StringInfoData createOrAlterRoleQueryBuffer = { 0 };
const char *createRoleQuery = "null";
const char *alterRoleQuery = "null";
if (createRoleStmt != NULL)
{
createRoleQuery = quote_literal_cstr(DeparseTreeNode((Node *) createRoleStmt));
}
if (alterRoleStmt != NULL)
{
alterRoleQuery = quote_literal_cstr(DeparseTreeNode((Node *) alterRoleStmt));
}
initStringInfo(&createOrAlterRoleQueryBuffer);
appendStringInfo(&createOrAlterRoleQueryBuffer,
"SELECT worker_create_or_alter_role(%s, %s, %s)",
quote_literal_cstr(roleName),
createRoleQuery,
alterRoleQuery);
return createOrAlterRoleQueryBuffer.data;
}
/*
* ExtractEncryptedPassword extracts the encrypted password of a role. The function
* gets the password from the pg_authid table.
*/
static const char *
ExtractEncryptedPassword(Oid roleOid)
{
Relation pgAuthId = table_open(AuthIdRelationId, AccessShareLock);
TupleDesc pgAuthIdDescription = RelationGetDescr(pgAuthId);
HeapTuple tuple = SearchSysCache1(AUTHOID, roleOid);
bool isNull = true;
if (!HeapTupleIsValid(tuple))
{
return NULL;
}
Datum passwordDatum = heap_getattr(tuple, Anum_pg_authid_rolpassword,
pgAuthIdDescription, &isNull);
table_close(pgAuthId, AccessShareLock);
ReleaseSysCache(tuple);
if (isNull)
{
return NULL;
}
return pstrdup(TextDatumGetCString(passwordDatum));
}
/*
* GenerateAlterRoleSetIfExistsCommandList generate a list of ALTER ROLE .. SET commands that
* copies a role session defaults from the pg_db_role_settings table.
*/
static List *
GenerateAlterRoleSetIfExistsCommandList(HeapTuple tuple,
TupleDesc DbRoleSettingDescription)
{
AlterRoleSetStmt *stmt = makeNode(AlterRoleSetStmt);
List *commandList = NIL;
bool isnull = false;
char *databaseName =
GetDatabaseNameFromDbRoleSetting(tuple, DbRoleSettingDescription);
if (databaseName != NULL)
{
stmt->database = databaseName;
}
char *roleName = GetRoleNameFromDbRoleSetting(tuple, DbRoleSettingDescription);
if (roleName != NULL)
{
stmt->role = makeNode(RoleSpec);
stmt->role->location = -1;
stmt->role->roletype = ROLESPEC_CSTRING;
stmt->role->rolename = roleName;
}
Datum setconfig = heap_getattr(tuple, Anum_pg_db_role_setting_setconfig,
DbRoleSettingDescription, &isnull);
Datum *configs;
int nconfigs;
int i;
deconstruct_array(DatumGetArrayTypeP(setconfig),
TEXTOID, -1, false, 'i',
&configs, NULL, &nconfigs);
/*
* A tuple might contain one or more settings that apply to the user-database combination.
* ALTER ROLE ... SET ... only allows to set one at a time. We will create a statement for every
* configuration contained in the tuple.
*/
for (i = 0; i < nconfigs; i++)
{
char *config = TextDatumGetCString(configs[i]);
stmt->setstmt = MakeVariableSetStmt(config);
commandList = lappend(commandList,
(void *) CreateAlterRoleSetIfExistsCommand(stmt));
}
return commandList;
}
/*
* MakeVariableSetStmt takes a "some-option=some value" string and creates a
* VariableSetStmt Node.
*/
static VariableSetStmt *
MakeVariableSetStmt(const char *config)
{
char *name = NULL;
char *value = NULL;
ParseLongOption(config, &name, &value);
VariableSetStmt *variableSetStmt = makeNode(VariableSetStmt);
variableSetStmt->kind = VAR_SET_VALUE;
variableSetStmt->name = name;
variableSetStmt->args = MakeSetStatementArguments(name, value);
return variableSetStmt;
}
/*
* GenerateRoleOptionsList returns the list of options set on a user based on the record
* in pg_authid. It requires the HeapTuple for a user entry to access both its fixed
* length and variable length fields.
*/
static List *
GenerateRoleOptionsList(HeapTuple tuple)
{
Form_pg_authid role = ((Form_pg_authid) GETSTRUCT(tuple));
List *options = NIL;
options = lappend(options, makeDefElemInt("superuser", role->rolsuper));
options = lappend(options, makeDefElemInt("createdb", role->rolcreatedb));
options = lappend(options, makeDefElemInt("createrole", role->rolcreaterole));
options = lappend(options, makeDefElemInt("inherit", role->rolinherit));
options = lappend(options, makeDefElemInt("canlogin", role->rolcanlogin));
options = lappend(options, makeDefElemInt("isreplication", role->rolreplication));
options = lappend(options, makeDefElemInt("bypassrls", role->rolbypassrls));
options = lappend(options, makeDefElemInt("connectionlimit", role->rolconnlimit));
/* load password from heap tuple, use NULL if not set */
bool isNull = true;
Datum rolPasswordDatum = SysCacheGetAttr(AUTHNAME, tuple, Anum_pg_authid_rolpassword,
&isNull);
if (!isNull)
{
char *rolPassword = pstrdup(TextDatumGetCString(rolPasswordDatum));
Node *passwordStringNode = (Node *) makeString(rolPassword);
DefElem *passwordOption = makeDefElem("password", passwordStringNode, -1);
options = lappend(options, passwordOption);
}
else
{
options = lappend(options, makeDefElem("password", NULL, -1));
}
/* load valid unitl data from the heap tuple, use default of infinity if not set */
Datum rolValidUntilDatum = SysCacheGetAttr(AUTHNAME, tuple,
Anum_pg_authid_rolvaliduntil, &isNull);
char *rolValidUntil = "infinity";
if (!isNull)
{
rolValidUntil = pstrdup((char *) timestamptz_to_str(rolValidUntilDatum));
}
Node *validUntilStringNode = (Node *) makeString(rolValidUntil);
DefElem *validUntilOption = makeDefElem("validUntil", validUntilStringNode, -1);
options = lappend(options, validUntilOption);
return options;
}
/*
* GenerateCreateOrAlterRoleCommand generates ALTER ROLE command that copies a role from
* the pg_authid table.
*/
List *
GenerateCreateOrAlterRoleCommand(Oid roleOid)
{
HeapTuple roleTuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleOid));
Form_pg_authid role = ((Form_pg_authid) GETSTRUCT(roleTuple));
CreateRoleStmt *createRoleStmt = NULL;
AlterRoleStmt *alterRoleStmt = NULL;
if (EnableAlterRolePropagation)
{
alterRoleStmt = makeNode(AlterRoleStmt);
alterRoleStmt->role = makeNode(RoleSpec);
alterRoleStmt->role->roletype = ROLESPEC_CSTRING;
alterRoleStmt->role->location = -1;
alterRoleStmt->role->rolename = pstrdup(NameStr(role->rolname));
alterRoleStmt->action = 1;
alterRoleStmt->options = GenerateRoleOptionsList(roleTuple);
}
ReleaseSysCache(roleTuple);
List *completeRoleList = NIL;
if (createRoleStmt != NULL || alterRoleStmt != NULL)
{
/* add a worker_create_or_alter_role command if any of them are set */
char *createOrAlterRoleQuery = CreateCreateOrAlterRoleCommand(
pstrdup(NameStr(role->rolname)),
createRoleStmt,
alterRoleStmt);
completeRoleList = lappend(completeRoleList, createOrAlterRoleQuery);
}
if (EnableAlterRoleSetPropagation)
{
/* append ALTER ROLE ... SET commands fot this specific user */
List *alterRoleSetCommands = GenerateAlterRoleSetCommandForRole(roleOid);
completeRoleList = list_concat(completeRoleList, alterRoleSetCommands);
}
return completeRoleList;
}
/*
* GenerateAlterRoleSetCommandForRole returns the list of database wide settings for a
* specifc role. If the roleid is InvalidOid it returns the commands that apply to all
* users for the database or postgres wide.
*/
List *
GenerateAlterRoleSetCommandForRole(Oid roleid)
{
Relation DbRoleSetting = table_open(DbRoleSettingRelationId, AccessShareLock);
TupleDesc DbRoleSettingDescription = RelationGetDescr(DbRoleSetting);
HeapTuple tuple = NULL;
List *commands = NIL;
TableScanDesc scan = table_beginscan_catalog(DbRoleSetting, 0, NULL);
while ((tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
{
Form_pg_db_role_setting roleSetting = (Form_pg_db_role_setting) GETSTRUCT(tuple);
if (roleSetting->setrole != roleid)
{
/* not the user we are looking for */
continue;
}
if (OidIsValid(roleSetting->setdatabase) &&
roleSetting->setdatabase != MyDatabaseId)
{
/* setting is database specific for a different database */
continue;
}
List *alterRoleSetQueries = GenerateAlterRoleSetIfExistsCommandList(tuple,
DbRoleSettingDescription);
commands = list_concat(commands, alterRoleSetQueries);
}
heap_endscan(scan);
table_close(DbRoleSetting, AccessShareLock);
return commands;
}
/*
* makeDefElemInt creates a DefElem with integer typed value with -1 as location.
*/
static DefElem *
makeDefElemInt(char *name, int value)
{
return makeDefElem(name, (Node *) makeInteger(value), -1);
}
/*
* GetDatabaseNameFromDbRoleSetting performs a lookup, and finds the database name
* associated DbRoleSetting Tuple
*/
static char *
GetDatabaseNameFromDbRoleSetting(HeapTuple tuple, TupleDesc DbRoleSettingDescription)
{
bool isnull;
Datum setdatabase = heap_getattr(tuple, Anum_pg_db_role_setting_setdatabase,
DbRoleSettingDescription, &isnull);
if (isnull)
{
return NULL;
}
Oid databaseId = DatumGetObjectId(setdatabase);
char *databaseName = get_database_name(databaseId);
return databaseName;
}
/*
* GetRoleNameFromDbRoleSetting performs a lookup, and finds the role name
* associated DbRoleSetting Tuple
*/
static char *
GetRoleNameFromDbRoleSetting(HeapTuple tuple, TupleDesc DbRoleSettingDescription)
{
bool isnull;
Datum setrole = heap_getattr(tuple, Anum_pg_db_role_setting_setrole,
DbRoleSettingDescription, &isnull);
if (isnull)
{
return NULL;
}
Oid roleId = DatumGetObjectId(setrole);
char *roleName = GetUserNameFromId(roleId, true);
return roleName;
}
/*
* MakeSetStatementArgs parses a configuration value and creates an List of A_Const
* Nodes with appropriate types.
*
* The allowed A_Const types are Integer, Float, and String.
*/
List *
MakeSetStatementArguments(char *configurationName, char *configurationValue)
{
List *args = NIL;
char **key = &configurationName;
/* Perform a lookup on GUC variables to find the config type and units.
* All user-defined GUCs have string values, but we need to perform a search
* on all the GUCs to understand if it is a user-defined one or not.
*
* Note: get_guc_variables() is intended for internal use only, but there
* is no other way to determine allowed units, and value types other than
* using this function
*/
struct config_generic **gucVariables = get_guc_variables();
int numOpts = GetNumConfigOptions();
struct config_generic **matchingConfig =
(struct config_generic **) SafeBsearch((void *) &key,
(void *) gucVariables,
numOpts,
sizeof(struct config_generic *),
ConfigGenericNameCompare);
/* If the config is not user-defined, lookup the variable type to contruct the arguments */
if (matchingConfig != NULL)
{
switch ((*matchingConfig)->vartype)
{
/* We use postgresql parser so that we will parse the units only if
* the configuration paramater allows it.
*
* e.g. `SET statement_timeout = '1min'` will be parsed as 60000 since
* the value is stored in units of ms internally.
*/
case PGC_INT:
{
int intValue;
parse_int(configurationValue, &intValue,
(*matchingConfig)->flags, NULL);
Node *arg = makeIntConst(intValue, -1);
args = lappend(args, arg);
break;
}
case PGC_REAL:
{
Node *arg = makeFloatConst(configurationValue, -1);
args = lappend(args, arg);
break;
}
case PGC_BOOL:
case PGC_STRING:
case PGC_ENUM:
{
List *configurationList = NIL;
if ((*matchingConfig)->flags & GUC_LIST_INPUT)
{
char *configurationValueCopy = pstrdup(configurationValue);
SplitIdentifierString(configurationValueCopy, ',',
&configurationList);
}
else
{
configurationList = list_make1(configurationValue);
}
char *configuration = NULL;
foreach_ptr(configuration, configurationList)
{
Node *arg = makeStringConst(configuration, -1);
args = lappend(args, arg);
}
break;
}
default:
{
ereport(ERROR, (errmsg("Unrecognized run-time parameter type for %s",
configurationName)));
break;
}
}
}
else
{
Node *arg = makeStringConst(configurationValue, -1);
args = lappend(args, arg);
}
return args;
}
/*
* makeStringConst creates a Const Node that stores a given string
*
* copied from backend/parser/gram.c
*/
static Node *
makeStringConst(char *str, int location)
{
A_Const *n = makeNode(A_Const);
#if PG_VERSION_NUM >= PG_VERSION_15
n->val.sval.type = T_String;
n->val.sval.sval = str;
#else
n->val.type = T_String;
n->val.val.str = str;
#endif
n->location = location;
return (Node *) n;
}
/*
* makeIntConst creates a Const Node that stores a given integer
*
* copied from backend/parser/gram.c
*/
static Node *
makeIntConst(int val, int location)
{
A_Const *n = makeNode(A_Const);
#if PG_VERSION_NUM >= PG_VERSION_15
n->val.ival.type = T_Integer;
n->val.ival.ival = val;
#else
n->val.type = T_Integer;
n->val.val.ival = val;
#endif
n->location = location;
return (Node *) n;
}
/*
* makeIntConst creates a Const Node that stores a given Float
*
* copied from backend/parser/gram.c
*/
static Node *
makeFloatConst(char *str, int location)
{
A_Const *n = makeNode(A_Const);
#if PG_VERSION_NUM >= PG_VERSION_15
n->val.fval.type = T_Float;
n->val.fval.fval = str;
#else
n->val.type = T_Float;
n->val.val.str = str;
#endif
n->location = location;
return (Node *) n;
}
/*
* ConfigGenericNameCompare compares two config_generic structs based on their
* name fields. If the name fields contain the same strings two structs are
* considered to be equal.
*
* copied from guc_var_compare in utils/misc/guc.c
*/
static int
ConfigGenericNameCompare(const void *a, const void *b)
{
const struct config_generic *confa = *(struct config_generic *const *) a;
const struct config_generic *confb = *(struct config_generic *const *) b;
/*
* guc_var_compare used a custom comparison function here to allow stable
* ordering, but we do not need it here as we only perform a lookup, and do
* not use this function to order the guc list.
*/
return pg_strcasecmp(confa->name, confb->name);
}