Enforce superuser permissions checks during ALTER ROLE/DATABASE SET, rather

than during define_custom_variable().  This entails rejecting an ALTER
command if the target variable doesn't have a known (non-placeholder)
definition, unless the calling user is superuser.  When the variable *is*
known, we can correctly apply the rule that only superusers can issue ALTER
for SUSET parameters.  This allows define_custom_variable to apply ALTER's
values for SUSET parameters at module load time, secure in the knowledge
that only a superuser could have set the ALTER value.  This change fixes a
longstanding gotcha in the usage of SUSET-level custom parameters; which
is a good thing to fix now that plpgsql defines such a parameter.
This commit is contained in:
Tom Lane 2010-04-21 20:54:19 +00:00
parent f6e092701c
commit a6dcd19a2a
2 changed files with 158 additions and 64 deletions

View File

@ -1,5 +1,5 @@
<!-- <!--
$PostgreSQL: pgsql/doc/src/sgml/ref/alter_role.sgml,v 1.16 2010/04/03 07:22:57 petere Exp $ $PostgreSQL: pgsql/doc/src/sgml/ref/alter_role.sgml,v 1.17 2010/04/21 20:54:19 tgl Exp $
PostgreSQL documentation PostgreSQL documentation
--> -->
@ -24,7 +24,7 @@ PostgreSQL documentation
ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replaceable class="PARAMETER">option</replaceable> [ ... ] ] ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replaceable class="PARAMETER">option</replaceable> [ ... ] ]
<phrase>where <replaceable class="PARAMETER">option</replaceable> can be:</phrase> <phrase>where <replaceable class="PARAMETER">option</replaceable> can be:</phrase>
SUPERUSER | NOSUPERUSER SUPERUSER | NOSUPERUSER
| CREATEDB | NOCREATEDB | CREATEDB | NOCREATEDB
| CREATEROLE | NOCREATEROLE | CREATEROLE | NOCREATEROLE
@ -33,7 +33,7 @@ ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replace
| LOGIN | NOLOGIN | LOGIN | NOLOGIN
| CONNECTION LIMIT <replaceable class="PARAMETER">connlimit</replaceable> | CONNECTION LIMIT <replaceable class="PARAMETER">connlimit</replaceable>
| [ ENCRYPTED | UNENCRYPTED ] PASSWORD '<replaceable class="PARAMETER">password</replaceable>' | [ ENCRYPTED | UNENCRYPTED ] PASSWORD '<replaceable class="PARAMETER">password</replaceable>'
| VALID UNTIL '<replaceable class="PARAMETER">timestamp</replaceable>' | VALID UNTIL '<replaceable class="PARAMETER">timestamp</replaceable>'
ALTER ROLE <replaceable class="PARAMETER">name</replaceable> RENAME TO <replaceable>new_name</replaceable> ALTER ROLE <replaceable class="PARAMETER">name</replaceable> RENAME TO <replaceable>new_name</replaceable>
@ -54,7 +54,7 @@ ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ IN DATABASE <repl
<para> <para>
The first variant of this command listed in the synopsis can change The first variant of this command listed in the synopsis can change
many of the role attributes that can be specified in many of the role attributes that can be specified in
<xref linkend="sql-createrole">. <xref linkend="sql-createrole">.
(All the possible attributes are covered, (All the possible attributes are covered,
except that there are no options for adding or removing memberships; use except that there are no options for adding or removing memberships; use
@ -79,20 +79,24 @@ ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ IN DATABASE <repl
password is <literal>MD5</>-encrypted. password is <literal>MD5</>-encrypted.
</para> </para>
<para> <para>
The remaining variants change a role's session default for a configuration variable The remaining variants change a role's session default for a configuration
for all databases or, when the <literal>IN DATABASE</literal> clause is specified, variable, either for all databases or, when the <literal>IN
for the named database. Whenever the role subsequently DATABASE</literal> clause is specified, only for sessions in
the named database. Whenever the role subsequently
starts a new session, the specified value becomes the session starts a new session, the specified value becomes the session
default, overriding whatever setting is present in default, overriding whatever setting is present in
<filename>postgresql.conf</> or has been received from the postgres <filename>postgresql.conf</> or has been received from the postgres
command line. This only happens at login time, so configuration command line. This only happens at login time; executing
settings associated with a role to which you've <xref <xref linkend="sql-set-role"> or
linkend="sql-set-role"> will be ignored. Settings set to <xref linkend="sql-set-session-authorization"> does not cause new
a role directly are overridden by any database specific settings attached to a role. configuration values to be set.
Settings set for all databases are overridden by database-specific settings
attached to a role.
Superusers can change anyone's session defaults. Roles having Superusers can change anyone's session defaults. Roles having
<literal>CREATEROLE</> privilege can change defaults for non-superuser <literal>CREATEROLE</> privilege can change defaults for non-superuser
roles. Certain variables cannot be set this way, or can only be roles. Ordinary roles can only set defaults for themselves.
Certain configuration variables cannot be set this way, or can only be
set if a superuser issues the command. set if a superuser issues the command.
</para> </para>
</refsect1> </refsect1>
@ -169,14 +173,15 @@ ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ IN DATABASE <repl
<literal>RESET ALL</literal> to clear all role-specific settings. <literal>RESET ALL</literal> to clear all role-specific settings.
<literal>SET FROM CURRENT</> saves the session's current value of <literal>SET FROM CURRENT</> saves the session's current value of
the parameter as the role-specific value. the parameter as the role-specific value.
If used in conjunction with <literal>IN DATABASE</literal>, the configuration If <literal>IN DATABASE</literal> is specified, the configuration
parameter is set or removed for the given role and database only. parameter is set or removed for the given role and database only.
</para> </para>
<para> <para>
Role-specific variable setting take effect only at login; Role-specific variable settings take effect only at login;
<xref linkend="sql-set-role"> <xref linkend="sql-set-role"> and
does not process role-specific variable settings. <xref linkend="sql-set-session-authorization">
do not process role-specific variable settings.
</para> </para>
<para> <para>
@ -210,8 +215,8 @@ ALTER ROLE <replaceable class="PARAMETER">name</replaceable> [ IN DATABASE <repl
in cleartext, and it might also be logged in the client's command in cleartext, and it might also be logged in the client's command
history or the server log. <xref linkend="app-psql"> history or the server log. <xref linkend="app-psql">
contains a command contains a command
<command>\password</command> that can be used to safely change a <command>\password</command> that can be used to change a
role's password. role's password without exposing the cleartext password.
</para> </para>
<para> <para>
@ -276,8 +281,8 @@ ALTER ROLE worker_bee SET maintenance_work_mem = 100000;
</para> </para>
<para> <para>
Give a role a non-default, database-specific setting of the Give a role a non-default, database-specific setting of the
<xref linkend="guc-client-min-messages"> parameter: <xref linkend="guc-client-min-messages"> parameter:
<programlisting> <programlisting>
ALTER ROLE fred IN DATABASE devel SET client_min_messages = DEBUG; ALTER ROLE fred IN DATABASE devel SET client_min_messages = DEBUG;
@ -287,7 +292,7 @@ ALTER ROLE fred IN DATABASE devel SET client_min_messages = DEBUG;
<refsect1> <refsect1>
<title>Compatibility</title> <title>Compatibility</title>
<para> <para>
The <command>ALTER ROLE</command> statement is a The <command>ALTER ROLE</command> statement is a
<productname>PostgreSQL</productname> extension. <productname>PostgreSQL</productname> extension.

View File

@ -10,7 +10,7 @@
* Written by Peter Eisentraut <peter_e@gmx.net>. * Written by Peter Eisentraut <peter_e@gmx.net>.
* *
* IDENTIFICATION * IDENTIFICATION
* $PostgreSQL: pgsql/src/backend/utils/misc/guc.c,v 1.549 2010/04/20 11:15:06 rhaas Exp $ * $PostgreSQL: pgsql/src/backend/utils/misc/guc.c,v 1.550 2010/04/21 20:54:19 tgl Exp $
* *
*-------------------------------------------------------------------- *--------------------------------------------------------------------
*/ */
@ -2864,6 +2864,8 @@ static void ShowGUCConfigOption(const char *name, DestReceiver *dest);
static void ShowAllGUCConfig(DestReceiver *dest); static void ShowAllGUCConfig(DestReceiver *dest);
static char *_ShowOption(struct config_generic * record, bool use_units); static char *_ShowOption(struct config_generic * record, bool use_units);
static bool is_newvalue_equal(struct config_generic * record, const char *newvalue); static bool is_newvalue_equal(struct config_generic * record, const char *newvalue);
static bool validate_option_array_item(const char *name, const char *value,
bool skipIfNoPermissions);
/* /*
@ -5474,14 +5476,15 @@ flatten_set_variable_args(const char *name, List *args)
if (args == NIL) if (args == NIL)
return NULL; return NULL;
/* Else get flags for the variable */ /*
record = find_option(name, true, ERROR); * Get flags for the variable; if it's not known, use default flags.
if (record == NULL) * (Caller might throw error later, but not our business to do so here.)
ereport(ERROR, */
(errcode(ERRCODE_UNDEFINED_OBJECT), record = find_option(name, false, WARNING);
errmsg("unrecognized configuration parameter \"%s\"", name))); if (record)
flags = record->flags;
flags = record->flags; else
flags = 0;
/* Complain if list input and non-list variable */ /* Complain if list input and non-list variable */
if ((flags & GUC_LIST_INPUT) == 0 && if ((flags & GUC_LIST_INPUT) == 0 &&
@ -5870,12 +5873,27 @@ define_custom_variable(struct config_generic * variable)
else else
phcontext = PGC_SIGHUP; phcontext = PGC_SIGHUP;
break; break;
case PGC_S_DATABASE: case PGC_S_DATABASE:
case PGC_S_USER: case PGC_S_USER:
case PGC_S_DATABASE_USER: case PGC_S_DATABASE_USER:
/*
* The existing value came from an ALTER ROLE/DATABASE SET command.
* We can assume that at the time the command was issued, we
* checked that the issuing user was superuser if the variable
* requires superuser privileges to set. So it's safe to
* use SUSET context here.
*/
phcontext = PGC_SUSET;
break;
case PGC_S_CLIENT: case PGC_S_CLIENT:
case PGC_S_SESSION: case PGC_S_SESSION:
default: default:
/*
* We must assume that the value came from an untrusted user,
* even if the current_user is a superuser.
*/
phcontext = PGC_USERSET; phcontext = PGC_USERSET;
break; break;
} }
@ -7180,7 +7198,7 @@ ProcessGUCArray(ArrayType *array,
ArrayType * ArrayType *
GUCArrayAdd(ArrayType *array, const char *name, const char *value) GUCArrayAdd(ArrayType *array, const char *name, const char *value)
{ {
const char *varname; struct config_generic *record;
Datum datum; Datum datum;
char *newval; char *newval;
ArrayType *a; ArrayType *a;
@ -7188,15 +7206,15 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
Assert(name); Assert(name);
Assert(value); Assert(value);
/* test if the option is valid */ /* test if the option is valid and we're allowed to set it */
set_config_option(name, value, (void) validate_option_array_item(name, value, false);
superuser() ? PGC_SUSET : PGC_USERSET,
PGC_S_TEST, GUC_ACTION_SET, false);
/* convert name to canonical spelling, so we can use plain strcmp */ /* normalize name (converts obsolete GUC names to modern spellings) */
(void) GetConfigOptionByName(name, &varname); record = find_option(name, false, WARNING);
name = varname; if (record)
name = record->name;
/* build new item for array */
newval = palloc(strlen(name) + 1 + strlen(value) + 1); newval = palloc(strlen(name) + 1 + strlen(value) + 1);
sprintf(newval, "%s=%s", name, value); sprintf(newval, "%s=%s", name, value);
datum = CStringGetTextDatum(newval); datum = CStringGetTextDatum(newval);
@ -7227,6 +7245,8 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
if (isnull) if (isnull)
continue; continue;
current = TextDatumGetCString(d); current = TextDatumGetCString(d);
/* check for match up through and including '=' */
if (strncmp(current, newval, strlen(name) + 1) == 0) if (strncmp(current, newval, strlen(name) + 1) == 0)
{ {
index = i; index = i;
@ -7259,21 +7279,20 @@ GUCArrayAdd(ArrayType *array, const char *name, const char *value)
ArrayType * ArrayType *
GUCArrayDelete(ArrayType *array, const char *name) GUCArrayDelete(ArrayType *array, const char *name)
{ {
const char *varname; struct config_generic *record;
ArrayType *newarray; ArrayType *newarray;
int i; int i;
int index; int index;
Assert(name); Assert(name);
/* test if the option is valid */ /* test if the option is valid and we're allowed to set it */
set_config_option(name, NULL, (void) validate_option_array_item(name, NULL, false);
superuser() ? PGC_SUSET : PGC_USERSET,
PGC_S_TEST, GUC_ACTION_SET, false);
/* convert name to canonical spelling, so we can use plain strcmp */ /* normalize name (converts obsolete GUC names to modern spellings) */
(void) GetConfigOptionByName(name, &varname); record = find_option(name, false, WARNING);
name = varname; if (record)
name = record->name;
/* if array is currently null, then surely nothing to delete */ /* if array is currently null, then surely nothing to delete */
if (!array) if (!array)
@ -7303,10 +7322,8 @@ GUCArrayDelete(ArrayType *array, const char *name)
&& val[strlen(name)] == '=') && val[strlen(name)] == '=')
continue; continue;
/* else add it to the output array */ /* else add it to the output array */
if (newarray) if (newarray)
{
newarray = array_set(newarray, 1, &index, newarray = array_set(newarray, 1, &index,
d, d,
false, false,
@ -7314,7 +7331,6 @@ GUCArrayDelete(ArrayType *array, const char *name)
-1 /* TEXT's typlen */ , -1 /* TEXT's typlen */ ,
false /* TEXT's typbyval */ , false /* TEXT's typbyval */ ,
'i' /* TEXT's typalign */ ); 'i' /* TEXT's typalign */ );
}
else else
newarray = construct_array(&d, 1, newarray = construct_array(&d, 1,
TEXTOID, TEXTOID,
@ -7326,6 +7342,7 @@ GUCArrayDelete(ArrayType *array, const char *name)
return newarray; return newarray;
} }
/* /*
* Given a GUC array, delete all settings from it that our permission * Given a GUC array, delete all settings from it that our permission
* level allows: if superuser, delete them all; if regular user, only * level allows: if superuser, delete them all; if regular user, only
@ -7342,7 +7359,7 @@ GUCArrayReset(ArrayType *array)
if (!array) if (!array)
return NULL; return NULL;
/* if we're superuser, we can delete everything */ /* if we're superuser, we can delete everything, so just do it */
if (superuser()) if (superuser())
return NULL; return NULL;
@ -7355,7 +7372,6 @@ GUCArrayReset(ArrayType *array)
char *val; char *val;
char *eqsgn; char *eqsgn;
bool isnull; bool isnull;
struct config_generic *gconf;
d = array_ref(array, 1, &i, d = array_ref(array, 1, &i,
-1 /* varlenarray */ , -1 /* varlenarray */ ,
@ -7363,7 +7379,6 @@ GUCArrayReset(ArrayType *array)
false /* TEXT's typbyval */ , false /* TEXT's typbyval */ ,
'i' /* TEXT's typalign */ , 'i' /* TEXT's typalign */ ,
&isnull); &isnull);
if (isnull) if (isnull)
continue; continue;
val = TextDatumGetCString(d); val = TextDatumGetCString(d);
@ -7371,20 +7386,12 @@ GUCArrayReset(ArrayType *array)
eqsgn = strchr(val, '='); eqsgn = strchr(val, '=');
*eqsgn = '\0'; *eqsgn = '\0';
gconf = find_option(val, false, WARNING); /* skip if we have permission to delete it */
if (!gconf) if (validate_option_array_item(val, NULL, true))
continue; continue;
/* note: superuser-ness was already checked above */
/* skip entry if OK to delete */
if (gconf->context == PGC_USERSET)
continue;
/* XXX do we need to worry about database owner? */
/* else add it to the output array */ /* else add it to the output array */
if (newarray) if (newarray)
{
newarray = array_set(newarray, 1, &index, newarray = array_set(newarray, 1, &index,
d, d,
false, false,
@ -7392,7 +7399,6 @@ GUCArrayReset(ArrayType *array)
-1 /* TEXT's typlen */ , -1 /* TEXT's typlen */ ,
false /* TEXT's typbyval */ , false /* TEXT's typbyval */ ,
'i' /* TEXT's typalign */ ); 'i' /* TEXT's typalign */ );
}
else else
newarray = construct_array(&d, 1, newarray = construct_array(&d, 1,
TEXTOID, TEXTOID,
@ -7405,6 +7411,89 @@ GUCArrayReset(ArrayType *array)
return newarray; return newarray;
} }
/*
* Validate a proposed option setting for GUCArrayAdd/Delete/Reset.
*
* name is the option name. value is the proposed value for the Add case,
* or NULL for the Delete/Reset cases. If skipIfNoPermissions is true, it's
* not an error to have no permissions to set the option.
*
* Returns TRUE if OK, FALSE if skipIfNoPermissions is true and user does not
* have permission to change this option (all other error cases result in an
* error being thrown).
*/
static bool
validate_option_array_item(const char *name, const char *value,
bool skipIfNoPermissions)
{
struct config_generic *gconf;
/*
* There are three cases to consider:
*
* name is a known GUC variable. Check the value normally, check
* permissions normally (ie, allow if variable is USERSET, or if it's
* SUSET and user is superuser).
*
* name is not known, but exists or can be created as a placeholder
* (implying it has a prefix listed in custom_variable_classes).
* We allow this case if you're a superuser, otherwise not. Superusers
* are assumed to know what they're doing. We can't allow it for other
* users, because when the placeholder is resolved it might turn out to
* be a SUSET variable; define_custom_variable assumes we checked that.
*
* name is not known and can't be created as a placeholder. Throw error,
* unless skipIfNoPermissions is true, in which case return FALSE.
* (It's tempting to allow this case to superusers, if the name is
* qualified but not listed in custom_variable_classes. That would
* ease restoring of dumps containing ALTER ROLE/DATABASE SET. However,
* it's not clear that this usage justifies such a loss of error checking.
* You can always fix custom_variable_classes before you restore.)
*/
gconf = find_option(name, true, WARNING);
if (!gconf)
{
/* not known, failed to make a placeholder */
if (skipIfNoPermissions)
return false;
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("unrecognized configuration parameter \"%s\"", name)));
}
if (gconf->flags & GUC_CUSTOM_PLACEHOLDER)
{
/*
* We cannot do any meaningful check on the value, so only permissions
* are useful to check.
*/
if (superuser())
return true;
if (skipIfNoPermissions)
return false;
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to set parameter \"%s\"", name)));
}
/* manual permissions check so we can avoid an error being thrown */
if (gconf->context == PGC_USERSET)
/* ok */ ;
else if (gconf->context == PGC_SUSET && superuser())
/* ok */ ;
else if (skipIfNoPermissions)
return false;
/* if a permissions error should be thrown, let set_config_option do it */
/* test for permissions and valid option value */
set_config_option(name, value,
superuser() ? PGC_SUSET : PGC_USERSET,
PGC_S_TEST, GUC_ACTION_SET, false);
return true;
}
/* /*
* assign_hook and show_hook subroutines * assign_hook and show_hook subroutines