mirror of
https://github.com/postgres/postgres.git
synced 2025-08-24 09:27:52 +03:00
The GRANT statement can now specify WITH INHERIT TRUE or WITH INHERIT FALSE to control whether the member inherits the granted role's permissions. For symmetry, you can now likewise write WITH ADMIN TRUE or WITH ADMIN FALSE to turn ADMIN OPTION on or off. If a GRANT does not specify WITH INHERIT, the behavior based on whether the member role is marked INHERIT or NOINHERIT. This means that if all roles are marked INHERIT or NOINHERIT before any role grants are performed, the behavior is identical to what we had before; otherwise, it's different, because ALTER ROLE [NO]INHERIT now only changes the default behavior of future grants, and has no effect on existing ones. Patch by me. Reviewed and testing by Nathan Bossart and Tushar Ahuja, with design-level comments from various others. Discussion: http://postgr.es/m/CA+Tgmoa5Sf4PiWrfxA=sGzDKg0Ojo3dADw=wAHOhR9dggV=RmQ@mail.gmail.com
2335 lines
68 KiB
C
2335 lines
68 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* user.c
|
|
* Commands for manipulating roles (formerly called users).
|
|
*
|
|
* Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* src/backend/commands/user.c
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/genam.h"
|
|
#include "access/htup_details.h"
|
|
#include "access/table.h"
|
|
#include "access/xact.h"
|
|
#include "catalog/binary_upgrade.h"
|
|
#include "catalog/catalog.h"
|
|
#include "catalog/dependency.h"
|
|
#include "catalog/indexing.h"
|
|
#include "catalog/objectaccess.h"
|
|
#include "catalog/pg_auth_members.h"
|
|
#include "catalog/pg_authid.h"
|
|
#include "catalog/pg_database.h"
|
|
#include "catalog/pg_db_role_setting.h"
|
|
#include "commands/comment.h"
|
|
#include "commands/dbcommands.h"
|
|
#include "commands/defrem.h"
|
|
#include "commands/seclabel.h"
|
|
#include "commands/user.h"
|
|
#include "libpq/crypt.h"
|
|
#include "miscadmin.h"
|
|
#include "storage/lmgr.h"
|
|
#include "utils/acl.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/catcache.h"
|
|
#include "utils/fmgroids.h"
|
|
#include "utils/syscache.h"
|
|
#include "utils/timestamp.h"
|
|
|
|
/*
|
|
* Removing a role grant - or the admin option on it - might recurse to
|
|
* dependent grants. We use these values to reason about what would need to
|
|
* be done in such cases.
|
|
*
|
|
* RRG_NOOP indicates a grant that would not need to be altered by the
|
|
* operation.
|
|
*
|
|
* RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
|
|
* admin_option set to false by the operation.
|
|
*
|
|
* RRG_REMOVE_INHERIT_OPTION indicates a grant that would need to have
|
|
* inherit_option set to false by the operation.
|
|
*
|
|
* RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
|
|
* by the operation.
|
|
*/
|
|
typedef enum
|
|
{
|
|
RRG_NOOP,
|
|
RRG_REMOVE_ADMIN_OPTION,
|
|
RRG_REMOVE_INHERIT_OPTION,
|
|
RRG_DELETE_GRANT
|
|
} RevokeRoleGrantAction;
|
|
|
|
/* Potentially set by pg_upgrade_support functions */
|
|
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
|
|
|
|
typedef struct
|
|
{
|
|
unsigned specified;
|
|
bool admin;
|
|
bool inherit;
|
|
} GrantRoleOptions;
|
|
|
|
#define GRANT_ROLE_SPECIFIED_ADMIN 0x0001
|
|
#define GRANT_ROLE_SPECIFIED_INHERIT 0x0002
|
|
|
|
/* GUC parameter */
|
|
int Password_encryption = PASSWORD_TYPE_SCRAM_SHA_256;
|
|
|
|
/* Hook to check passwords in CreateRole() and AlterRole() */
|
|
check_password_hook_type check_password_hook = NULL;
|
|
|
|
static void AddRoleMems(const char *rolename, Oid roleid,
|
|
List *memberSpecs, List *memberIds,
|
|
Oid grantorId, GrantRoleOptions *popt);
|
|
static void DelRoleMems(const char *rolename, Oid roleid,
|
|
List *memberSpecs, List *memberIds,
|
|
Oid grantorId, GrantRoleOptions *popt,
|
|
DropBehavior behavior);
|
|
static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
|
|
bool is_grant);
|
|
static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
|
|
static bool plan_single_revoke(CatCList *memlist,
|
|
RevokeRoleGrantAction *actions,
|
|
Oid member, Oid grantor,
|
|
GrantRoleOptions *popt,
|
|
DropBehavior behavior);
|
|
static void plan_member_revoke(CatCList *memlist,
|
|
RevokeRoleGrantAction *actions, Oid member);
|
|
static void plan_recursive_revoke(CatCList *memlist,
|
|
RevokeRoleGrantAction *actions,
|
|
int index,
|
|
bool revoke_admin_option_only,
|
|
DropBehavior behavior);
|
|
static void InitGrantRoleOptions(GrantRoleOptions *popt);
|
|
|
|
|
|
/* Check if current user has createrole privileges */
|
|
static bool
|
|
have_createrole_privilege(void)
|
|
{
|
|
return has_createrole_privilege(GetUserId());
|
|
}
|
|
|
|
|
|
/*
|
|
* CREATE ROLE
|
|
*/
|
|
Oid
|
|
CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
|
|
{
|
|
Relation pg_authid_rel;
|
|
TupleDesc pg_authid_dsc;
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_authid] = {0};
|
|
bool new_record_nulls[Natts_pg_authid] = {0};
|
|
Oid roleid;
|
|
ListCell *item;
|
|
ListCell *option;
|
|
char *password = NULL; /* user password */
|
|
bool issuper = false; /* Make the user a superuser? */
|
|
bool inherit = true; /* Auto inherit privileges? */
|
|
bool createrole = false; /* Can this user create roles? */
|
|
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 */
|
|
List *adminmembers = NIL; /* roles to be admins of this role */
|
|
char *validUntil = NULL; /* time the login is valid until */
|
|
Datum validUntil_datum; /* same, as timestamptz Datum */
|
|
bool validUntil_null;
|
|
DefElem *dpassword = NULL;
|
|
DefElem *dissuper = NULL;
|
|
DefElem *dinherit = NULL;
|
|
DefElem *dcreaterole = NULL;
|
|
DefElem *dcreatedb = NULL;
|
|
DefElem *dcanlogin = NULL;
|
|
DefElem *disreplication = NULL;
|
|
DefElem *dconnlimit = NULL;
|
|
DefElem *daddroleto = NULL;
|
|
DefElem *drolemembers = NULL;
|
|
DefElem *dadminmembers = NULL;
|
|
DefElem *dvalidUntil = NULL;
|
|
DefElem *dbypassRLS = NULL;
|
|
GrantRoleOptions popt;
|
|
|
|
/* The defaults can vary depending on the original statement type */
|
|
switch (stmt->stmt_type)
|
|
{
|
|
case ROLESTMT_ROLE:
|
|
break;
|
|
case ROLESTMT_USER:
|
|
canlogin = true;
|
|
/* may eventually want inherit to default to false here */
|
|
break;
|
|
case ROLESTMT_GROUP:
|
|
break;
|
|
}
|
|
|
|
/* Extract options from the statement node tree */
|
|
foreach(option, stmt->options)
|
|
{
|
|
DefElem *defel = (DefElem *) lfirst(option);
|
|
|
|
if (strcmp(defel->defname, "password") == 0)
|
|
{
|
|
if (dpassword)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dpassword = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "sysid") == 0)
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("SYSID can no longer be specified")));
|
|
}
|
|
else if (strcmp(defel->defname, "superuser") == 0)
|
|
{
|
|
if (dissuper)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dissuper = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "inherit") == 0)
|
|
{
|
|
if (dinherit)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dinherit = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createrole") == 0)
|
|
{
|
|
if (dcreaterole)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcreaterole = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createdb") == 0)
|
|
{
|
|
if (dcreatedb)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcreatedb = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "canlogin") == 0)
|
|
{
|
|
if (dcanlogin)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcanlogin = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "isreplication") == 0)
|
|
{
|
|
if (disreplication)
|
|
errorConflictingDefElem(defel, pstate);
|
|
disreplication = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "connectionlimit") == 0)
|
|
{
|
|
if (dconnlimit)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dconnlimit = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "addroleto") == 0)
|
|
{
|
|
if (daddroleto)
|
|
errorConflictingDefElem(defel, pstate);
|
|
daddroleto = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "rolemembers") == 0)
|
|
{
|
|
if (drolemembers)
|
|
errorConflictingDefElem(defel, pstate);
|
|
drolemembers = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "adminmembers") == 0)
|
|
{
|
|
if (dadminmembers)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dadminmembers = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "validUntil") == 0)
|
|
{
|
|
if (dvalidUntil)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dvalidUntil = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "bypassrls") == 0)
|
|
{
|
|
if (dbypassRLS)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dbypassRLS = defel;
|
|
}
|
|
else
|
|
elog(ERROR, "option \"%s\" not recognized",
|
|
defel->defname);
|
|
}
|
|
|
|
if (dpassword && dpassword->arg)
|
|
password = strVal(dpassword->arg);
|
|
if (dissuper)
|
|
issuper = boolVal(dissuper->arg);
|
|
if (dinherit)
|
|
inherit = boolVal(dinherit->arg);
|
|
if (dcreaterole)
|
|
createrole = boolVal(dcreaterole->arg);
|
|
if (dcreatedb)
|
|
createdb = boolVal(dcreatedb->arg);
|
|
if (dcanlogin)
|
|
canlogin = boolVal(dcanlogin->arg);
|
|
if (disreplication)
|
|
isreplication = boolVal(disreplication->arg);
|
|
if (dconnlimit)
|
|
{
|
|
connlimit = intVal(dconnlimit->arg);
|
|
if (connlimit < -1)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("invalid connection limit: %d", connlimit)));
|
|
}
|
|
if (daddroleto)
|
|
addroleto = (List *) daddroleto->arg;
|
|
if (drolemembers)
|
|
rolemembers = (List *) drolemembers->arg;
|
|
if (dadminmembers)
|
|
adminmembers = (List *) dadminmembers->arg;
|
|
if (dvalidUntil)
|
|
validUntil = strVal(dvalidUntil->arg);
|
|
if (dbypassRLS)
|
|
bypassrls = boolVal(dbypassRLS->arg);
|
|
|
|
/* Check some permissions first */
|
|
if (issuper)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to create superusers")));
|
|
}
|
|
else if (isreplication)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(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 create bypassrls users")));
|
|
}
|
|
else
|
|
{
|
|
if (!have_createrole_privilege())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to create role")));
|
|
}
|
|
|
|
/*
|
|
* Check that the user is not trying to create a role in the reserved
|
|
* "pg_" namespace.
|
|
*/
|
|
if (IsReservedName(stmt->role))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_RESERVED_NAME),
|
|
errmsg("role name \"%s\" is reserved",
|
|
stmt->role),
|
|
errdetail("Role names starting with \"pg_\" are reserved.")));
|
|
|
|
/*
|
|
* If built with appropriate switch, whine when regression-testing
|
|
* conventions for role names are violated.
|
|
*/
|
|
#ifdef ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS
|
|
if (strncmp(stmt->role, "regress_", 8) != 0)
|
|
elog(WARNING, "roles created by regression test cases should have names starting with \"regress_\"");
|
|
#endif
|
|
|
|
/*
|
|
* Check the pg_authid relation to be certain the role doesn't already
|
|
* exist.
|
|
*/
|
|
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
|
|
pg_authid_dsc = RelationGetDescr(pg_authid_rel);
|
|
|
|
if (OidIsValid(get_role_oid(stmt->role, true)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("role \"%s\" already exists",
|
|
stmt->role)));
|
|
|
|
/* Convert validuntil to internal form */
|
|
if (validUntil)
|
|
{
|
|
validUntil_datum = DirectFunctionCall3(timestamptz_in,
|
|
CStringGetDatum(validUntil),
|
|
ObjectIdGetDatum(InvalidOid),
|
|
Int32GetDatum(-1));
|
|
validUntil_null = false;
|
|
}
|
|
else
|
|
{
|
|
validUntil_datum = (Datum) 0;
|
|
validUntil_null = true;
|
|
}
|
|
|
|
/*
|
|
* Call the password checking hook if there is one defined
|
|
*/
|
|
if (check_password_hook && password)
|
|
(*check_password_hook) (stmt->role,
|
|
password,
|
|
get_password_type(password),
|
|
validUntil_datum,
|
|
validUntil_null);
|
|
|
|
/*
|
|
* Build a tuple to insert
|
|
*/
|
|
new_record[Anum_pg_authid_rolname - 1] =
|
|
DirectFunctionCall1(namein, CStringGetDatum(stmt->role));
|
|
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper);
|
|
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(inherit);
|
|
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole);
|
|
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
|
|
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
|
|
new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(isreplication);
|
|
new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
|
|
|
|
if (password)
|
|
{
|
|
char *shadow_pass;
|
|
const char *logdetail = NULL;
|
|
|
|
/*
|
|
* Don't allow an empty password. Libpq treats an empty password the
|
|
* same as no password at all, and won't even try to authenticate. But
|
|
* other clients might, so allowing it would be confusing. By clearing
|
|
* the password when an empty string is specified, the account is
|
|
* consistently locked for all clients.
|
|
*
|
|
* Note that this only covers passwords stored in the database itself.
|
|
* There are also checks in the authentication code, to forbid an
|
|
* empty password from being used with authentication methods that
|
|
* fetch the password from an external system, like LDAP or PAM.
|
|
*/
|
|
if (password[0] == '\0' ||
|
|
plain_crypt_verify(stmt->role, password, "", &logdetail) == STATUS_OK)
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("empty string is not a valid password, clearing password")));
|
|
new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
|
|
}
|
|
else
|
|
{
|
|
/* Encrypt the password to the requested format. */
|
|
shadow_pass = encrypt_password(Password_encryption, stmt->role,
|
|
password);
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
CStringGetTextDatum(shadow_pass);
|
|
}
|
|
}
|
|
else
|
|
new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
|
|
|
|
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);
|
|
|
|
/*
|
|
* pg_largeobject_metadata contains pg_authid.oid's, so we use the
|
|
* binary-upgrade override.
|
|
*/
|
|
if (IsBinaryUpgrade)
|
|
{
|
|
if (!OidIsValid(binary_upgrade_next_pg_authid_oid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("pg_authid OID value not set when in binary upgrade mode")));
|
|
|
|
roleid = binary_upgrade_next_pg_authid_oid;
|
|
binary_upgrade_next_pg_authid_oid = InvalidOid;
|
|
}
|
|
else
|
|
{
|
|
roleid = GetNewOidWithIndex(pg_authid_rel, AuthIdOidIndexId,
|
|
Anum_pg_authid_oid);
|
|
}
|
|
|
|
new_record[Anum_pg_authid_oid - 1] = ObjectIdGetDatum(roleid);
|
|
|
|
tuple = heap_form_tuple(pg_authid_dsc, new_record, new_record_nulls);
|
|
|
|
/*
|
|
* Insert new record in the pg_authid table
|
|
*/
|
|
CatalogTupleInsert(pg_authid_rel, tuple);
|
|
|
|
/*
|
|
* Advance command counter so we can see new record; else tests in
|
|
* AddRoleMems may fail.
|
|
*/
|
|
if (addroleto || adminmembers || rolemembers)
|
|
CommandCounterIncrement();
|
|
|
|
/* Default grant. */
|
|
InitGrantRoleOptions(&popt);
|
|
|
|
/*
|
|
* Add the new role to the specified existing roles.
|
|
*/
|
|
if (addroleto)
|
|
{
|
|
RoleSpec *thisrole = makeNode(RoleSpec);
|
|
List *thisrole_list = list_make1(thisrole);
|
|
List *thisrole_oidlist = list_make1_oid(roleid);
|
|
|
|
thisrole->roletype = ROLESPEC_CSTRING;
|
|
thisrole->rolename = stmt->role;
|
|
thisrole->location = -1;
|
|
|
|
foreach(item, addroleto)
|
|
{
|
|
RoleSpec *oldrole = lfirst(item);
|
|
HeapTuple oldroletup = get_rolespec_tuple(oldrole);
|
|
Form_pg_authid oldroleform = (Form_pg_authid) GETSTRUCT(oldroletup);
|
|
Oid oldroleid = oldroleform->oid;
|
|
char *oldrolename = NameStr(oldroleform->rolname);
|
|
|
|
AddRoleMems(oldrolename, oldroleid,
|
|
thisrole_list,
|
|
thisrole_oidlist,
|
|
InvalidOid, &popt);
|
|
|
|
ReleaseSysCache(oldroletup);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Add the specified members to this new role. adminmembers get the admin
|
|
* option, rolemembers don't.
|
|
*/
|
|
AddRoleMems(stmt->role, roleid,
|
|
rolemembers, roleSpecsToIds(rolemembers),
|
|
InvalidOid, &popt);
|
|
popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
|
|
popt.admin = true;
|
|
AddRoleMems(stmt->role, roleid,
|
|
adminmembers, roleSpecsToIds(adminmembers),
|
|
InvalidOid, &popt);
|
|
|
|
/* Post creation hook for new role */
|
|
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
|
|
|
|
/*
|
|
* Close pg_authid, but keep lock till commit.
|
|
*/
|
|
table_close(pg_authid_rel, NoLock);
|
|
|
|
return roleid;
|
|
}
|
|
|
|
|
|
/*
|
|
* ALTER ROLE
|
|
*
|
|
* Note: the rolemembers option accepted here is intended to support the
|
|
* backwards-compatible ALTER GROUP syntax. Although it will work to say
|
|
* "ALTER ROLE role ROLE rolenames", we don't document it.
|
|
*/
|
|
Oid
|
|
AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
|
|
{
|
|
Datum new_record[Natts_pg_authid] = {0};
|
|
bool new_record_nulls[Natts_pg_authid] = {0};
|
|
bool new_record_repl[Natts_pg_authid] = {0};
|
|
Relation pg_authid_rel;
|
|
TupleDesc pg_authid_dsc;
|
|
HeapTuple tuple,
|
|
new_tuple;
|
|
Form_pg_authid authform;
|
|
ListCell *option;
|
|
char *rolename;
|
|
char *password = NULL; /* user password */
|
|
int connlimit = -1; /* maximum connections allowed */
|
|
char *validUntil = NULL; /* time the login is valid until */
|
|
Datum validUntil_datum; /* same, as timestamptz Datum */
|
|
bool validUntil_null;
|
|
DefElem *dpassword = NULL;
|
|
DefElem *dissuper = NULL;
|
|
DefElem *dinherit = NULL;
|
|
DefElem *dcreaterole = NULL;
|
|
DefElem *dcreatedb = NULL;
|
|
DefElem *dcanlogin = NULL;
|
|
DefElem *disreplication = NULL;
|
|
DefElem *dconnlimit = NULL;
|
|
DefElem *drolemembers = NULL;
|
|
DefElem *dvalidUntil = NULL;
|
|
DefElem *dbypassRLS = NULL;
|
|
Oid roleid;
|
|
GrantRoleOptions popt;
|
|
|
|
check_rolespec_name(stmt->role,
|
|
"Cannot alter reserved roles.");
|
|
|
|
/* Extract options from the statement node tree */
|
|
foreach(option, stmt->options)
|
|
{
|
|
DefElem *defel = (DefElem *) lfirst(option);
|
|
|
|
if (strcmp(defel->defname, "password") == 0)
|
|
{
|
|
if (dpassword)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dpassword = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "superuser") == 0)
|
|
{
|
|
if (dissuper)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dissuper = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "inherit") == 0)
|
|
{
|
|
if (dinherit)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dinherit = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createrole") == 0)
|
|
{
|
|
if (dcreaterole)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcreaterole = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createdb") == 0)
|
|
{
|
|
if (dcreatedb)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcreatedb = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "canlogin") == 0)
|
|
{
|
|
if (dcanlogin)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dcanlogin = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "isreplication") == 0)
|
|
{
|
|
if (disreplication)
|
|
errorConflictingDefElem(defel, pstate);
|
|
disreplication = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "connectionlimit") == 0)
|
|
{
|
|
if (dconnlimit)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dconnlimit = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "rolemembers") == 0 &&
|
|
stmt->action != 0)
|
|
{
|
|
if (drolemembers)
|
|
errorConflictingDefElem(defel, pstate);
|
|
drolemembers = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "validUntil") == 0)
|
|
{
|
|
if (dvalidUntil)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dvalidUntil = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "bypassrls") == 0)
|
|
{
|
|
if (dbypassRLS)
|
|
errorConflictingDefElem(defel, pstate);
|
|
dbypassRLS = defel;
|
|
}
|
|
else
|
|
elog(ERROR, "option \"%s\" not recognized",
|
|
defel->defname);
|
|
}
|
|
|
|
if (dpassword && dpassword->arg)
|
|
password = strVal(dpassword->arg);
|
|
if (dconnlimit)
|
|
{
|
|
connlimit = intVal(dconnlimit->arg);
|
|
if (connlimit < -1)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("invalid connection limit: %d", connlimit)));
|
|
}
|
|
if (dvalidUntil)
|
|
validUntil = strVal(dvalidUntil->arg);
|
|
|
|
/*
|
|
* Scan the pg_authid relation to be certain the user exists.
|
|
*/
|
|
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
|
|
pg_authid_dsc = RelationGetDescr(pg_authid_rel);
|
|
|
|
tuple = get_rolespec_tuple(stmt->role);
|
|
authform = (Form_pg_authid) GETSTRUCT(tuple);
|
|
rolename = pstrdup(NameStr(authform->rolname));
|
|
roleid = authform->oid;
|
|
|
|
/*
|
|
* To mess with a superuser or replication role in any way you gotta be
|
|
* superuser. We also insist on superuser to change the BYPASSRLS
|
|
* property. Otherwise, if you don't have createrole, you're only allowed
|
|
* to (1) change your own password or (2) add members to a role for which
|
|
* you have ADMIN OPTION.
|
|
*/
|
|
if (authform->rolsuper || dissuper)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter superuser roles or change superuser attribute")));
|
|
}
|
|
else if (authform->rolreplication || disreplication)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter replication roles or change replication attribute")));
|
|
}
|
|
else if (dbypassRLS)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to change bypassrls attribute")));
|
|
}
|
|
else if (!have_createrole_privilege())
|
|
{
|
|
/* things you certainly can't do without CREATEROLE */
|
|
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
|
|
dvalidUntil)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied")));
|
|
|
|
/* without CREATEROLE, can only change your own password */
|
|
if (dpassword && roleid != GetUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must have CREATEROLE privilege to change another user's password")));
|
|
|
|
/* without CREATEROLE, can only add members to roles you admin */
|
|
if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must have admin option on role \"%s\" to add members",
|
|
rolename)));
|
|
}
|
|
|
|
/* Convert validuntil to internal form */
|
|
if (dvalidUntil)
|
|
{
|
|
validUntil_datum = DirectFunctionCall3(timestamptz_in,
|
|
CStringGetDatum(validUntil),
|
|
ObjectIdGetDatum(InvalidOid),
|
|
Int32GetDatum(-1));
|
|
validUntil_null = false;
|
|
}
|
|
else
|
|
{
|
|
/* fetch existing setting in case hook needs it */
|
|
validUntil_datum = SysCacheGetAttr(AUTHNAME, tuple,
|
|
Anum_pg_authid_rolvaliduntil,
|
|
&validUntil_null);
|
|
}
|
|
|
|
/*
|
|
* Call the password checking hook if there is one defined
|
|
*/
|
|
if (check_password_hook && password)
|
|
(*check_password_hook) (rolename,
|
|
password,
|
|
get_password_type(password),
|
|
validUntil_datum,
|
|
validUntil_null);
|
|
|
|
/*
|
|
* Build an updated tuple, perusing the information just obtained
|
|
*/
|
|
|
|
/*
|
|
* issuper/createrole/etc
|
|
*/
|
|
if (dissuper)
|
|
{
|
|
bool should_be_super = boolVal(dissuper->arg);
|
|
|
|
if (!should_be_super && roleid == BOOTSTRAP_SUPERUSERID)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied: bootstrap user must be superuser")));
|
|
|
|
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(should_be_super);
|
|
new_record_repl[Anum_pg_authid_rolsuper - 1] = true;
|
|
}
|
|
|
|
if (dinherit)
|
|
{
|
|
new_record[Anum_pg_authid_rolinherit - 1] = BoolGetDatum(boolVal(dinherit->arg));
|
|
new_record_repl[Anum_pg_authid_rolinherit - 1] = true;
|
|
}
|
|
|
|
if (dcreaterole)
|
|
{
|
|
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(boolVal(dcreaterole->arg));
|
|
new_record_repl[Anum_pg_authid_rolcreaterole - 1] = true;
|
|
}
|
|
|
|
if (dcreatedb)
|
|
{
|
|
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(boolVal(dcreatedb->arg));
|
|
new_record_repl[Anum_pg_authid_rolcreatedb - 1] = true;
|
|
}
|
|
|
|
if (dcanlogin)
|
|
{
|
|
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(boolVal(dcanlogin->arg));
|
|
new_record_repl[Anum_pg_authid_rolcanlogin - 1] = true;
|
|
}
|
|
|
|
if (disreplication)
|
|
{
|
|
new_record[Anum_pg_authid_rolreplication - 1] = BoolGetDatum(boolVal(disreplication->arg));
|
|
new_record_repl[Anum_pg_authid_rolreplication - 1] = true;
|
|
}
|
|
|
|
if (dconnlimit)
|
|
{
|
|
new_record[Anum_pg_authid_rolconnlimit - 1] = Int32GetDatum(connlimit);
|
|
new_record_repl[Anum_pg_authid_rolconnlimit - 1] = true;
|
|
}
|
|
|
|
/* password */
|
|
if (password)
|
|
{
|
|
char *shadow_pass;
|
|
const char *logdetail = NULL;
|
|
|
|
/* Like in CREATE USER, don't allow an empty password. */
|
|
if (password[0] == '\0' ||
|
|
plain_crypt_verify(rolename, password, "", &logdetail) == STATUS_OK)
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("empty string is not a valid password, clearing password")));
|
|
new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
|
|
}
|
|
else
|
|
{
|
|
/* Encrypt the password to the requested format. */
|
|
shadow_pass = encrypt_password(Password_encryption, rolename,
|
|
password);
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
CStringGetTextDatum(shadow_pass);
|
|
}
|
|
new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
|
|
}
|
|
|
|
/* unset password */
|
|
if (dpassword && dpassword->arg == NULL)
|
|
{
|
|
new_record_repl[Anum_pg_authid_rolpassword - 1] = true;
|
|
new_record_nulls[Anum_pg_authid_rolpassword - 1] = true;
|
|
}
|
|
|
|
/* valid until */
|
|
new_record[Anum_pg_authid_rolvaliduntil - 1] = validUntil_datum;
|
|
new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = validUntil_null;
|
|
new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = true;
|
|
|
|
if (dbypassRLS)
|
|
{
|
|
new_record[Anum_pg_authid_rolbypassrls - 1] = BoolGetDatum(boolVal(dbypassRLS->arg));
|
|
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);
|
|
CatalogTupleUpdate(pg_authid_rel, &tuple->t_self, new_tuple);
|
|
|
|
InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
|
|
|
|
ReleaseSysCache(tuple);
|
|
heap_freetuple(new_tuple);
|
|
|
|
InitGrantRoleOptions(&popt);
|
|
|
|
/*
|
|
* Advance command counter so we can see new record; else tests in
|
|
* AddRoleMems may fail.
|
|
*/
|
|
if (drolemembers)
|
|
{
|
|
List *rolemembers = (List *) drolemembers->arg;
|
|
|
|
CommandCounterIncrement();
|
|
|
|
if (stmt->action == +1) /* add members to role */
|
|
AddRoleMems(rolename, roleid,
|
|
rolemembers, roleSpecsToIds(rolemembers),
|
|
InvalidOid, &popt);
|
|
else if (stmt->action == -1) /* drop members from role */
|
|
DelRoleMems(rolename, roleid,
|
|
rolemembers, roleSpecsToIds(rolemembers),
|
|
InvalidOid, &popt, DROP_RESTRICT);
|
|
}
|
|
|
|
/*
|
|
* Close pg_authid, but keep lock till commit.
|
|
*/
|
|
table_close(pg_authid_rel, NoLock);
|
|
|
|
return roleid;
|
|
}
|
|
|
|
|
|
/*
|
|
* ALTER ROLE ... SET
|
|
*/
|
|
Oid
|
|
AlterRoleSet(AlterRoleSetStmt *stmt)
|
|
{
|
|
HeapTuple roletuple;
|
|
Form_pg_authid roleform;
|
|
Oid databaseid = InvalidOid;
|
|
Oid roleid = InvalidOid;
|
|
|
|
if (stmt->role)
|
|
{
|
|
check_rolespec_name(stmt->role,
|
|
"Cannot alter reserved roles.");
|
|
|
|
roletuple = get_rolespec_tuple(stmt->role);
|
|
roleform = (Form_pg_authid) GETSTRUCT(roletuple);
|
|
roleid = roleform->oid;
|
|
|
|
/*
|
|
* Obtain a lock on the role and make sure it didn't go away in the
|
|
* meantime.
|
|
*/
|
|
shdepLockAndCheckObject(AuthIdRelationId, roleid);
|
|
|
|
/*
|
|
* To mess with a superuser you gotta be superuser; else you need
|
|
* createrole, or just want to change your own settings
|
|
*/
|
|
if (roleform->rolsuper)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter superusers")));
|
|
}
|
|
else
|
|
{
|
|
if (!have_createrole_privilege() && roleid != GetUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied")));
|
|
}
|
|
|
|
ReleaseSysCache(roletuple);
|
|
}
|
|
|
|
/* look up and lock the database, if specified */
|
|
if (stmt->database != NULL)
|
|
{
|
|
databaseid = get_database_oid(stmt->database, false);
|
|
shdepLockAndCheckObject(DatabaseRelationId, databaseid);
|
|
|
|
if (!stmt->role)
|
|
{
|
|
/*
|
|
* If no role is specified, then this is effectively the same as
|
|
* ALTER DATABASE ... SET, so use the same permission check.
|
|
*/
|
|
if (!pg_database_ownercheck(databaseid, GetUserId()))
|
|
aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_DATABASE,
|
|
stmt->database);
|
|
}
|
|
}
|
|
|
|
if (!stmt->role && !stmt->database)
|
|
{
|
|
/* Must be superuser to alter settings globally. */
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter settings globally")));
|
|
}
|
|
|
|
AlterSetting(databaseid, roleid, stmt->setstmt);
|
|
|
|
return roleid;
|
|
}
|
|
|
|
|
|
/*
|
|
* DROP ROLE
|
|
*/
|
|
void
|
|
DropRole(DropRoleStmt *stmt)
|
|
{
|
|
Relation pg_authid_rel,
|
|
pg_auth_members_rel;
|
|
ListCell *item;
|
|
List *role_addresses = NIL;
|
|
|
|
if (!have_createrole_privilege())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to drop role")));
|
|
|
|
/*
|
|
* Scan the pg_authid relation to find the Oid of the role(s) to be
|
|
* deleted and perform preliminary permissions and sanity checks.
|
|
*/
|
|
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
|
|
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
|
|
|
|
foreach(item, stmt->roles)
|
|
{
|
|
RoleSpec *rolspec = lfirst(item);
|
|
char *role;
|
|
HeapTuple tuple,
|
|
tmp_tuple;
|
|
Form_pg_authid roleform;
|
|
ScanKeyData scankey;
|
|
SysScanDesc sscan;
|
|
Oid roleid;
|
|
ObjectAddress *role_address;
|
|
|
|
if (rolspec->roletype != ROLESPEC_CSTRING)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("cannot use special role specifier in DROP ROLE")));
|
|
role = rolspec->rolename;
|
|
|
|
tuple = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
|
|
if (!HeapTupleIsValid(tuple))
|
|
{
|
|
if (!stmt->missing_ok)
|
|
{
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("role \"%s\" does not exist", role)));
|
|
}
|
|
else
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("role \"%s\" does not exist, skipping",
|
|
role)));
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
roleform = (Form_pg_authid) GETSTRUCT(tuple);
|
|
roleid = roleform->oid;
|
|
|
|
if (roleid == GetUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("current user cannot be dropped")));
|
|
if (roleid == GetOuterUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("current user cannot be dropped")));
|
|
if (roleid == GetSessionUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("session user cannot be dropped")));
|
|
|
|
/*
|
|
* For safety's sake, we allow createrole holders to drop ordinary
|
|
* roles but not superuser roles. This is mainly to avoid the
|
|
* scenario where you accidentally drop the last superuser.
|
|
*/
|
|
if (roleform->rolsuper && !superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to drop superusers")));
|
|
|
|
/* DROP hook for the role being removed */
|
|
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
|
|
|
|
/* Don't leak the syscache tuple */
|
|
ReleaseSysCache(tuple);
|
|
|
|
/*
|
|
* Lock the role, so nobody can add dependencies to her while we drop
|
|
* her. We keep the lock until the end of transaction.
|
|
*/
|
|
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
|
|
|
|
/*
|
|
* If there is a pg_auth_members entry that has one of the roles to be
|
|
* dropped as the roleid or member, it should be silently removed, but
|
|
* if there is a pg_auth_members entry that has one of the roles to be
|
|
* dropped as the grantor, the operation should fail.
|
|
*
|
|
* It's possible, however, that a single pg_auth_members entry could
|
|
* fall into multiple categories - e.g. the user could do "GRANT foo
|
|
* TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want such
|
|
* an operation to succeed regardless of the order in which the
|
|
* to-be-dropped roles are passed to DROP ROLE.
|
|
*
|
|
* To make that work, we remove all pg_auth_members entries that can
|
|
* be silently removed in this loop, and then below we'll make a
|
|
* second pass over the list of roles to be removed and check for any
|
|
* remaining dependencies.
|
|
*/
|
|
ScanKeyInit(&scankey,
|
|
Anum_pg_auth_members_roleid,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(roleid));
|
|
|
|
sscan = systable_beginscan(pg_auth_members_rel, AuthMemRoleMemIndexId,
|
|
true, NULL, 1, &scankey);
|
|
|
|
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
|
|
{
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
|
|
deleteSharedDependencyRecordsFor(AuthMemRelationId,
|
|
authmem_form->oid, 0);
|
|
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
|
|
}
|
|
|
|
systable_endscan(sscan);
|
|
|
|
ScanKeyInit(&scankey,
|
|
Anum_pg_auth_members_member,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
ObjectIdGetDatum(roleid));
|
|
|
|
sscan = systable_beginscan(pg_auth_members_rel, AuthMemMemRoleIndexId,
|
|
true, NULL, 1, &scankey);
|
|
|
|
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
|
|
{
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
|
|
deleteSharedDependencyRecordsFor(AuthMemRelationId,
|
|
authmem_form->oid, 0);
|
|
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
|
|
}
|
|
|
|
systable_endscan(sscan);
|
|
|
|
/*
|
|
* Advance command counter so that later iterations of this loop will
|
|
* see the changes already made. This is essential if, for example,
|
|
* we are trying to drop both a role and one of its direct members ---
|
|
* we'll get an error if we try to delete the linking pg_auth_members
|
|
* tuple twice. (We do not need a CCI between the two delete loops
|
|
* above, because it's not allowed for a role to directly contain
|
|
* itself.)
|
|
*/
|
|
CommandCounterIncrement();
|
|
|
|
/* Looks tentatively OK, add it to the list. */
|
|
role_address = palloc(sizeof(ObjectAddress));
|
|
role_address->classId = AuthIdRelationId;
|
|
role_address->objectId = roleid;
|
|
role_address->objectSubId = 0;
|
|
role_addresses = lappend(role_addresses, role_address);
|
|
}
|
|
|
|
/*
|
|
* Second pass over the roles to be removed.
|
|
*/
|
|
foreach(item, role_addresses)
|
|
{
|
|
ObjectAddress *role_address = lfirst(item);
|
|
Oid roleid = role_address->objectId;
|
|
HeapTuple tuple;
|
|
Form_pg_authid roleform;
|
|
char *detail;
|
|
char *detail_log;
|
|
|
|
/*
|
|
* Re-find the pg_authid tuple.
|
|
*
|
|
* Since we've taken a lock on the role OID, it shouldn't be possible
|
|
* for the tuple to have been deleted -- or for that matter updated --
|
|
* unless the user is manually modifying the system catalogs.
|
|
*/
|
|
tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
|
|
if (!HeapTupleIsValid(tuple))
|
|
elog(ERROR, "could not find tuple for role %u", roleid);
|
|
roleform = (Form_pg_authid) GETSTRUCT(tuple);
|
|
|
|
/*
|
|
* Check for pg_shdepend entries depending on this role.
|
|
*
|
|
* This needs to happen after we've completed removing any
|
|
* pg_auth_members entries that can be removed silently, in order to
|
|
* avoid spurious failures. See notes above for more details.
|
|
*/
|
|
if (checkSharedDependencies(AuthIdRelationId, roleid,
|
|
&detail, &detail_log))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
|
|
errmsg("role \"%s\" cannot be dropped because some objects depend on it",
|
|
NameStr(roleform->rolname)),
|
|
errdetail_internal("%s", detail),
|
|
errdetail_log("%s", detail_log)));
|
|
|
|
/*
|
|
* Remove the role from the pg_authid table
|
|
*/
|
|
CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
|
|
|
|
ReleaseSysCache(tuple);
|
|
|
|
/*
|
|
* Remove any comments or security labels on this role.
|
|
*/
|
|
DeleteSharedComments(roleid, AuthIdRelationId);
|
|
DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
|
|
|
|
/*
|
|
* Remove settings for this role.
|
|
*/
|
|
DropSetting(InvalidOid, roleid);
|
|
}
|
|
|
|
/*
|
|
* Now we can clean up; but keep locks until commit.
|
|
*/
|
|
table_close(pg_auth_members_rel, NoLock);
|
|
table_close(pg_authid_rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* Rename role
|
|
*/
|
|
ObjectAddress
|
|
RenameRole(const char *oldname, const char *newname)
|
|
{
|
|
HeapTuple oldtuple,
|
|
newtuple;
|
|
TupleDesc dsc;
|
|
Relation rel;
|
|
Datum datum;
|
|
bool isnull;
|
|
Datum repl_val[Natts_pg_authid];
|
|
bool repl_null[Natts_pg_authid];
|
|
bool repl_repl[Natts_pg_authid];
|
|
int i;
|
|
Oid roleid;
|
|
ObjectAddress address;
|
|
Form_pg_authid authform;
|
|
|
|
rel = table_open(AuthIdRelationId, RowExclusiveLock);
|
|
dsc = RelationGetDescr(rel);
|
|
|
|
oldtuple = SearchSysCache1(AUTHNAME, CStringGetDatum(oldname));
|
|
if (!HeapTupleIsValid(oldtuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("role \"%s\" does not exist", oldname)));
|
|
|
|
/*
|
|
* XXX Client applications probably store the session user somewhere, so
|
|
* renaming it could cause confusion. On the other hand, there may not be
|
|
* an actual problem besides a little confusion, so think about this and
|
|
* decide. Same for SET ROLE ... we don't restrict renaming the current
|
|
* effective userid, though.
|
|
*/
|
|
|
|
authform = (Form_pg_authid) GETSTRUCT(oldtuple);
|
|
roleid = authform->oid;
|
|
|
|
if (roleid == GetSessionUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("session user cannot be renamed")));
|
|
if (roleid == GetOuterUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("current user cannot be renamed")));
|
|
|
|
/*
|
|
* Check that the user is not trying to rename a system role and not
|
|
* trying to rename a role into the reserved "pg_" namespace.
|
|
*/
|
|
if (IsReservedName(NameStr(authform->rolname)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_RESERVED_NAME),
|
|
errmsg("role name \"%s\" is reserved",
|
|
NameStr(authform->rolname)),
|
|
errdetail("Role names starting with \"pg_\" are reserved.")));
|
|
|
|
if (IsReservedName(newname))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_RESERVED_NAME),
|
|
errmsg("role name \"%s\" is reserved",
|
|
newname),
|
|
errdetail("Role names starting with \"pg_\" are reserved.")));
|
|
|
|
/*
|
|
* If built with appropriate switch, whine when regression-testing
|
|
* conventions for role names are violated.
|
|
*/
|
|
#ifdef ENFORCE_REGRESSION_TEST_NAME_RESTRICTIONS
|
|
if (strncmp(newname, "regress_", 8) != 0)
|
|
elog(WARNING, "roles created by regression test cases should have names starting with \"regress_\"");
|
|
#endif
|
|
|
|
/* make sure the new name doesn't exist */
|
|
if (SearchSysCacheExists1(AUTHNAME, CStringGetDatum(newname)))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("role \"%s\" already exists", newname)));
|
|
|
|
/*
|
|
* createrole is enough privilege unless you want to mess with a superuser
|
|
*/
|
|
if (((Form_pg_authid) GETSTRUCT(oldtuple))->rolsuper)
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to rename superusers")));
|
|
}
|
|
else
|
|
{
|
|
if (!have_createrole_privilege())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to rename role")));
|
|
}
|
|
|
|
/* OK, construct the modified tuple */
|
|
for (i = 0; i < Natts_pg_authid; i++)
|
|
repl_repl[i] = false;
|
|
|
|
repl_repl[Anum_pg_authid_rolname - 1] = true;
|
|
repl_val[Anum_pg_authid_rolname - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(newname));
|
|
repl_null[Anum_pg_authid_rolname - 1] = false;
|
|
|
|
datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
|
|
|
|
if (!isnull && get_password_type(TextDatumGetCString(datum)) == PASSWORD_TYPE_MD5)
|
|
{
|
|
/* MD5 uses the username as salt, so just clear it on a rename */
|
|
repl_repl[Anum_pg_authid_rolpassword - 1] = true;
|
|
repl_null[Anum_pg_authid_rolpassword - 1] = true;
|
|
|
|
ereport(NOTICE,
|
|
(errmsg("MD5 password cleared because of role rename")));
|
|
}
|
|
|
|
newtuple = heap_modify_tuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
|
|
CatalogTupleUpdate(rel, &oldtuple->t_self, newtuple);
|
|
|
|
InvokeObjectPostAlterHook(AuthIdRelationId, roleid, 0);
|
|
|
|
ObjectAddressSet(address, AuthIdRelationId, roleid);
|
|
|
|
ReleaseSysCache(oldtuple);
|
|
|
|
/*
|
|
* Close pg_authid, but keep lock till commit.
|
|
*/
|
|
table_close(rel, NoLock);
|
|
|
|
return address;
|
|
}
|
|
|
|
/*
|
|
* GrantRoleStmt
|
|
*
|
|
* Grant/Revoke roles to/from roles
|
|
*/
|
|
void
|
|
GrantRole(ParseState *pstate, GrantRoleStmt *stmt)
|
|
{
|
|
Relation pg_authid_rel;
|
|
Oid grantor;
|
|
List *grantee_ids;
|
|
ListCell *item;
|
|
GrantRoleOptions popt;
|
|
|
|
/* Parse options list. */
|
|
InitGrantRoleOptions(&popt);
|
|
foreach(item, stmt->opt)
|
|
{
|
|
DefElem *opt = (DefElem *) lfirst(item);
|
|
char *optval = defGetString(opt);
|
|
|
|
if (strcmp(opt->defname, "admin") == 0)
|
|
{
|
|
popt.specified |= GRANT_ROLE_SPECIFIED_ADMIN;
|
|
|
|
if (parse_bool(optval, &popt.admin))
|
|
continue;
|
|
}
|
|
else if (strcmp(opt->defname, "inherit") == 0)
|
|
{
|
|
popt.specified |= GRANT_ROLE_SPECIFIED_INHERIT;
|
|
if (parse_bool(optval, &popt.inherit))
|
|
continue;
|
|
}
|
|
else
|
|
ereport(ERROR,
|
|
errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("unrecognized role option \"%s\"", opt->defname),
|
|
parser_errposition(pstate, opt->location));
|
|
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("unrecognized value for role option \"%s\": \"%s\"",
|
|
opt->defname, optval),
|
|
parser_errposition(pstate, opt->location)));
|
|
}
|
|
|
|
/* Lookup OID of grantor, if specified. */
|
|
if (stmt->grantor)
|
|
grantor = get_rolespec_oid(stmt->grantor, false);
|
|
else
|
|
grantor = InvalidOid;
|
|
|
|
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
|
|
|
|
/* AccessShareLock is enough since we aren't modifying pg_authid */
|
|
pg_authid_rel = table_open(AuthIdRelationId, AccessShareLock);
|
|
|
|
/*
|
|
* Step through all of the granted roles and add, update, or remove
|
|
* entries in pg_auth_members as appropriate. If stmt->is_grant is true,
|
|
* we are adding new grants or, if they already exist, updating options
|
|
* on those grants. If stmt->is_grant is false, we are revoking grants or
|
|
* removing options from them.
|
|
*/
|
|
foreach(item, stmt->granted_roles)
|
|
{
|
|
AccessPriv *priv = (AccessPriv *) lfirst(item);
|
|
char *rolename = priv->priv_name;
|
|
Oid roleid;
|
|
|
|
/* Must reject priv(columns) and ALL PRIVILEGES(columns) */
|
|
if (rolename == NULL || priv->cols != NIL)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
|
|
errmsg("column names cannot be included in GRANT/REVOKE ROLE")));
|
|
|
|
roleid = get_role_oid(rolename, false);
|
|
if (stmt->is_grant)
|
|
AddRoleMems(rolename, roleid,
|
|
stmt->grantee_roles, grantee_ids,
|
|
grantor, &popt);
|
|
else
|
|
DelRoleMems(rolename, roleid,
|
|
stmt->grantee_roles, grantee_ids,
|
|
grantor, &popt, stmt->behavior);
|
|
}
|
|
|
|
/*
|
|
* Close pg_authid, but keep lock till commit.
|
|
*/
|
|
table_close(pg_authid_rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* DropOwnedObjects
|
|
*
|
|
* Drop the objects owned by a given list of roles.
|
|
*/
|
|
void
|
|
DropOwnedObjects(DropOwnedStmt *stmt)
|
|
{
|
|
List *role_ids = roleSpecsToIds(stmt->roles);
|
|
ListCell *cell;
|
|
|
|
/* Check privileges */
|
|
foreach(cell, role_ids)
|
|
{
|
|
Oid roleid = lfirst_oid(cell);
|
|
|
|
if (!has_privs_of_role(GetUserId(), roleid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to drop objects")));
|
|
}
|
|
|
|
/* Ok, do it */
|
|
shdepDropOwned(role_ids, stmt->behavior);
|
|
}
|
|
|
|
/*
|
|
* ReassignOwnedObjects
|
|
*
|
|
* Give the objects owned by a given list of roles away to another user.
|
|
*/
|
|
void
|
|
ReassignOwnedObjects(ReassignOwnedStmt *stmt)
|
|
{
|
|
List *role_ids = roleSpecsToIds(stmt->roles);
|
|
ListCell *cell;
|
|
Oid newrole;
|
|
|
|
/* Check privileges */
|
|
foreach(cell, role_ids)
|
|
{
|
|
Oid roleid = lfirst_oid(cell);
|
|
|
|
if (!has_privs_of_role(GetUserId(), roleid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to reassign objects")));
|
|
}
|
|
|
|
/* Must have privileges on the receiving side too */
|
|
newrole = get_rolespec_oid(stmt->newrole, false);
|
|
|
|
if (!has_privs_of_role(GetUserId(), newrole))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to reassign objects")));
|
|
|
|
/* Ok, do it */
|
|
shdepReassignOwned(role_ids, newrole);
|
|
}
|
|
|
|
/*
|
|
* roleSpecsToIds
|
|
*
|
|
* Given a list of RoleSpecs, generate a list of role OIDs in the same order.
|
|
*
|
|
* ROLESPEC_PUBLIC is not allowed.
|
|
*/
|
|
List *
|
|
roleSpecsToIds(List *memberNames)
|
|
{
|
|
List *result = NIL;
|
|
ListCell *l;
|
|
|
|
foreach(l, memberNames)
|
|
{
|
|
RoleSpec *rolespec = lfirst_node(RoleSpec, l);
|
|
Oid roleid;
|
|
|
|
roleid = get_rolespec_oid(rolespec, false);
|
|
result = lappend_oid(result, roleid);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* AddRoleMems -- Add given members to the specified role
|
|
*
|
|
* rolename: name of role to add to (used only for error messages)
|
|
* roleid: OID of role to add to
|
|
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
|
|
* memberIds: OIDs of roles to add
|
|
* grantorId: who is granting the membership (InvalidOid if not set explicitly)
|
|
* popt: information about grant options
|
|
*/
|
|
static void
|
|
AddRoleMems(const char *rolename, Oid roleid,
|
|
List *memberSpecs, List *memberIds,
|
|
Oid grantorId, GrantRoleOptions *popt)
|
|
{
|
|
Relation pg_authmem_rel;
|
|
TupleDesc pg_authmem_dsc;
|
|
ListCell *specitem;
|
|
ListCell *iditem;
|
|
Oid currentUserId = GetUserId();
|
|
|
|
Assert(list_length(memberSpecs) == list_length(memberIds));
|
|
|
|
/* Skip permission check if nothing to do */
|
|
if (!memberIds)
|
|
return;
|
|
|
|
/*
|
|
* Check permissions: must have createrole or admin option on the role to
|
|
* be changed. To mess with a superuser role, you gotta be superuser.
|
|
*/
|
|
if (superuser_arg(roleid))
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter superusers")));
|
|
}
|
|
else
|
|
{
|
|
if (!have_createrole_privilege() &&
|
|
!is_admin_of_role(currentUserId, roleid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must have admin option on role \"%s\"",
|
|
rolename)));
|
|
}
|
|
|
|
/*
|
|
* The charter of pg_database_owner is to have exactly one, implicit,
|
|
* situation-dependent member. There's no technical need for this
|
|
* restriction. (One could lift it and take the further step of making
|
|
* pg_database_ownercheck() equivalent to has_privs_of_role(roleid,
|
|
* ROLE_PG_DATABASE_OWNER), in which case explicit, situation-independent
|
|
* members could act as the owner of any database.)
|
|
*/
|
|
if (roleid == ROLE_PG_DATABASE_OWNER)
|
|
ereport(ERROR,
|
|
errmsg("role \"%s\" cannot have explicit members", rolename));
|
|
|
|
/* Validate grantor (and resolve implicit grantor if not specified). */
|
|
grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
|
|
|
|
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
|
|
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
|
|
|
|
/*
|
|
* Only allow changes to this role by one backend at a time, so that we
|
|
* can check integrity constraints like the lack of circular ADMIN OPTION
|
|
* grants without fear of race conditions.
|
|
*/
|
|
LockSharedObject(AuthIdRelationId, roleid, 0,
|
|
ShareUpdateExclusiveLock);
|
|
|
|
/* Preliminary sanity checks. */
|
|
forboth(specitem, memberSpecs, iditem, memberIds)
|
|
{
|
|
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
|
|
Oid memberid = lfirst_oid(iditem);
|
|
|
|
/*
|
|
* pg_database_owner is never a role member. Lifting this restriction
|
|
* would require a policy decision about membership loops. One could
|
|
* prevent loops, which would include making "ALTER DATABASE x OWNER
|
|
* TO proposed_datdba" fail if is_member_of_role(pg_database_owner,
|
|
* proposed_datdba). Hence, gaining a membership could reduce what a
|
|
* role could do. Alternately, one could allow these memberships to
|
|
* complete loops. A role could then have actual WITH ADMIN OPTION on
|
|
* itself, prompting a decision about is_admin_of_role() treatment of
|
|
* the case.
|
|
*
|
|
* Lifting this restriction also has policy implications for ownership
|
|
* of shared objects (databases and tablespaces). We allow such
|
|
* ownership, but we might find cause to ban it in the future.
|
|
* Designing such a ban would more troublesome if the design had to
|
|
* address pg_database_owner being a member of role FOO that owns a
|
|
* shared object. (The effect of such ownership is that any owner of
|
|
* another database can act as the owner of affected shared objects.)
|
|
*/
|
|
if (memberid == ROLE_PG_DATABASE_OWNER)
|
|
ereport(ERROR,
|
|
errmsg("role \"%s\" cannot be a member of any role",
|
|
get_rolespec_name(memberRole)));
|
|
|
|
/*
|
|
* Refuse creation of membership loops, including the trivial case
|
|
* where a role is made a member of itself. We do this by checking to
|
|
* see if the target role is already a member of the proposed member
|
|
* role. We have to ignore possible superuserness, however, else we
|
|
* could never grant membership in a superuser-privileged role.
|
|
*/
|
|
if (is_member_of_role_nosuper(roleid, memberid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
|
|
errmsg("role \"%s\" is a member of role \"%s\"",
|
|
rolename, get_rolespec_name(memberRole))));
|
|
}
|
|
|
|
/*
|
|
* Disallow attempts to grant ADMIN OPTION back to a user who granted it
|
|
* to you, similar to what check_circularity does for ACLs. We want the
|
|
* chains of grants to remain acyclic, so that it's always possible to use
|
|
* REVOKE .. CASCADE to clean up all grants that depend on the one being
|
|
* revoked.
|
|
*
|
|
* NB: This check might look redundant with the check for membership loops
|
|
* above, but it isn't. That's checking for role-member loop (e.g. A is a
|
|
* member of B and B is a member of A) while this is checking for a
|
|
* member-grantor loop (e.g. A gave ADMIN OPTION on X to B and now B, who
|
|
* has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION on
|
|
* X back to A).
|
|
*/
|
|
if (popt->admin && grantorId != BOOTSTRAP_SUPERUSERID)
|
|
{
|
|
CatCList *memlist;
|
|
RevokeRoleGrantAction *actions;
|
|
int i;
|
|
|
|
/* Get the list of members for this role. */
|
|
memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid));
|
|
|
|
/*
|
|
* Figure out what would happen if we removed all existing grants to
|
|
* every role to which we've been asked to make a new grant.
|
|
*/
|
|
actions = initialize_revoke_actions(memlist);
|
|
foreach(iditem, memberIds)
|
|
{
|
|
Oid memberid = lfirst_oid(iditem);
|
|
|
|
if (memberid == BOOTSTRAP_SUPERUSERID)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
|
|
errmsg("admin option cannot be granted back to your own grantor")));
|
|
plan_member_revoke(memlist, actions, memberid);
|
|
}
|
|
|
|
/*
|
|
* If the result would be that the grantor role would no longer have
|
|
* the ability to perform the grant, then the proposed grant would
|
|
* create a circularity.
|
|
*/
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple authmem_tuple;
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
authmem_tuple = &memlist->members[i]->tuple;
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
if (actions[i] == RRG_NOOP &&
|
|
authmem_form->member == grantorId &&
|
|
authmem_form->admin_option)
|
|
break;
|
|
}
|
|
if (i >= memlist->n_members)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
|
|
errmsg("admin option cannot be granted back to your own grantor")));
|
|
|
|
ReleaseSysCacheList(memlist);
|
|
}
|
|
|
|
/* Now perform the catalog updates. */
|
|
forboth(specitem, memberSpecs, iditem, memberIds)
|
|
{
|
|
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
|
|
Oid memberid = lfirst_oid(iditem);
|
|
HeapTuple authmem_tuple;
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_auth_members] = {0};
|
|
bool new_record_nulls[Natts_pg_auth_members] = {0};
|
|
bool new_record_repl[Natts_pg_auth_members] = {0};
|
|
|
|
/* Common initialization for possible insert or update */
|
|
new_record[Anum_pg_auth_members_roleid - 1] =
|
|
ObjectIdGetDatum(roleid);
|
|
new_record[Anum_pg_auth_members_member - 1] =
|
|
ObjectIdGetDatum(memberid);
|
|
new_record[Anum_pg_auth_members_grantor - 1] =
|
|
ObjectIdGetDatum(grantorId);
|
|
|
|
/* Find any existing tuple */
|
|
authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid),
|
|
ObjectIdGetDatum(memberid),
|
|
ObjectIdGetDatum(grantorId));
|
|
|
|
/*
|
|
* If we found a tuple, update it with new option values, unless
|
|
* there are no changes, in which case issue a WARNING.
|
|
*
|
|
* If we didn't find a tuple, just insert one.
|
|
*/
|
|
if (HeapTupleIsValid(authmem_tuple))
|
|
{
|
|
Form_pg_auth_members authmem_form;
|
|
bool at_least_one_change = false;
|
|
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
if ((popt->specified & GRANT_ROLE_SPECIFIED_ADMIN) != 0
|
|
&& authmem_form->admin_option != popt->admin)
|
|
{
|
|
new_record[Anum_pg_auth_members_admin_option - 1] =
|
|
BoolGetDatum(popt->admin);
|
|
new_record_repl[Anum_pg_auth_members_admin_option - 1] =
|
|
true;
|
|
at_least_one_change = true;
|
|
}
|
|
|
|
if ((popt->specified & GRANT_ROLE_SPECIFIED_INHERIT) != 0
|
|
&& authmem_form->inherit_option != popt->inherit)
|
|
{
|
|
new_record[Anum_pg_auth_members_inherit_option - 1] =
|
|
BoolGetDatum(popt->inherit);
|
|
new_record_repl[Anum_pg_auth_members_inherit_option - 1] =
|
|
true;
|
|
at_least_one_change = true;
|
|
}
|
|
|
|
if (!at_least_one_change)
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
|
|
get_rolespec_name(memberRole), rolename,
|
|
GetUserNameFromId(grantorId, false))));
|
|
ReleaseSysCache(authmem_tuple);
|
|
continue;
|
|
}
|
|
|
|
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
|
|
new_record,
|
|
new_record_nulls, new_record_repl);
|
|
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
|
|
|
|
ReleaseSysCache(authmem_tuple);
|
|
}
|
|
else
|
|
{
|
|
Oid objectId;
|
|
Oid *newmembers = palloc(sizeof(Oid));
|
|
|
|
/* Set admin option if user set it to true, otherwise not. */
|
|
new_record[Anum_pg_auth_members_admin_option - 1] =
|
|
BoolGetDatum(popt->admin);
|
|
|
|
/*
|
|
* If the user specified a value for the inherit option, use
|
|
* whatever was specified. Otherwise, set the default value based
|
|
* on the role-level property.
|
|
*/
|
|
if ((popt->specified & GRANT_ROLE_SPECIFIED_INHERIT) != 0)
|
|
new_record[Anum_pg_auth_members_inherit_option - 1] =
|
|
popt->inherit;
|
|
else
|
|
{
|
|
HeapTuple mrtup;
|
|
Form_pg_authid mrform;
|
|
|
|
mrtup = SearchSysCache1(AUTHOID, memberid);
|
|
if (!HeapTupleIsValid(mrtup))
|
|
elog(ERROR, "cache lookup failed for role %u", memberid);
|
|
mrform = (Form_pg_authid) GETSTRUCT(mrtup);
|
|
new_record[Anum_pg_auth_members_inherit_option - 1] =
|
|
mrform->rolinherit;
|
|
ReleaseSysCache(mrtup);
|
|
}
|
|
|
|
/* get an OID for the new row and insert it */
|
|
objectId = GetNewObjectId();
|
|
new_record[Anum_pg_auth_members_oid - 1] = objectId;
|
|
tuple = heap_form_tuple(pg_authmem_dsc,
|
|
new_record, new_record_nulls);
|
|
CatalogTupleInsert(pg_authmem_rel, tuple);
|
|
|
|
/* updateAclDependencies wants to pfree array inputs */
|
|
newmembers[0] = grantorId;
|
|
updateAclDependencies(AuthMemRelationId, objectId,
|
|
0, InvalidOid,
|
|
0, NULL,
|
|
1, newmembers);
|
|
}
|
|
|
|
/* CCI after each change, in case there are duplicates in list */
|
|
CommandCounterIncrement();
|
|
}
|
|
|
|
/*
|
|
* Close pg_authmem, but keep lock till commit.
|
|
*/
|
|
table_close(pg_authmem_rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* DelRoleMems -- Remove given members from the specified role
|
|
*
|
|
* rolename: name of role to del from (used only for error messages)
|
|
* roleid: OID of role to del from
|
|
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
|
|
* memberIds: OIDs of roles to del
|
|
* grantorId: who is revoking the membership
|
|
* popt: information about grant options
|
|
* behavior: RESTRICT or CASCADE behavior for recursive removal
|
|
*/
|
|
static void
|
|
DelRoleMems(const char *rolename, Oid roleid,
|
|
List *memberSpecs, List *memberIds,
|
|
Oid grantorId, GrantRoleOptions *popt, DropBehavior behavior)
|
|
{
|
|
Relation pg_authmem_rel;
|
|
TupleDesc pg_authmem_dsc;
|
|
ListCell *specitem;
|
|
ListCell *iditem;
|
|
Oid currentUserId = GetUserId();
|
|
CatCList *memlist;
|
|
RevokeRoleGrantAction *actions;
|
|
int i;
|
|
|
|
Assert(list_length(memberSpecs) == list_length(memberIds));
|
|
|
|
/* Skip permission check if nothing to do */
|
|
if (!memberIds)
|
|
return;
|
|
|
|
/*
|
|
* Check permissions: must have createrole or admin option on the role to
|
|
* be changed. To mess with a superuser role, you gotta be superuser.
|
|
*/
|
|
if (superuser_arg(roleid))
|
|
{
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to alter superusers")));
|
|
}
|
|
else
|
|
{
|
|
if (!have_createrole_privilege() &&
|
|
!is_admin_of_role(currentUserId, roleid))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must have admin option on role \"%s\"",
|
|
rolename)));
|
|
}
|
|
|
|
/* Validate grantor (and resolve implicit grantor if not specified). */
|
|
grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
|
|
|
|
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
|
|
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
|
|
|
|
/*
|
|
* Only allow changes to this role by one backend at a time, so that we
|
|
* can check for things like dependent privileges without fear of race
|
|
* conditions.
|
|
*/
|
|
LockSharedObject(AuthIdRelationId, roleid, 0,
|
|
ShareUpdateExclusiveLock);
|
|
|
|
memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
|
|
actions = initialize_revoke_actions(memlist);
|
|
|
|
/*
|
|
* We may need to recurse to dependent privileges if DROP_CASCADE was
|
|
* specified, or refuse to perform the operation if dependent privileges
|
|
* exist and DROP_RESTRICT was specified. plan_single_revoke() will figure
|
|
* out what to do with each catalog tuple.
|
|
*/
|
|
forboth(specitem, memberSpecs, iditem, memberIds)
|
|
{
|
|
RoleSpec *memberRole = lfirst(specitem);
|
|
Oid memberid = lfirst_oid(iditem);
|
|
|
|
if (!plan_single_revoke(memlist, actions, memberid, grantorId,
|
|
popt, behavior))
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
|
|
get_rolespec_name(memberRole), rolename,
|
|
GetUserNameFromId(grantorId, false))));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* We now know what to do with each catalog tuple: it should either be
|
|
* left alone, deleted, or just have the admin_option flag cleared.
|
|
* Perform the appropriate action in each case.
|
|
*/
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple authmem_tuple;
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
if (actions[i] == RRG_NOOP)
|
|
continue;
|
|
|
|
authmem_tuple = &memlist->members[i]->tuple;
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
if (actions[i] == RRG_DELETE_GRANT)
|
|
{
|
|
/*
|
|
* Remove the entry altogether, after first removing its
|
|
* dependencies
|
|
*/
|
|
deleteSharedDependencyRecordsFor(AuthMemRelationId,
|
|
authmem_form->oid, 0);
|
|
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
|
|
}
|
|
else
|
|
{
|
|
/* Just turn off the specified option */
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_auth_members] = {0};
|
|
bool new_record_nulls[Natts_pg_auth_members] = {0};
|
|
bool new_record_repl[Natts_pg_auth_members] = {0};
|
|
|
|
/* Build a tuple to update with */
|
|
if (actions[i] == RRG_REMOVE_ADMIN_OPTION)
|
|
{
|
|
new_record[Anum_pg_auth_members_admin_option - 1] =
|
|
BoolGetDatum(false);
|
|
new_record_repl[Anum_pg_auth_members_admin_option - 1] =
|
|
true;
|
|
}
|
|
else if (actions[i] == RRG_REMOVE_INHERIT_OPTION)
|
|
{
|
|
new_record[Anum_pg_auth_members_inherit_option - 1] =
|
|
BoolGetDatum(false);
|
|
new_record_repl[Anum_pg_auth_members_inherit_option - 1] =
|
|
true;
|
|
}
|
|
else
|
|
elog(ERROR, "unknown role revoke action");
|
|
|
|
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
|
|
new_record,
|
|
new_record_nulls, new_record_repl);
|
|
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
|
|
}
|
|
}
|
|
|
|
ReleaseSysCacheList(memlist);
|
|
|
|
/*
|
|
* Close pg_authmem, but keep lock till commit.
|
|
*/
|
|
table_close(pg_authmem_rel, NoLock);
|
|
}
|
|
|
|
/*
|
|
* Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
|
|
* targeting a role.
|
|
*
|
|
* The grantor must always be either a role with ADMIN OPTION on the role in
|
|
* which membership is being granted, or the bootstrap superuser. This is
|
|
* similar to the restriction enforced by select_best_grantor, except that
|
|
* roles don't have owners, so we regard the bootstrap superuser as the
|
|
* implicit owner.
|
|
*
|
|
* If the grantor was not explicitly specified by the user, grantorId should
|
|
* be passed as InvalidOid, and this function will infer the user to be
|
|
* recorded as the grantor. In many cases, this will be the current user, but
|
|
* things get more complicated when the current user doesn't possess ADMIN
|
|
* OPTION on the role but rather relies on having CREATEROLE privileges, or
|
|
* on inheriting the privileges of a role which does have ADMIN OPTION. See
|
|
* below for details.
|
|
*
|
|
* If the grantor was specified by the user, then it must be a user that
|
|
* can legally be recorded as the grantor, as per the rule stated above.
|
|
* This is an integrity constraint, not a permissions check, and thus even
|
|
* superusers are subject to this restriction. However, there is also a
|
|
* permissions check: to specify a role as the grantor, the current user
|
|
* must possess the privileges of that role. Superusers will always pass
|
|
* this check, but for non-superusers it may lead to an error.
|
|
*
|
|
* The return value is the OID to be regarded as the grantor when executing
|
|
* the operation.
|
|
*/
|
|
static Oid
|
|
check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
|
|
{
|
|
/* If the grantor ID was not specified, pick one to use. */
|
|
if (!OidIsValid(grantorId))
|
|
{
|
|
/*
|
|
* Grants where the grantor is recorded as the bootstrap superuser do
|
|
* not depend on any other existing grants, so always default to this
|
|
* interpretation when possible.
|
|
*/
|
|
if (has_createrole_privilege(currentUserId))
|
|
return BOOTSTRAP_SUPERUSERID;
|
|
|
|
/*
|
|
* Otherwise, the grantor must either have ADMIN OPTION on the role or
|
|
* inherit the privileges of a role which does. In the former case,
|
|
* record the grantor as the current user; in the latter, pick one of
|
|
* the roles that is "most directly" inherited by the current role
|
|
* (i.e. fewest "hops").
|
|
*
|
|
* (We shouldn't fail to find a best grantor, because we've already
|
|
* established that the current user has permission to perform the
|
|
* operation.)
|
|
*/
|
|
grantorId = select_best_admin(currentUserId, roleid);
|
|
if (!OidIsValid(grantorId))
|
|
elog(ERROR, "no possible grantors");
|
|
return grantorId;
|
|
}
|
|
|
|
/*
|
|
* If an explicit grantor is specified, it must be a role whose privileges
|
|
* the current user possesses.
|
|
*
|
|
* It should also be a role that has ADMIN OPTION on the target role, but
|
|
* we check this condition only in case of GRANT. For REVOKE, no matching
|
|
* grant should exist anyway, but if it somehow does, let the user get rid
|
|
* of it.
|
|
*/
|
|
if (is_grant)
|
|
{
|
|
if (!has_privs_of_role(currentUserId, grantorId))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to grant privileges as role \"%s\"",
|
|
GetUserNameFromId(grantorId, false))));
|
|
|
|
if (grantorId != BOOTSTRAP_SUPERUSERID &&
|
|
select_best_admin(grantorId, roleid) != grantorId)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("grantor must have ADMIN OPTION on \"%s\"",
|
|
GetUserNameFromId(roleid, false))));
|
|
}
|
|
else
|
|
{
|
|
if (!has_privs_of_role(currentUserId, grantorId))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied to revoke privileges granted by role \"%s\"",
|
|
GetUserNameFromId(grantorId, false))));
|
|
}
|
|
|
|
/*
|
|
* If a grantor was specified explicitly, always attribute the grant to
|
|
* that role (unless we error out above).
|
|
*/
|
|
return grantorId;
|
|
}
|
|
|
|
/*
|
|
* Initialize an array of RevokeRoleGrantAction objects.
|
|
*
|
|
* 'memlist' should be a list of all grants for the target role.
|
|
*
|
|
* This constructs an array indicating that no actions are to be performed;
|
|
* that is, every element is initially RRG_NOOP.
|
|
*/
|
|
static RevokeRoleGrantAction *
|
|
initialize_revoke_actions(CatCList *memlist)
|
|
{
|
|
RevokeRoleGrantAction *result;
|
|
int i;
|
|
|
|
if (memlist->n_members == 0)
|
|
return NULL;
|
|
|
|
result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
|
|
for (i = 0; i < memlist->n_members; i++)
|
|
result[i] = RRG_NOOP;
|
|
return result;
|
|
}
|
|
|
|
/*
|
|
* Figure out what we would need to do in order to revoke a grant, or just the
|
|
* admin option on a grant, given that there might be dependent privileges.
|
|
*
|
|
* 'memlist' should be a list of all grants for the target role.
|
|
*
|
|
* Whatever actions prove to be necessary will be signalled by updating
|
|
* 'actions'.
|
|
*
|
|
* If behavior is DROP_RESTRICT, an error will occur if there are dependent
|
|
* role membership grants; if DROP_CASCADE, those grants will be scheduled
|
|
* for deletion.
|
|
*
|
|
* The return value is true if the matching grant was found in the list,
|
|
* and false if not.
|
|
*/
|
|
static bool
|
|
plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
|
|
Oid member, Oid grantor, GrantRoleOptions *popt,
|
|
DropBehavior behavior)
|
|
{
|
|
int i;
|
|
|
|
/*
|
|
* If popt.specified == 0, we're revoking the grant entirely; otherwise,
|
|
* we expect just one bit to be set, and we're revoking the corresponding
|
|
* option. As of this writing, there's no syntax that would allow for
|
|
* an attempt to revoke multiple options at once, and the logic below
|
|
* wouldn't work properly if such syntax were added, so assert that our
|
|
* caller isn't trying to do that.
|
|
*/
|
|
Assert(pg_popcount32(popt->specified) <= 1);
|
|
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple authmem_tuple;
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
authmem_tuple = &memlist->members[i]->tuple;
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
if (authmem_form->member == member &&
|
|
authmem_form->grantor == grantor)
|
|
{
|
|
if ((popt->specified & GRANT_ROLE_SPECIFIED_INHERIT) != 0)
|
|
{
|
|
/*
|
|
* Revoking the INHERIT option doesn't change anything for
|
|
* dependent privileges, so we don't need to recurse.
|
|
*/
|
|
actions[i] = RRG_REMOVE_INHERIT_OPTION;
|
|
}
|
|
else
|
|
{
|
|
bool revoke_admin_option_only;
|
|
|
|
/*
|
|
* Revoking the grant entirely, or ADMIN option on a grant,
|
|
* implicates dependent privileges, so we may need to recurse.
|
|
*/
|
|
revoke_admin_option_only =
|
|
(popt->specified & GRANT_ROLE_SPECIFIED_ADMIN) != 0;
|
|
plan_recursive_revoke(memlist, actions, i,
|
|
revoke_admin_option_only, behavior);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Figure out what we would need to do in order to revoke all grants to
|
|
* a given member, given that there might be dependent privileges.
|
|
*
|
|
* 'memlist' should be a list of all grants for the target role.
|
|
*
|
|
* Whatever actions prove to be necessary will be signalled by updating
|
|
* 'actions'.
|
|
*/
|
|
static void
|
|
plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
|
|
Oid member)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple authmem_tuple;
|
|
Form_pg_auth_members authmem_form;
|
|
|
|
authmem_tuple = &memlist->members[i]->tuple;
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
if (authmem_form->member == member)
|
|
plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Workhorse for figuring out recursive revocation of role grants.
|
|
*
|
|
* This is similar to what recursive_revoke() does for ACLs.
|
|
*/
|
|
static void
|
|
plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
|
|
int index,
|
|
bool revoke_admin_option_only, DropBehavior behavior)
|
|
{
|
|
bool would_still_have_admin_option = false;
|
|
HeapTuple authmem_tuple;
|
|
Form_pg_auth_members authmem_form;
|
|
int i;
|
|
|
|
/* If it's already been done, we can just return. */
|
|
if (actions[index] == RRG_DELETE_GRANT)
|
|
return;
|
|
if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
|
|
revoke_admin_option_only)
|
|
return;
|
|
|
|
/* Locate tuple data. */
|
|
authmem_tuple = &memlist->members[index]->tuple;
|
|
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
|
|
|
|
/*
|
|
* If the existing tuple does not have admin_option set, then we do not
|
|
* need to recurse. If we're just supposed to clear that bit we don't need
|
|
* to do anything at all; if we're supposed to remove the grant, we need
|
|
* to do something, but only to the tuple, and not any others.
|
|
*/
|
|
if (!revoke_admin_option_only)
|
|
{
|
|
actions[index] = RRG_DELETE_GRANT;
|
|
if (!authmem_form->admin_option)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (!authmem_form->admin_option)
|
|
return;
|
|
actions[index] = RRG_REMOVE_ADMIN_OPTION;
|
|
}
|
|
|
|
/* Determine whether the member would still have ADMIN OPTION. */
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple am_cascade_tuple;
|
|
Form_pg_auth_members am_cascade_form;
|
|
|
|
am_cascade_tuple = &memlist->members[i]->tuple;
|
|
am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
|
|
|
|
if (am_cascade_form->member == authmem_form->member &&
|
|
am_cascade_form->admin_option && actions[i] == RRG_NOOP)
|
|
{
|
|
would_still_have_admin_option = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* If the member would still have ADMIN OPTION, we need not recurse. */
|
|
if (would_still_have_admin_option)
|
|
return;
|
|
|
|
/*
|
|
* Recurse to grants that are not yet slated for deletion which have this
|
|
* member as the grantor.
|
|
*/
|
|
for (i = 0; i < memlist->n_members; ++i)
|
|
{
|
|
HeapTuple am_cascade_tuple;
|
|
Form_pg_auth_members am_cascade_form;
|
|
|
|
am_cascade_tuple = &memlist->members[i]->tuple;
|
|
am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
|
|
|
|
if (am_cascade_form->grantor == authmem_form->member &&
|
|
actions[i] != RRG_DELETE_GRANT)
|
|
{
|
|
if (behavior == DROP_RESTRICT)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
|
|
errmsg("dependent privileges exist"),
|
|
errhint("Use CASCADE to revoke them too.")));
|
|
|
|
plan_recursive_revoke(memlist, actions, i, false, behavior);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Initialize a GrantRoleOptions object with default values.
|
|
*/
|
|
static void
|
|
InitGrantRoleOptions(GrantRoleOptions *popt)
|
|
{
|
|
popt->specified = 0;
|
|
popt->admin = false;
|
|
popt->inherit = false;
|
|
}
|