mirror of
https://github.com/postgres/postgres.git
synced 2025-04-24 10:47:04 +03:00
In particular, make hba.c cope with zero-length tokens, which it never did properly before. Also, enforce rolcanlogin.
1189 lines
33 KiB
C
1189 lines
33 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* user.c
|
|
* Commands for manipulating roles (formerly called users).
|
|
*
|
|
* Portions Copyright (c) 1996-2005, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* $PostgreSQL: pgsql/src/backend/commands/user.c,v 1.154 2005/06/28 22:16:44 tgl Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "access/heapam.h"
|
|
#include "catalog/indexing.h"
|
|
#include "catalog/pg_auth_members.h"
|
|
#include "catalog/pg_authid.h"
|
|
#include "catalog/pg_database.h"
|
|
#include "commands/user.h"
|
|
#include "libpq/crypt.h"
|
|
#include "miscadmin.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/catcache.h"
|
|
#include "utils/flatfiles.h"
|
|
#include "utils/fmgroids.h"
|
|
#include "utils/guc.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/syscache.h"
|
|
|
|
|
|
extern bool Password_encryption;
|
|
|
|
static List *roleNamesToIds(List *memberNames);
|
|
static void AddRoleMems(const char *rolename, Oid roleid,
|
|
List *memberNames, List *memberIds,
|
|
Oid grantorId, bool admin_opt);
|
|
static void DelRoleMems(const char *rolename, Oid roleid,
|
|
List *memberNames, List *memberIds,
|
|
bool admin_opt);
|
|
|
|
|
|
/*
|
|
* CREATE ROLE
|
|
*/
|
|
void
|
|
CreateRole(CreateRoleStmt *stmt)
|
|
{
|
|
Relation pg_authid_rel;
|
|
TupleDesc pg_authid_dsc;
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_authid];
|
|
char new_record_nulls[Natts_pg_authid];
|
|
Oid roleid;
|
|
ListCell *item;
|
|
ListCell *option;
|
|
char *password = NULL; /* user password */
|
|
bool encrypt_password = Password_encryption; /* encrypt password? */
|
|
char encrypted_password[MD5_PASSWD_LEN + 1];
|
|
bool issuper = false; /* Make the user a superuser? */
|
|
bool createrole = false; /* Can this user create roles? */
|
|
bool createdb = false; /* Can the user create databases? */
|
|
bool canlogin = false; /* Can this user login? */
|
|
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 */
|
|
DefElem *dpassword = NULL;
|
|
DefElem *dcreatedb = NULL;
|
|
DefElem *dcreaterole = NULL;
|
|
DefElem *dcanlogin = NULL;
|
|
DefElem *daddroleto = NULL;
|
|
DefElem *drolemembers = NULL;
|
|
DefElem *dadminmembers = NULL;
|
|
DefElem *dvalidUntil = NULL;
|
|
|
|
/* Extract options from the statement node tree */
|
|
foreach(option, stmt->options)
|
|
{
|
|
DefElem *defel = (DefElem *) lfirst(option);
|
|
|
|
if (strcmp(defel->defname, "password") == 0 ||
|
|
strcmp(defel->defname, "encryptedPassword") == 0 ||
|
|
strcmp(defel->defname, "unencryptedPassword") == 0)
|
|
{
|
|
if (dpassword)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dpassword = defel;
|
|
if (strcmp(defel->defname, "encryptedPassword") == 0)
|
|
encrypt_password = true;
|
|
else if (strcmp(defel->defname, "unencryptedPassword") == 0)
|
|
encrypt_password = false;
|
|
}
|
|
else if (strcmp(defel->defname, "sysid") == 0)
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg("SYSID can no longer be specified")));
|
|
}
|
|
else if (strcmp(defel->defname, "createrole") == 0)
|
|
{
|
|
if (dcreaterole)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcreaterole = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createdb") == 0)
|
|
{
|
|
if (dcreatedb)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcreatedb = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "canlogin") == 0)
|
|
{
|
|
if (dcanlogin)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcanlogin = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "addroleto") == 0)
|
|
{
|
|
if (daddroleto)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
daddroleto = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "rolemembers") == 0)
|
|
{
|
|
if (drolemembers)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
drolemembers = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "adminmembers") == 0)
|
|
{
|
|
if (dadminmembers)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dadminmembers = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "validUntil") == 0)
|
|
{
|
|
if (dvalidUntil)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dvalidUntil = defel;
|
|
}
|
|
else
|
|
elog(ERROR, "option \"%s\" not recognized",
|
|
defel->defname);
|
|
}
|
|
|
|
if (dcreatedb)
|
|
createdb = intVal(dcreatedb->arg) != 0;
|
|
if (dcreaterole)
|
|
{
|
|
createrole = intVal(dcreaterole->arg) != 0;
|
|
/* XXX issuper is implied by createrole for now */
|
|
issuper = createrole;
|
|
}
|
|
if (dcanlogin)
|
|
canlogin = intVal(dcanlogin->arg) != 0;
|
|
if (dvalidUntil)
|
|
validUntil = strVal(dvalidUntil->arg);
|
|
if (dpassword)
|
|
password = strVal(dpassword->arg);
|
|
if (daddroleto)
|
|
addroleto = (List *) daddroleto->arg;
|
|
if (drolemembers)
|
|
rolemembers = (List *) drolemembers->arg;
|
|
if (dadminmembers)
|
|
adminmembers = (List *) dadminmembers->arg;
|
|
|
|
/* Check some permissions first */
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to create roles")));
|
|
|
|
if (strcmp(stmt->role, "public") == 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_RESERVED_NAME),
|
|
errmsg("role name \"%s\" is reserved",
|
|
stmt->role)));
|
|
|
|
/*
|
|
* Check the pg_authid relation to be certain the role doesn't
|
|
* already exist. Note we secure exclusive lock because
|
|
* we need to protect our eventual update of the flat auth file.
|
|
*/
|
|
pg_authid_rel = heap_open(AuthIdRelationId, ExclusiveLock);
|
|
pg_authid_dsc = RelationGetDescr(pg_authid_rel);
|
|
|
|
tuple = SearchSysCache(AUTHNAME,
|
|
PointerGetDatum(stmt->role),
|
|
0, 0, 0);
|
|
if (HeapTupleIsValid(tuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("role \"%s\" already exists",
|
|
stmt->role)));
|
|
|
|
/*
|
|
* Build a tuple to insert
|
|
*/
|
|
MemSet(new_record, 0, sizeof(new_record));
|
|
MemSet(new_record_nulls, ' ', sizeof(new_record_nulls));
|
|
|
|
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_rolcreaterole - 1] = BoolGetDatum(createrole);
|
|
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb);
|
|
/* superuser gets catupdate right by default */
|
|
new_record[Anum_pg_authid_rolcatupdate - 1] = BoolGetDatum(issuper);
|
|
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin);
|
|
|
|
if (password)
|
|
{
|
|
if (!encrypt_password || isMD5(password))
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
DirectFunctionCall1(textin, CStringGetDatum(password));
|
|
else
|
|
{
|
|
if (!EncryptMD5(password, stmt->role, strlen(stmt->role),
|
|
encrypted_password))
|
|
elog(ERROR, "password encryption failed");
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
DirectFunctionCall1(textin, CStringGetDatum(encrypted_password));
|
|
}
|
|
}
|
|
else
|
|
new_record_nulls[Anum_pg_authid_rolpassword - 1] = 'n';
|
|
|
|
if (validUntil)
|
|
new_record[Anum_pg_authid_rolvaliduntil - 1] =
|
|
DirectFunctionCall3(timestamptz_in,
|
|
CStringGetDatum(validUntil),
|
|
ObjectIdGetDatum(InvalidOid),
|
|
Int32GetDatum(-1));
|
|
|
|
else
|
|
new_record_nulls[Anum_pg_authid_rolvaliduntil - 1] = 'n';
|
|
|
|
new_record_nulls[Anum_pg_authid_rolconfig - 1] = 'n';
|
|
|
|
tuple = heap_formtuple(pg_authid_dsc, new_record, new_record_nulls);
|
|
|
|
/*
|
|
* Insert new record in the pg_authid table
|
|
*/
|
|
roleid = simple_heap_insert(pg_authid_rel, tuple);
|
|
Assert(OidIsValid(roleid));
|
|
|
|
/* Update indexes */
|
|
CatalogUpdateIndexes(pg_authid_rel, tuple);
|
|
|
|
/*
|
|
* Add the new role to the specified existing roles.
|
|
*/
|
|
foreach(item, addroleto)
|
|
{
|
|
char *oldrolename = strVal(lfirst(item));
|
|
Oid oldroleid = get_roleid_checked(oldrolename);
|
|
|
|
AddRoleMems(oldrolename, oldroleid,
|
|
list_make1(makeString(stmt->role)),
|
|
list_make1_oid(roleid),
|
|
GetUserId(), false);
|
|
}
|
|
|
|
/*
|
|
* Add the specified members to this new role. adminmembers get the
|
|
* admin option, rolemembers don't.
|
|
*/
|
|
AddRoleMems(stmt->role, roleid,
|
|
adminmembers, roleNamesToIds(adminmembers),
|
|
GetUserId(), true);
|
|
AddRoleMems(stmt->role, roleid,
|
|
rolemembers, roleNamesToIds(rolemembers),
|
|
GetUserId(), false);
|
|
|
|
/*
|
|
* Now we can clean up; but keep lock until commit (to avoid possible
|
|
* deadlock when commit code tries to acquire lock).
|
|
*/
|
|
heap_close(pg_authid_rel, NoLock);
|
|
|
|
/*
|
|
* Set flag to update flat auth file at commit.
|
|
*/
|
|
auth_file_update_needed();
|
|
}
|
|
|
|
|
|
/*
|
|
* ALTER ROLE
|
|
*/
|
|
void
|
|
AlterRole(AlterRoleStmt *stmt)
|
|
{
|
|
Datum new_record[Natts_pg_authid];
|
|
char new_record_nulls[Natts_pg_authid];
|
|
char new_record_repl[Natts_pg_authid];
|
|
Relation pg_authid_rel;
|
|
TupleDesc pg_authid_dsc;
|
|
HeapTuple tuple,
|
|
new_tuple;
|
|
ListCell *option;
|
|
char *password = NULL; /* user password */
|
|
bool encrypt_password = Password_encryption; /* encrypt password? */
|
|
char encrypted_password[MD5_PASSWD_LEN + 1];
|
|
int issuper = -1; /* Make the user a superuser? */
|
|
int createrole = -1; /* Can this user create roles? */
|
|
int createdb = -1; /* Can the user create databases? */
|
|
int canlogin = -1; /* Can this user login? */
|
|
List *rolemembers = NIL; /* roles to be added/removed */
|
|
char *validUntil = NULL; /* time the login is valid until */
|
|
DefElem *dpassword = NULL;
|
|
DefElem *dcreatedb = NULL;
|
|
DefElem *dcreaterole = NULL;
|
|
DefElem *dcanlogin = NULL;
|
|
DefElem *dvalidUntil = NULL;
|
|
DefElem *drolemembers = NULL;
|
|
Oid roleid;
|
|
|
|
/* Extract options from the statement node tree */
|
|
foreach(option, stmt->options)
|
|
{
|
|
DefElem *defel = (DefElem *) lfirst(option);
|
|
|
|
if (strcmp(defel->defname, "password") == 0 ||
|
|
strcmp(defel->defname, "encryptedPassword") == 0 ||
|
|
strcmp(defel->defname, "unencryptedPassword") == 0)
|
|
{
|
|
if (dpassword)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dpassword = defel;
|
|
if (strcmp(defel->defname, "encryptedPassword") == 0)
|
|
encrypt_password = true;
|
|
else if (strcmp(defel->defname, "unencryptedPassword") == 0)
|
|
encrypt_password = false;
|
|
}
|
|
else if (strcmp(defel->defname, "createdb") == 0)
|
|
{
|
|
if (dcreatedb)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcreatedb = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "createrole") == 0)
|
|
{
|
|
if (dcreaterole)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcreaterole = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "canlogin") == 0)
|
|
{
|
|
if (dcanlogin)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dcanlogin = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "validUntil") == 0)
|
|
{
|
|
if (dvalidUntil)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
dvalidUntil = defel;
|
|
}
|
|
else if (strcmp(defel->defname, "rolemembers") == 0 &&
|
|
stmt->action != 0)
|
|
{
|
|
if (drolemembers)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_SYNTAX_ERROR),
|
|
errmsg("conflicting or redundant options")));
|
|
drolemembers = defel;
|
|
}
|
|
else
|
|
elog(ERROR, "option \"%s\" not recognized",
|
|
defel->defname);
|
|
}
|
|
|
|
if (dcreatedb)
|
|
createdb = intVal(dcreatedb->arg);
|
|
if (dcreaterole)
|
|
{
|
|
createrole = intVal(dcreaterole->arg);
|
|
/* XXX createrole implies issuper for now */
|
|
issuper = createrole;
|
|
}
|
|
if (dcanlogin)
|
|
canlogin = intVal(dcanlogin->arg);
|
|
if (dvalidUntil)
|
|
validUntil = strVal(dvalidUntil->arg);
|
|
if (dpassword)
|
|
password = strVal(dpassword->arg);
|
|
if (drolemembers)
|
|
rolemembers = (List *) drolemembers->arg;
|
|
|
|
/* must be superuser or just want to change your own password */
|
|
if (!superuser() &&
|
|
!(issuper < 0 &&
|
|
createrole < 0 &&
|
|
createdb < 0 &&
|
|
canlogin < 0 &&
|
|
!validUntil &&
|
|
!rolemembers &&
|
|
password &&
|
|
strcmp(GetUserNameFromId(GetUserId()), stmt->role) == 0))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied")));
|
|
|
|
/*
|
|
* Scan the pg_authid relation to be certain the user exists. Note we
|
|
* secure exclusive lock to protect our update of the flat auth file.
|
|
*/
|
|
pg_authid_rel = heap_open(AuthIdRelationId, ExclusiveLock);
|
|
pg_authid_dsc = RelationGetDescr(pg_authid_rel);
|
|
|
|
tuple = SearchSysCache(AUTHNAME,
|
|
PointerGetDatum(stmt->role),
|
|
0, 0, 0);
|
|
if (!HeapTupleIsValid(tuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("role \"%s\" does not exist", stmt->role)));
|
|
|
|
roleid = HeapTupleGetOid(tuple);
|
|
|
|
/*
|
|
* Build an updated tuple, perusing the information just obtained
|
|
*/
|
|
MemSet(new_record, 0, sizeof(new_record));
|
|
MemSet(new_record_nulls, ' ', sizeof(new_record_nulls));
|
|
MemSet(new_record_repl, ' ', sizeof(new_record_repl));
|
|
|
|
new_record[Anum_pg_authid_rolname - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(stmt->role));
|
|
new_record_repl[Anum_pg_authid_rolname - 1] = 'r';
|
|
|
|
/*
|
|
* issuper/createrole/catupdate/etc
|
|
*
|
|
* XXX It's rather unclear how to handle catupdate. It's probably best to
|
|
* keep it equal to the superuser status, otherwise you could end up
|
|
* with a situation where no existing superuser can alter the
|
|
* catalogs, including pg_authid!
|
|
*/
|
|
if (issuper >= 0)
|
|
{
|
|
new_record[Anum_pg_authid_rolsuper - 1] = BoolGetDatum(issuper > 0);
|
|
new_record_repl[Anum_pg_authid_rolsuper - 1] = 'r';
|
|
|
|
new_record[Anum_pg_authid_rolcatupdate - 1] = BoolGetDatum(issuper > 0);
|
|
new_record_repl[Anum_pg_authid_rolcatupdate - 1] = 'r';
|
|
}
|
|
|
|
if (createrole >= 0)
|
|
{
|
|
new_record[Anum_pg_authid_rolcreaterole - 1] = BoolGetDatum(createrole > 0);
|
|
new_record_repl[Anum_pg_authid_rolcreaterole - 1] = 'r';
|
|
}
|
|
|
|
if (createdb >= 0)
|
|
{
|
|
new_record[Anum_pg_authid_rolcreatedb - 1] = BoolGetDatum(createdb > 0);
|
|
new_record_repl[Anum_pg_authid_rolcreatedb - 1] = 'r';
|
|
}
|
|
|
|
if (canlogin >= 0)
|
|
{
|
|
new_record[Anum_pg_authid_rolcanlogin - 1] = BoolGetDatum(canlogin > 0);
|
|
new_record_repl[Anum_pg_authid_rolcanlogin - 1] = 'r';
|
|
}
|
|
|
|
/* password */
|
|
if (password)
|
|
{
|
|
if (!encrypt_password || isMD5(password))
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
DirectFunctionCall1(textin, CStringGetDatum(password));
|
|
else
|
|
{
|
|
if (!EncryptMD5(password, stmt->role, strlen(stmt->role),
|
|
encrypted_password))
|
|
elog(ERROR, "password encryption failed");
|
|
new_record[Anum_pg_authid_rolpassword - 1] =
|
|
DirectFunctionCall1(textin, CStringGetDatum(encrypted_password));
|
|
}
|
|
new_record_repl[Anum_pg_authid_rolpassword - 1] = 'r';
|
|
}
|
|
|
|
/* valid until */
|
|
if (validUntil)
|
|
{
|
|
new_record[Anum_pg_authid_rolvaliduntil - 1] =
|
|
DirectFunctionCall3(timestamptz_in,
|
|
CStringGetDatum(validUntil),
|
|
ObjectIdGetDatum(InvalidOid),
|
|
Int32GetDatum(-1));
|
|
new_record_repl[Anum_pg_authid_rolvaliduntil - 1] = 'r';
|
|
}
|
|
|
|
new_tuple = heap_modifytuple(tuple, pg_authid_dsc, new_record,
|
|
new_record_nulls, new_record_repl);
|
|
simple_heap_update(pg_authid_rel, &tuple->t_self, new_tuple);
|
|
|
|
/* Update indexes */
|
|
CatalogUpdateIndexes(pg_authid_rel, new_tuple);
|
|
|
|
ReleaseSysCache(tuple);
|
|
heap_freetuple(new_tuple);
|
|
|
|
/*
|
|
* Now we can clean up; but keep lock until commit (to avoid possible
|
|
* deadlock when commit code tries to acquire lock).
|
|
*/
|
|
heap_close(pg_authid_rel, NoLock);
|
|
|
|
if (stmt->action == +1) /* add members to role */
|
|
AddRoleMems(stmt->role, roleid,
|
|
rolemembers, roleNamesToIds(rolemembers),
|
|
GetUserId(), false);
|
|
else if (stmt->action == -1) /* drop members from role */
|
|
DelRoleMems(stmt->role, roleid,
|
|
rolemembers, roleNamesToIds(rolemembers),
|
|
false);
|
|
|
|
/*
|
|
* Set flag to update flat auth file at commit.
|
|
*/
|
|
auth_file_update_needed();
|
|
}
|
|
|
|
|
|
/*
|
|
* ALTER ROLE ... SET
|
|
*/
|
|
void
|
|
AlterRoleSet(AlterRoleSetStmt *stmt)
|
|
{
|
|
char *valuestr;
|
|
HeapTuple oldtuple,
|
|
newtuple;
|
|
Relation rel;
|
|
Datum repl_val[Natts_pg_authid];
|
|
char repl_null[Natts_pg_authid];
|
|
char repl_repl[Natts_pg_authid];
|
|
int i;
|
|
|
|
valuestr = flatten_set_variable_args(stmt->variable, stmt->value);
|
|
|
|
/*
|
|
* RowExclusiveLock is sufficient, because we don't need to update the
|
|
* flat auth file.
|
|
*/
|
|
rel = heap_open(AuthIdRelationId, RowExclusiveLock);
|
|
oldtuple = SearchSysCache(AUTHNAME,
|
|
PointerGetDatum(stmt->role),
|
|
0, 0, 0);
|
|
if (!HeapTupleIsValid(oldtuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("role \"%s\" does not exist", stmt->role)));
|
|
|
|
if (!(superuser() ||
|
|
(HeapTupleGetOid(oldtuple) == GetUserId())))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("permission denied")));
|
|
|
|
for (i = 0; i < Natts_pg_authid; i++)
|
|
repl_repl[i] = ' ';
|
|
|
|
repl_repl[Anum_pg_authid_rolconfig - 1] = 'r';
|
|
if (strcmp(stmt->variable, "all") == 0 && valuestr == NULL)
|
|
{
|
|
/* RESET ALL */
|
|
repl_null[Anum_pg_authid_rolconfig - 1] = 'n';
|
|
}
|
|
else
|
|
{
|
|
Datum datum;
|
|
bool isnull;
|
|
ArrayType *array;
|
|
|
|
repl_null[Anum_pg_authid_rolconfig - 1] = ' ';
|
|
|
|
datum = SysCacheGetAttr(AUTHNAME, oldtuple,
|
|
Anum_pg_authid_rolconfig, &isnull);
|
|
|
|
array = isnull ? NULL : DatumGetArrayTypeP(datum);
|
|
|
|
if (valuestr)
|
|
array = GUCArrayAdd(array, stmt->variable, valuestr);
|
|
else
|
|
array = GUCArrayDelete(array, stmt->variable);
|
|
|
|
if (array)
|
|
repl_val[Anum_pg_authid_rolconfig - 1] = PointerGetDatum(array);
|
|
else
|
|
repl_null[Anum_pg_authid_rolconfig - 1] = 'n';
|
|
}
|
|
|
|
newtuple = heap_modifytuple(oldtuple, RelationGetDescr(rel), repl_val, repl_null, repl_repl);
|
|
simple_heap_update(rel, &oldtuple->t_self, newtuple);
|
|
|
|
CatalogUpdateIndexes(rel, newtuple);
|
|
|
|
ReleaseSysCache(oldtuple);
|
|
heap_close(rel, RowExclusiveLock);
|
|
}
|
|
|
|
|
|
/*
|
|
* DROP ROLE
|
|
*/
|
|
void
|
|
DropRole(DropRoleStmt *stmt)
|
|
{
|
|
Relation pg_authid_rel, pg_auth_members_rel;
|
|
ListCell *item;
|
|
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to drop roles")));
|
|
|
|
/*
|
|
* Scan the pg_authid relation to find the Oid of the role to be
|
|
* deleted. Note we secure exclusive lock, because we need to protect
|
|
* our update of the flat auth file.
|
|
*/
|
|
pg_authid_rel = heap_open(AuthIdRelationId, ExclusiveLock);
|
|
pg_auth_members_rel = heap_open(AuthMemRelationId, ExclusiveLock);
|
|
|
|
foreach(item, stmt->roles)
|
|
{
|
|
const char *role = strVal(lfirst(item));
|
|
HeapTuple tuple,
|
|
tmp_tuple;
|
|
Relation pg_rel;
|
|
TupleDesc pg_dsc;
|
|
ScanKeyData scankey;
|
|
HeapScanDesc scan;
|
|
CatCList *auth_mem_list;
|
|
Oid roleid;
|
|
int i;
|
|
|
|
tuple = SearchSysCache(AUTHNAME,
|
|
PointerGetDatum(role),
|
|
0, 0, 0);
|
|
if (!HeapTupleIsValid(tuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_UNDEFINED_OBJECT),
|
|
errmsg("role \"%s\" does not exist", role)));
|
|
|
|
roleid = HeapTupleGetOid(tuple);
|
|
|
|
if (roleid == GetUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("current role cannot be dropped")));
|
|
if (roleid == GetSessionUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("session role cannot be dropped")));
|
|
|
|
/*
|
|
* Check if role still owns a database. If so, error out.
|
|
*
|
|
* (It used to be that this function would drop the database
|
|
* automatically. This is not only very dangerous for people that
|
|
* don't read the manual, it doesn't seem to be the behaviour one
|
|
* would expect either.) -- petere 2000/01/14)
|
|
*/
|
|
pg_rel = heap_open(DatabaseRelationId, AccessShareLock);
|
|
pg_dsc = RelationGetDescr(pg_rel);
|
|
|
|
ScanKeyInit(&scankey,
|
|
Anum_pg_database_datdba,
|
|
BTEqualStrategyNumber, F_OIDEQ,
|
|
roleid);
|
|
|
|
scan = heap_beginscan(pg_rel, SnapshotNow, 1, &scankey);
|
|
|
|
if ((tmp_tuple = heap_getnext(scan, ForwardScanDirection)) != NULL)
|
|
{
|
|
char *dbname;
|
|
|
|
dbname = NameStr(((Form_pg_database) GETSTRUCT(tmp_tuple))->datname);
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_OBJECT_IN_USE),
|
|
errmsg("role \"%s\" cannot be dropped", role),
|
|
errdetail("The role owns database \"%s\".", dbname)));
|
|
}
|
|
|
|
heap_endscan(scan);
|
|
heap_close(pg_rel, AccessShareLock);
|
|
|
|
/*
|
|
* Somehow we'd have to check for tables, views, etc. owned by the
|
|
* role as well, but those could be spread out over all sorts of
|
|
* databases which we don't have access to (easily).
|
|
*/
|
|
|
|
/*
|
|
* Remove the role from the pg_authid table
|
|
*/
|
|
simple_heap_delete(pg_authid_rel, &tuple->t_self);
|
|
|
|
ReleaseSysCache(tuple);
|
|
|
|
/*
|
|
* Remove role from roles
|
|
*
|
|
* scan pg_auth_members and remove tuples which have
|
|
* roleid == member or roleid == role
|
|
*/
|
|
auth_mem_list = SearchSysCacheList(AUTHMEMROLEMEM, 1,
|
|
ObjectIdGetDatum(roleid),
|
|
0, 0, 0);
|
|
|
|
for (i = 0; i < auth_mem_list->n_members; i++)
|
|
{
|
|
HeapTuple authmemtup = &auth_mem_list->members[i]->tuple;
|
|
simple_heap_delete(pg_auth_members_rel, &authmemtup->t_self);
|
|
}
|
|
ReleaseSysCacheList(auth_mem_list);
|
|
|
|
auth_mem_list = SearchSysCacheList(AUTHMEMMEMROLE, 1,
|
|
ObjectIdGetDatum(roleid),
|
|
0, 0, 0);
|
|
|
|
for (i = 0; i < auth_mem_list->n_members; i++)
|
|
{
|
|
HeapTuple authmemtup = &auth_mem_list->members[i]->tuple;
|
|
simple_heap_delete(pg_auth_members_rel, &authmemtup->t_self);
|
|
}
|
|
ReleaseSysCacheList(auth_mem_list);
|
|
}
|
|
|
|
/*
|
|
* Now we can clean up; but keep lock until commit (to avoid possible
|
|
* deadlock when commit code tries to acquire lock).
|
|
*/
|
|
heap_close(pg_auth_members_rel, NoLock);
|
|
heap_close(pg_authid_rel, NoLock);
|
|
|
|
/*
|
|
* Set flag to update flat auth file at commit.
|
|
*/
|
|
auth_file_update_needed();
|
|
}
|
|
|
|
/*
|
|
* Rename role
|
|
*/
|
|
void
|
|
RenameRole(const char *oldname, const char *newname)
|
|
{
|
|
HeapTuple oldtuple,
|
|
newtuple;
|
|
TupleDesc dsc;
|
|
Relation rel;
|
|
Datum datum;
|
|
bool isnull;
|
|
Datum repl_val[Natts_pg_authid];
|
|
char repl_null[Natts_pg_authid];
|
|
char repl_repl[Natts_pg_authid];
|
|
int i;
|
|
Oid roleid;
|
|
|
|
/* ExclusiveLock because we need to update the password file */
|
|
rel = heap_open(AuthIdRelationId, ExclusiveLock);
|
|
dsc = RelationGetDescr(rel);
|
|
|
|
oldtuple = SearchSysCache(AUTHNAME,
|
|
CStringGetDatum(oldname),
|
|
0, 0, 0);
|
|
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.
|
|
*/
|
|
|
|
roleid = HeapTupleGetOid(oldtuple);
|
|
|
|
if (roleid == GetSessionUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
|
errmsg("session role may not be renamed")));
|
|
|
|
/* make sure the new name doesn't exist */
|
|
if (SearchSysCacheExists(AUTHNAME,
|
|
CStringGetDatum(newname),
|
|
0, 0, 0))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_OBJECT),
|
|
errmsg("role \"%s\" already exists", newname)));
|
|
|
|
/* must be superuser */
|
|
if (!superuser())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to rename roles")));
|
|
|
|
for (i = 0; i < Natts_pg_authid; i++)
|
|
repl_repl[i] = ' ';
|
|
|
|
repl_repl[Anum_pg_authid_rolname - 1] = 'r';
|
|
repl_val[Anum_pg_authid_rolname - 1] = DirectFunctionCall1(namein,
|
|
CStringGetDatum(newname));
|
|
repl_null[Anum_pg_authid_rolname - 1] = ' ';
|
|
|
|
datum = heap_getattr(oldtuple, Anum_pg_authid_rolpassword, dsc, &isnull);
|
|
|
|
if (!isnull && isMD5(DatumGetCString(DirectFunctionCall1(textout, datum))))
|
|
{
|
|
/* MD5 uses the username as salt, so just clear it on a rename */
|
|
repl_repl[Anum_pg_authid_rolpassword - 1] = 'r';
|
|
repl_null[Anum_pg_authid_rolpassword - 1] = 'n';
|
|
|
|
ereport(NOTICE,
|
|
(errmsg("MD5 password cleared because of role rename")));
|
|
}
|
|
|
|
newtuple = heap_modifytuple(oldtuple, dsc, repl_val, repl_null, repl_repl);
|
|
simple_heap_update(rel, &oldtuple->t_self, newtuple);
|
|
|
|
CatalogUpdateIndexes(rel, newtuple);
|
|
|
|
ReleaseSysCache(oldtuple);
|
|
heap_close(rel, NoLock);
|
|
|
|
/*
|
|
* Set flag to update flat auth file at commit.
|
|
*/
|
|
auth_file_update_needed();
|
|
}
|
|
|
|
/*
|
|
* GrantRoleStmt
|
|
*
|
|
* Grant/Revoke roles to/from roles
|
|
*/
|
|
void
|
|
GrantRole(GrantRoleStmt *stmt)
|
|
{
|
|
Oid grantor;
|
|
List *grantee_ids;
|
|
ListCell *item;
|
|
|
|
if (stmt->grantor)
|
|
grantor = get_roleid_checked(stmt->grantor);
|
|
else
|
|
grantor = GetUserId();
|
|
|
|
grantee_ids = roleNamesToIds(stmt->grantee_roles);
|
|
|
|
/*
|
|
* Step through all of the granted roles and add/remove
|
|
* entries for the grantees, or, if admin_opt is set, then
|
|
* just add/remove the admin option.
|
|
*
|
|
* Note: Permissions checking is done by AddRoleMems/DelRoleMems
|
|
*/
|
|
foreach(item, stmt->granted_roles)
|
|
{
|
|
char *rolename = strVal(lfirst(item));
|
|
Oid roleid = get_roleid_checked(rolename);
|
|
|
|
if (stmt->is_grant)
|
|
AddRoleMems(rolename, roleid,
|
|
stmt->grantee_roles, grantee_ids,
|
|
grantor, stmt->admin_opt);
|
|
else
|
|
DelRoleMems(rolename, roleid,
|
|
stmt->grantee_roles, grantee_ids,
|
|
stmt->admin_opt);
|
|
}
|
|
|
|
/*
|
|
* Set flag to update flat auth file at commit.
|
|
*/
|
|
auth_file_update_needed();
|
|
}
|
|
|
|
/*
|
|
* roleNamesToIds
|
|
*
|
|
* Given a list of role names (as String nodes), generate a list of role OIDs
|
|
* in the same order.
|
|
*/
|
|
static List *
|
|
roleNamesToIds(List *memberNames)
|
|
{
|
|
List *result = NIL;
|
|
ListCell *l;
|
|
|
|
foreach(l, memberNames)
|
|
{
|
|
char *rolename = strVal(lfirst(l));
|
|
Oid roleid = get_roleid_checked(rolename);
|
|
|
|
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
|
|
* memberNames: list of names of roles to add (used only for error messages)
|
|
* memberIds: OIDs of roles to add
|
|
* grantorId: who is granting the membership
|
|
* admin_opt: granting admin option?
|
|
*/
|
|
static void
|
|
AddRoleMems(const char *rolename, Oid roleid,
|
|
List *memberNames, List *memberIds,
|
|
Oid grantorId, bool admin_opt)
|
|
{
|
|
Relation pg_authmem_rel;
|
|
TupleDesc pg_authmem_dsc;
|
|
ListCell *nameitem;
|
|
ListCell *iditem;
|
|
|
|
Assert(list_length(memberNames) == list_length(memberIds));
|
|
|
|
/* Skip permission check if nothing to do */
|
|
if (!memberIds)
|
|
return;
|
|
|
|
/*
|
|
* Check permissions: must be superuser or have admin option on the
|
|
* role to be changed.
|
|
*
|
|
* XXX: The admin option is not considered to be inherited through
|
|
* multiple roles, unlike normal 'is_member_of_role' privilege checks.
|
|
*/
|
|
if (!superuser())
|
|
{
|
|
HeapTuple authmem_chk_tuple;
|
|
Form_pg_auth_members authmem_chk;
|
|
|
|
if (grantorId != GetUserId())
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser to set grantor ID")));
|
|
|
|
authmem_chk_tuple = SearchSysCache(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid),
|
|
ObjectIdGetDatum(grantorId),
|
|
0, 0);
|
|
if (!HeapTupleIsValid(authmem_chk_tuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser or have admin option on role \"%s\"",
|
|
rolename)));
|
|
|
|
authmem_chk = (Form_pg_auth_members) GETSTRUCT(authmem_chk_tuple);
|
|
if (!authmem_chk->admin_option)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser or have admin option on role \"%s\"",
|
|
rolename)));
|
|
ReleaseSysCache(authmem_chk_tuple);
|
|
}
|
|
|
|
/*
|
|
* Secure exclusive lock to protect our update of the flat auth file.
|
|
*/
|
|
pg_authmem_rel = heap_open(AuthMemRelationId, ExclusiveLock);
|
|
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
|
|
|
|
forboth(nameitem, memberNames, iditem, memberIds)
|
|
{
|
|
const char *membername = strVal(lfirst(nameitem));
|
|
Oid memberid = lfirst_oid(iditem);
|
|
HeapTuple authmem_tuple;
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_auth_members];
|
|
char new_record_nulls[Natts_pg_auth_members];
|
|
char new_record_repl[Natts_pg_auth_members];
|
|
|
|
/*
|
|
* Check if entry for this role/member already exists;
|
|
* if so, give warning unless we are adding admin option.
|
|
*/
|
|
authmem_tuple = SearchSysCache(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid),
|
|
ObjectIdGetDatum(memberid),
|
|
0, 0);
|
|
if (HeapTupleIsValid(authmem_tuple) && !admin_opt)
|
|
{
|
|
ereport(NOTICE,
|
|
(errmsg("role \"%s\" is already a member of role \"%s\"",
|
|
membername, rolename)));
|
|
ReleaseSysCache(authmem_tuple);
|
|
continue;
|
|
}
|
|
|
|
/* Build a tuple to insert or update */
|
|
MemSet(new_record, 0, sizeof(new_record));
|
|
MemSet(new_record_nulls, ' ', sizeof(new_record_nulls));
|
|
MemSet(new_record_repl, ' ', sizeof(new_record_repl));
|
|
|
|
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);
|
|
new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(admin_opt);
|
|
|
|
if (HeapTupleIsValid(authmem_tuple))
|
|
{
|
|
new_record_repl[Anum_pg_auth_members_grantor - 1] = 'r';
|
|
new_record_repl[Anum_pg_auth_members_admin_option - 1] = 'r';
|
|
tuple = heap_modifytuple(authmem_tuple, pg_authmem_dsc,
|
|
new_record,
|
|
new_record_nulls, new_record_repl);
|
|
simple_heap_update(pg_authmem_rel, &tuple->t_self, tuple);
|
|
CatalogUpdateIndexes(pg_authmem_rel, tuple);
|
|
ReleaseSysCache(authmem_tuple);
|
|
}
|
|
else
|
|
{
|
|
tuple = heap_formtuple(pg_authmem_dsc,
|
|
new_record, new_record_nulls);
|
|
simple_heap_insert(pg_authmem_rel, tuple);
|
|
CatalogUpdateIndexes(pg_authmem_rel, tuple);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Now we can clean up; but keep lock until commit (to avoid possible
|
|
* deadlock when commit code tries to acquire lock).
|
|
*/
|
|
heap_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
|
|
* memberNames: list of names of roles to del (used only for error messages)
|
|
* memberIds: OIDs of roles to del
|
|
* admin_opt: remove admin option only?
|
|
*/
|
|
static void
|
|
DelRoleMems(const char *rolename, Oid roleid,
|
|
List *memberNames, List *memberIds,
|
|
bool admin_opt)
|
|
{
|
|
Relation pg_authmem_rel;
|
|
TupleDesc pg_authmem_dsc;
|
|
ListCell *nameitem;
|
|
ListCell *iditem;
|
|
|
|
Assert(list_length(memberNames) == list_length(memberIds));
|
|
|
|
/* Skip permission check if nothing to do */
|
|
if (!memberIds)
|
|
return;
|
|
|
|
/*
|
|
* Check permissions: must be superuser or have admin option on the
|
|
* role to be changed.
|
|
*
|
|
* XXX: The admin option is not considered to be inherited through
|
|
* multiple roles, unlike normal 'is_member_of_role' privilege checks.
|
|
*/
|
|
if (!superuser())
|
|
{
|
|
HeapTuple authmem_chk_tuple;
|
|
Form_pg_auth_members authmem_chk;
|
|
|
|
authmem_chk_tuple = SearchSysCache(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid),
|
|
ObjectIdGetDatum(GetUserId()),
|
|
0, 0);
|
|
if (!HeapTupleIsValid(authmem_chk_tuple))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser or have admin option on role \"%s\"",
|
|
rolename)));
|
|
|
|
authmem_chk = (Form_pg_auth_members) GETSTRUCT(authmem_chk_tuple);
|
|
if (!authmem_chk->admin_option)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
|
|
errmsg("must be superuser or have admin option on role \"%s\"",
|
|
rolename)));
|
|
ReleaseSysCache(authmem_chk_tuple);
|
|
}
|
|
|
|
/*
|
|
* Secure exclusive lock to protect our update of the flat auth file.
|
|
*/
|
|
pg_authmem_rel = heap_open(AuthMemRelationId, ExclusiveLock);
|
|
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
|
|
|
|
forboth(nameitem, memberNames, iditem, memberIds)
|
|
{
|
|
const char *membername = strVal(lfirst(nameitem));
|
|
Oid memberid = lfirst_oid(iditem);
|
|
HeapTuple authmem_tuple;
|
|
|
|
/*
|
|
* Find entry for this role/member
|
|
*/
|
|
authmem_tuple = SearchSysCache(AUTHMEMROLEMEM,
|
|
ObjectIdGetDatum(roleid),
|
|
ObjectIdGetDatum(memberid),
|
|
0, 0);
|
|
if (!HeapTupleIsValid(authmem_tuple))
|
|
{
|
|
ereport(WARNING,
|
|
(errmsg("role \"%s\" is not a member of role \"%s\"",
|
|
membername, rolename)));
|
|
continue;
|
|
}
|
|
|
|
if (!admin_opt)
|
|
{
|
|
/* Remove the entry altogether */
|
|
simple_heap_delete(pg_authmem_rel, &authmem_tuple->t_self);
|
|
}
|
|
else
|
|
{
|
|
/* Just turn off the admin option */
|
|
HeapTuple tuple;
|
|
Datum new_record[Natts_pg_auth_members];
|
|
char new_record_nulls[Natts_pg_auth_members];
|
|
char new_record_repl[Natts_pg_auth_members];
|
|
|
|
/* Build a tuple to update with */
|
|
MemSet(new_record, 0, sizeof(new_record));
|
|
MemSet(new_record_nulls, ' ', sizeof(new_record_nulls));
|
|
MemSet(new_record_repl, ' ', sizeof(new_record_repl));
|
|
|
|
new_record[Anum_pg_auth_members_admin_option - 1] = BoolGetDatum(false);
|
|
new_record_repl[Anum_pg_auth_members_admin_option - 1] = 'r';
|
|
|
|
tuple = heap_modifytuple(authmem_tuple, pg_authmem_dsc,
|
|
new_record,
|
|
new_record_nulls, new_record_repl);
|
|
simple_heap_update(pg_authmem_rel, &tuple->t_self, tuple);
|
|
CatalogUpdateIndexes(pg_authmem_rel, tuple);
|
|
}
|
|
|
|
ReleaseSysCache(authmem_tuple);
|
|
}
|
|
|
|
/*
|
|
* Now we can clean up; but keep lock until commit (to avoid possible
|
|
* deadlock when commit code tries to acquire lock).
|
|
*/
|
|
heap_close(pg_authmem_rel, NoLock);
|
|
}
|