diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml index 670a5406d618..e91152c729a5 100644 --- a/doc/src/sgml/catalogs.sgml +++ b/doc/src/sgml/catalogs.sgml @@ -1647,11 +1647,10 @@ SCRAM-SHA-256$<iteration count>:&l - Because user identities are cluster-wide, - pg_auth_members - is shared across all databases of a cluster: there is only one - copy of pg_auth_members per cluster, not - one per database. + User identities are cluster-wide, but role memberships can be either + cluster-wide or database-specific (as specified by the value of the + dbid column). The pg_auth_members + catalog is shared across all databases of a cluster. @@ -1708,6 +1707,17 @@ SCRAM-SHA-256$<iteration count>:&l roleid to others + + + + dbid oid + (references pg_database.oid) + + + ID of the database that this membership is constrained to; zero if membership is cluster-wide + + +
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index f744b05b55dc..c1d07b74d88f 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -98,6 +98,7 @@ GRANT { USAGE | ALL [ PRIVILEGES ] } [ GRANTED BY role_specification ] GRANT role_name [, ...] TO role_specification [, ...] + [ IN DATABASE database_name | IN CURRENT DATABASE ] [ WITH ADMIN OPTION ] [ GRANTED BY role_specification ] @@ -251,7 +252,23 @@ GRANT role_name [, ...] TO GRANT command grants membership in a role to one or more other roles. Membership in a role is significant because it conveys the privileges granted to a role to each of its - members. + members. Membership is effective cluster-wide unless otherwise constrained + through the use of a database-specific clause. Both database-specific and cluster-wide + versions of a role membership grant may exist at the same time. In the event that + multiple grants apply, the membership privileges conferred are additive. + + + + If IN DATABASE database_name + is specified, membership in role_name + will be effective only when the recipient is connected to the database specified by + database_name. + + + + If IN CURRENT DATABASE is specified, the membership in + role_name will be effective only when the + recipient is connected to the same database that the grant was issued in. @@ -277,6 +294,10 @@ GRANT role_name [, ...] TO GROUP in role_specification. + + + See for more information about role memberships. + @@ -398,10 +419,18 @@ GRANT ALL PRIVILEGES ON kinds TO manuel; - Grant membership in role admins to user joe: + Grant cluster-wide membership in role admins to user joe: GRANT admins TO joe; + + + + Grant read and write access to user alice in the database + named sales: + + +GRANT pg_read_all_data, pg_write_all_data TO alice IN DATABASE sales; diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 62f197103696..7e16e09087b0 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -127,6 +127,7 @@ REVOKE [ GRANT OPTION FOR ] REVOKE [ ADMIN OPTION FOR ] role_name [, ...] FROM role_specification [, ...] + [ IN DATABASE database_name | IN CURRENT DATABASE ] [ GRANTED BY role_specification ] [ CASCADE | RESTRICT ] @@ -305,6 +306,14 @@ REVOKE ALL PRIVILEGES ON kinds FROM manuel; REVOKE admins FROM joe; + + + + Revoke write access for user bob from the sales + database: + + +REVOKE pg_write_all_data FROM bob IN DATABASE sales; diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml index 6eaaaa36b881..94a2c6cb9e2e 100644 --- a/doc/src/sgml/user-manag.sgml +++ b/doc/src/sgml/user-manag.sgml @@ -308,6 +308,11 @@ CREATE ROLE name; GRANT group_role TO role1, ... ; REVOKE group_role FROM role1, ... ; + + Role membership can also be granted and revoked within the context of a specific database: + +GRANT group_role TO role1, ... IN CURRENT DATABASE; +REVOKE group_role FROM role1, ... IN DATABASE database_name; You can grant membership to other group roles, too (since there isn't really any distinction between group roles and non-group roles). The @@ -398,9 +403,6 @@ RESET ROLE; SET ROLE admin. - - - To destroy a group role, use DROP ROLE: @@ -639,7 +641,7 @@ DROP ROLE doomed_role; - Administrators can grant access to these roles to users using the + Administrators can grant cluster-wide access to these roles to users using the GRANT command, for example: @@ -647,6 +649,14 @@ GRANT pg_signal_backend TO admin_user; + + Access can also be granted within the context of a specific database, + for example: + + +GRANT pg_read_all_data TO reporting_user IN DATABASE sales; + + diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c index 6f43870779f6..058e3155c00f 100644 --- a/src/backend/catalog/catalog.c +++ b/src/backend/catalog/catalog.c @@ -260,8 +260,9 @@ IsSharedRelation(Oid relationId) /* These are their indexes */ if (relationId == AuthIdOidIndexId || relationId == AuthIdRolnameIndexId || - relationId == AuthMemMemRoleIndexId || - relationId == AuthMemRoleMemIndexId || + relationId == AuthMemDbMemRoleIndexId || + relationId == AuthMemRoleMemDbIndexId || + relationId == AuthMemMemRoleDbIndexId || relationId == DatabaseNameIndexId || relationId == DatabaseOidIndexId || relationId == DbRoleSettingDatidRolidIndexId || diff --git a/src/backend/commands/dbcommands.c b/src/backend/commands/dbcommands.c index 099d369b2f4a..b9583cf06344 100644 --- a/src/backend/commands/dbcommands.c +++ b/src/backend/commands/dbcommands.c @@ -47,6 +47,7 @@ #include "commands/defrem.h" #include "commands/seclabel.h" #include "commands/tablespace.h" +#include "commands/user.h" #include "mb/pg_wchar.h" #include "miscadmin.h" #include "pgstat.h" @@ -1643,6 +1644,11 @@ dropdb(const char *dbname, bool missing_ok, bool force) DeleteSharedComments(db_id, DatabaseRelationId); DeleteSharedSecurityLabel(db_id, DatabaseRelationId); + /* + * Delete any roles memberships directly associated with this database. + */ + DropDatabaseSpecificRoles(db_id); + /* * Remove settings associated with this database */ diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 5b24b6dcad80..ede6723c6d68 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -51,10 +51,10 @@ check_password_hook_type check_password_hook = NULL; static void AddRoleMems(const char *rolename, Oid roleid, List *memberSpecs, List *memberIds, - Oid grantorId, bool admin_opt); + Oid grantorId, bool admin_opt, Oid dbid); static void DelRoleMems(const char *rolename, Oid roleid, List *memberSpecs, List *memberIds, - bool admin_opt); + bool admin_opt, Oid dbid); /* Check if current user has createrole privileges */ @@ -449,7 +449,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) AddRoleMems(oldrolename, oldroleid, thisrole_list, thisrole_oidlist, - GetUserId(), false); + GetUserId(), false, InvalidOid); ReleaseSysCache(oldroletup); } @@ -461,10 +461,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) */ AddRoleMems(stmt->role, roleid, adminmembers, roleSpecsToIds(adminmembers), - GetUserId(), true); + GetUserId(), true, InvalidOid); AddRoleMems(stmt->role, roleid, rolemembers, roleSpecsToIds(rolemembers), - GetUserId(), false); + GetUserId(), false, InvalidOid); /* Post creation hook for new role */ InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0); @@ -798,11 +798,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) if (stmt->action == +1) /* add members to role */ AddRoleMems(rolename, roleid, rolemembers, roleSpecsToIds(rolemembers), - GetUserId(), false); + GetUserId(), false, InvalidOid); else if (stmt->action == -1) /* drop members from role */ DelRoleMems(rolename, roleid, rolemembers, roleSpecsToIds(rolemembers), - false); + false, InvalidOid); } /* @@ -1018,7 +1018,7 @@ DropRole(DropRoleStmt *stmt) BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(roleid)); - sscan = systable_beginscan(pg_auth_members_rel, AuthMemRoleMemIndexId, + sscan = systable_beginscan(pg_auth_members_rel, AuthMemRoleMemDbIndexId, true, NULL, 1, &scankey); while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan))) @@ -1033,7 +1033,7 @@ DropRole(DropRoleStmt *stmt) BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(roleid)); - sscan = systable_beginscan(pg_auth_members_rel, AuthMemMemRoleIndexId, + sscan = systable_beginscan(pg_auth_members_rel, AuthMemMemRoleDbIndexId, true, NULL, 1, &scankey); while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan))) @@ -1223,6 +1223,17 @@ GrantRole(GrantRoleStmt *stmt) Oid grantor; List *grantee_ids; ListCell *item; + Oid dbid; + + /* Determine if this grant/revoke is database-specific */ + if (stmt->database == NULL) { + dbid = InvalidOid; + } else if (strcmp(stmt->database, "") == 0) { + dbid = MyDatabaseId; + } else { + dbid = get_database_oid(stmt->database, false); + } + if (stmt->grantor) grantor = get_rolespec_oid(stmt->grantor, false); @@ -1257,11 +1268,11 @@ GrantRole(GrantRoleStmt *stmt) if (stmt->is_grant) AddRoleMems(rolename, roleid, stmt->grantee_roles, grantee_ids, - grantor, stmt->admin_opt); + grantor, stmt->admin_opt, dbid); else DelRoleMems(rolename, roleid, stmt->grantee_roles, grantee_ids, - stmt->admin_opt); + stmt->admin_opt, dbid); } /* @@ -1368,7 +1379,7 @@ roleSpecsToIds(List *memberNames) static void AddRoleMems(const char *rolename, Oid roleid, List *memberSpecs, List *memberIds, - Oid grantorId, bool admin_opt) + Oid grantorId, bool admin_opt, Oid dbid) { Relation pg_authmem_rel; TupleDesc pg_authmem_dsc; @@ -1395,7 +1406,7 @@ AddRoleMems(const char *rolename, Oid roleid, else { if (!have_createrole_privilege() && - !is_admin_of_role(grantorId, roleid)) + !is_admin_of_role(grantorId, roleid, dbid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must have admin option on role \"%s\"", @@ -1478,16 +1489,23 @@ AddRoleMems(const char *rolename, Oid roleid, * Check if entry for this role/member already exists; if so, give * warning unless we are adding admin option. */ - authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM, + authmem_tuple = SearchSysCache3(AUTHMEMROLEMEMDB, ObjectIdGetDatum(roleid), - ObjectIdGetDatum(memberid)); + ObjectIdGetDatum(memberid), + ObjectIdGetDatum(dbid)); if (HeapTupleIsValid(authmem_tuple) && (!admin_opt || ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option)) { - ereport(NOTICE, - (errmsg("role \"%s\" is already a member of role \"%s\"", - get_rolespec_name(memberRole), rolename))); + if (dbid == InvalidOid) { + ereport(NOTICE, + (errmsg("role \"%s\" is already a member of role \"%s\"", + get_rolespec_name(memberRole), rolename))); + } else { + ereport(NOTICE, + (errmsg("role \"%s\" is already a member of role \"%s\" in database \"%s\"", + get_rolespec_name(memberRole), rolename, get_database_name(dbid)))); + } ReleaseSysCache(authmem_tuple); continue; } @@ -1497,6 +1515,7 @@ AddRoleMems(const char *rolename, Oid 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); + new_record[Anum_pg_auth_members_dbid - 1] = ObjectIdGetDatum(dbid); if (HeapTupleIsValid(authmem_tuple)) { @@ -1537,7 +1556,7 @@ AddRoleMems(const char *rolename, Oid roleid, static void DelRoleMems(const char *rolename, Oid roleid, List *memberSpecs, List *memberIds, - bool admin_opt) + bool admin_opt, Oid dbid) { Relation pg_authmem_rel; TupleDesc pg_authmem_dsc; @@ -1564,7 +1583,7 @@ DelRoleMems(const char *rolename, Oid roleid, else { if (!have_createrole_privilege() && - !is_admin_of_role(GetUserId(), roleid)) + !is_admin_of_role(GetUserId(), roleid, dbid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must have admin option on role \"%s\"", @@ -1583,14 +1602,21 @@ DelRoleMems(const char *rolename, Oid roleid, /* * Find entry for this role/member */ - authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM, + authmem_tuple = SearchSysCache3(AUTHMEMROLEMEMDB, ObjectIdGetDatum(roleid), - ObjectIdGetDatum(memberid)); + ObjectIdGetDatum(memberid), + ObjectIdGetDatum(dbid)); if (!HeapTupleIsValid(authmem_tuple)) { - ereport(WARNING, - (errmsg("role \"%s\" is not a member of role \"%s\"", - get_rolespec_name(memberRole), rolename))); + if (dbid == InvalidOid){ + ereport(WARNING, + (errmsg("role \"%s\" is not a member of role \"%s\"", + get_rolespec_name(memberRole), rolename))); + } else { + ereport(WARNING, + (errmsg("role \"%s\" is not a member of role \"%s\" in database \"%s\"", + get_rolespec_name(memberRole), rolename, get_database_name(dbid)))); + } continue; } @@ -1628,3 +1654,42 @@ DelRoleMems(const char *rolename, Oid roleid, */ table_close(pg_authmem_rel, NoLock); } + +/* + * DropDatabaseSpecificRoles + * + * Delete pg_auth_members entries corresponding to a database that's being + * dropped. + */ +void +DropDatabaseSpecificRoles(Oid databaseId) +{ + Relation pg_authmem_rel; + ScanKeyData key[1]; + SysScanDesc scan; + HeapTuple tup; + + pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock); + + /* + * First, delete all the entries that have the database Oid in the dbid + * field. + */ + ScanKeyInit(&key[0], + Anum_pg_auth_members_dbid, + BTEqualStrategyNumber, F_OIDEQ, + ObjectIdGetDatum(databaseId)); + /* We leave the other index fields unspecified */ + + scan = systable_beginscan(pg_authmem_rel, AuthMemDbMemRoleIndexId, true, + NULL, 1, key); + + while (HeapTupleIsValid(tup = systable_getnext(scan))) + { + CatalogTupleDelete(pg_authmem_rel, &tup->t_self); + } + + systable_endscan(scan); + + table_close(pg_authmem_rel, RowExclusiveLock); +} diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index c018140afe41..c502a6b5dd46 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -369,7 +369,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %type opt_type %type foreign_server_version opt_foreign_server_version -%type opt_in_database +%type opt_in_database opt_grant_in_database %type OptSchemaName parameter_name %type OptSchemaEltList parameter_name_list @@ -7816,6 +7816,11 @@ grantee: ; +opt_grant_in_database: + IN_P CURRENT_P DATABASE { $$ = ""; } + | opt_in_database { $$ = $1; } + ; + opt_grant_grant_option: WITH GRANT OPTION { $$ = true; } | /*EMPTY*/ { $$ = false; } @@ -7828,21 +7833,22 @@ opt_grant_grant_option: *****************************************************************************/ GrantRoleStmt: - GRANT privilege_list TO role_list opt_grant_admin_option opt_granted_by + GRANT privilege_list TO role_list opt_grant_in_database opt_grant_admin_option opt_granted_by { GrantRoleStmt *n = makeNode(GrantRoleStmt); n->is_grant = true; n->granted_roles = $2; n->grantee_roles = $4; - n->admin_opt = $5; - n->grantor = $6; + n->database = $5; + n->admin_opt = $6; + n->grantor = $7; $$ = (Node *) n; } ; RevokeRoleStmt: - REVOKE privilege_list FROM role_list opt_granted_by opt_drop_behavior + REVOKE privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior { GrantRoleStmt *n = makeNode(GrantRoleStmt); @@ -7850,10 +7856,11 @@ RevokeRoleStmt: n->admin_opt = false; n->granted_roles = $2; n->grantee_roles = $4; - n->behavior = $6; + n->database = $5; + n->behavior = $7; $$ = (Node *) n; } - | REVOKE ADMIN OPTION FOR privilege_list FROM role_list opt_granted_by opt_drop_behavior + | REVOKE ADMIN OPTION FOR privilege_list FROM role_list opt_grant_in_database opt_granted_by opt_drop_behavior { GrantRoleStmt *n = makeNode(GrantRoleStmt); @@ -7861,7 +7868,8 @@ RevokeRoleStmt: n->admin_opt = true; n->granted_roles = $5; n->grantee_roles = $7; - n->behavior = $9; + n->database = $8; + n->behavior = $10; $$ = (Node *) n; } ; diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 6fa58dd8eb07..7e7fb9d00efe 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -4704,7 +4704,7 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode) { if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE)) { - if (is_admin_of_role(roleid, role_oid)) + if (is_admin_of_role(roleid, role_oid, MyDatabaseId)) return ACLCHECK_OK; } if (mode & ACL_CREATE) @@ -4738,7 +4738,7 @@ initialize_acl(void) * of pg_auth_members (for roles_is_member_of()), pg_authid (for * has_rolinherit()), or pg_database (for roles_is_member_of()) */ - CacheRegisterSyscacheCallback(AUTHMEMROLEMEM, + CacheRegisterSyscacheCallback(AUTHMEMROLEMEMDB, RoleMembershipCacheCallback, (Datum) 0); CacheRegisterSyscacheCallback(AUTHOID, @@ -4787,6 +4787,48 @@ has_rolinherit(Oid roleid) } +/* + * Appends role memberships to the list of roles + */ +static void +append_role_memberships(List *roles_list, bool *is_admin, Oid admin_of, + Oid memberid, Oid targetDatabaseId, Oid databaseId) +{ + CatCList *memlist; + int i; + + /* Find roles that memberid is directly a member of */ + memlist = SearchSysCacheList2(AUTHMEMDBMEMROLE, + ObjectIdGetDatum(targetDatabaseId), + ObjectIdGetDatum(memberid)); + for (i = 0; i < memlist->n_members; i++) + { + HeapTuple tup = &memlist->members[i]->tuple; + Oid otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid; + + /* + * While otherid==InvalidOid shouldn't appear in the catalog, the + * OidIsValid() avoids crashing if that arises. This reports if + * the admin option has been granted. + */ + if (otherid == admin_of && + ((Form_pg_auth_members) GETSTRUCT(tup))->admin_option && + ((Form_pg_auth_members) GETSTRUCT(tup))->dbid == databaseId && + OidIsValid(admin_of)) + *is_admin = true; + + /* + * Even though there shouldn't be any loops in the membership + * graph, we must test for having already seen this role. It is + * legal for instance to have both A->B and A->C->B. + */ + roles_list = list_append_unique_oid(roles_list, otherid); + } + ReleaseSysCacheList(memlist); + +} + + /* * Get a list of roles that the specified roleid is a member of * @@ -4804,7 +4846,7 @@ has_rolinherit(Oid roleid) */ static List * roles_is_member_of(Oid roleid, enum RoleRecurseType type, - Oid admin_of, bool *is_admin) + Oid admin_of, bool *is_admin, Oid databaseId) { Oid dba; List *roles_list; @@ -4853,37 +4895,15 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type, foreach(l, roles_list) { Oid memberid = lfirst_oid(l); - CatCList *memlist; - int i; if (type == ROLERECURSE_PRIVS && !has_rolinherit(memberid)) continue; /* ignore non-inheriting roles */ - /* Find roles that memberid is directly a member of */ - memlist = SearchSysCacheList1(AUTHMEMMEMROLE, - ObjectIdGetDatum(memberid)); - for (i = 0; i < memlist->n_members; i++) - { - HeapTuple tup = &memlist->members[i]->tuple; - Oid otherid = ((Form_pg_auth_members) GETSTRUCT(tup))->roleid; - - /* - * While otherid==InvalidOid shouldn't appear in the catalog, the - * OidIsValid() avoids crashing if that arises. - */ - if (otherid == admin_of && - ((Form_pg_auth_members) GETSTRUCT(tup))->admin_option && - OidIsValid(admin_of)) - *is_admin = true; - - /* - * Even though there shouldn't be any loops in the membership - * graph, we must test for having already seen this role. It is - * legal for instance to have both A->B and A->C->B. - */ - roles_list = list_append_unique_oid(roles_list, otherid); - } - ReleaseSysCacheList(memlist); + /* Find roles that memberid is directly a member of globally */ + append_role_memberships(roles_list, is_admin, admin_of, memberid, InvalidOid, InvalidOid); + + /* Find roles that memberid is directly a member of in the current database */ + append_role_memberships(roles_list, is_admin, admin_of, memberid, MyDatabaseId, databaseId); /* implement pg_database_owner implicit membership */ if (memberid == dba && OidIsValid(dba)) @@ -4935,7 +4955,7 @@ has_privs_of_role(Oid member, Oid role) * multi-level recursion, then see if target role is any one of them. */ return list_member_oid(roles_is_member_of(member, ROLERECURSE_PRIVS, - InvalidOid, NULL), + InvalidOid, NULL, InvalidOid), role); } @@ -4963,7 +4983,7 @@ is_member_of_role(Oid member, Oid role) * recursion, then see if target role is any one of them. */ return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS, - InvalidOid, NULL), + InvalidOid, NULL, InvalidOid), role); } @@ -5001,7 +5021,7 @@ is_member_of_role_nosuper(Oid member, Oid role) * recursion, then see if target role is any one of them. */ return list_member_oid(roles_is_member_of(member, ROLERECURSE_MEMBERS, - InvalidOid, NULL), + InvalidOid, NULL, InvalidOid), role); } @@ -5012,7 +5032,7 @@ is_member_of_role_nosuper(Oid member, Oid role) * or a superuser? */ bool -is_admin_of_role(Oid member, Oid role) +is_admin_of_role(Oid member, Oid role, Oid databaseId) { bool result = false; @@ -5023,7 +5043,8 @@ is_admin_of_role(Oid member, Oid role) if (member == role) return false; - (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result); + /* Check for WITH ADMIN OPTION either globally or for the given database */ + (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result, databaseId); return result; } @@ -5099,7 +5120,7 @@ select_best_grantor(Oid roleId, AclMode privileges, * doesn't query any role memberships. */ roles_list = roles_is_member_of(roleId, ROLERECURSE_PRIVS, - InvalidOid, NULL); + InvalidOid, NULL, InvalidOid); /* initialize candidate result as default */ *grantorId = roleId; diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c index 6ae7c1f50b89..2a83c586fb42 100644 --- a/src/backend/utils/cache/catcache.c +++ b/src/backend/utils/cache/catcache.c @@ -1104,7 +1104,7 @@ IndexScanOK(CatCache *cache, ScanKey cur_skey) case AUTHNAME: case AUTHOID: - case AUTHMEMMEMROLE: + case AUTHMEMMEMROLEDB: case DATABASEOID: /* diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c index bdb771d278f4..a41657e7ad26 100644 --- a/src/backend/utils/cache/relcache.c +++ b/src/backend/utils/cache/relcache.c @@ -4086,7 +4086,7 @@ RelationCacheInitializePhase3(void) AuthIdRelationId); load_critical_index(AuthIdOidIndexId, AuthIdRelationId); - load_critical_index(AuthMemMemRoleIndexId, + load_critical_index(AuthMemMemRoleDbIndexId, AuthMemRelationId); load_critical_index(SharedSecLabelObjectIndexId, SharedSecLabelRelationId); diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c index 1912b121463d..2cd084e7afe0 100644 --- a/src/backend/utils/cache/syscache.c +++ b/src/backend/utils/cache/syscache.c @@ -211,24 +211,35 @@ static const struct cachedesc cacheinfo[] = { }, 128 }, - {AuthMemRelationId, /* AUTHMEMMEMROLE */ - AuthMemMemRoleIndexId, - 2, + {AuthMemRelationId, /* AUTHMEMDBMEMROLE */ + AuthMemDbMemRoleIndexId, + 3, { + Anum_pg_auth_members_dbid, Anum_pg_auth_members_member, Anum_pg_auth_members_roleid, - 0, 0 }, 8 }, - {AuthMemRelationId, /* AUTHMEMROLEMEM */ - AuthMemRoleMemIndexId, - 2, + {AuthMemRelationId, /* AUTHMEMMEMROLEDB */ + AuthMemMemRoleDbIndexId, + 3, + { + Anum_pg_auth_members_member, + Anum_pg_auth_members_roleid, + Anum_pg_auth_members_dbid, + 0 + }, + 8 + }, + {AuthMemRelationId, /* AUTHMEMROLEMEMDB */ + AuthMemRoleMemDbIndexId, + 3, { Anum_pg_auth_members_roleid, Anum_pg_auth_members_member, - 0, + Anum_pg_auth_members_dbid, 0 }, 8 diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c index 26d3d53809ba..52ad68a5bf38 100644 --- a/src/bin/pg_dump/pg_dumpall.c +++ b/src/bin/pg_dump/pg_dumpall.c @@ -36,7 +36,7 @@ static void help(void); static void dropRoles(PGconn *conn); static void dumpRoles(PGconn *conn); -static void dumpRoleMembership(PGconn *conn); +static void dumpRoleMembership(PGconn *conn, const char *databaseId); static void dumpRoleGUCPrivs(PGconn *conn); static void dropTablespaces(PGconn *conn); static void dumpTablespaces(PGconn *conn); @@ -564,7 +564,7 @@ main(int argc, char *argv[]) dumpRoles(conn); /* Dump role memberships */ - dumpRoleMembership(conn); + dumpRoleMembership(conn, "0"); /* Dump role GUC privileges */ if (server_version >= 150000 && !skip_acls) @@ -921,7 +921,7 @@ dumpRoles(PGconn *conn) * no membership yet. */ static void -dumpRoleMembership(PGconn *conn) +dumpRoleMembership(PGconn *conn, const char *databaseId) { PQExpBuffer buf = createPQExpBuffer(); PGresult *res; @@ -935,8 +935,9 @@ dumpRoleMembership(PGconn *conn) "LEFT JOIN %s ur on ur.oid = a.roleid " "LEFT JOIN %s um on um.oid = a.member " "LEFT JOIN %s ug on ug.oid = a.grantor " - "WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')" - "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog); + "WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_') " + "AND a.dbid = %s " + "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog, databaseId); res = executeQuery(conn, buf->data); if (PQntuples(res) > 0) @@ -950,6 +951,8 @@ dumpRoleMembership(PGconn *conn) fprintf(OPF, "GRANT %s", fmtId(roleid)); fprintf(OPF, " TO %s", fmtId(member)); + if (strcmp(databaseId, "0") != 0) + fprintf(OPF, " IN CURRENT DATABASE"); if (*option == 't') fprintf(OPF, " WITH ADMIN OPTION"); @@ -1319,7 +1322,7 @@ dumpDatabases(PGconn *conn) * doesn't have some failure mode with --clean. */ res = executeQuery(conn, - "SELECT datname " + "SELECT datname, oid " "FROM pg_database d " "WHERE datallowconn " "ORDER BY (datname <> 'template1'), datname"); @@ -1330,6 +1333,7 @@ dumpDatabases(PGconn *conn) for (i = 0; i < PQntuples(res); i++) { char *dbname = PQgetvalue(res, i, 0); + char *dbid = PQgetvalue(res, i, 1); const char *create_opts; int ret; @@ -1370,6 +1374,10 @@ dumpDatabases(PGconn *conn) else create_opts = "--create"; + /* Dump database-specific roles if server is running 16.0 or later */ + if (server_version >= 160000) + dumpRoleMembership(conn, dbid); + if (filename) fclose(OPF); diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h index 1bc027f133d5..26d0d5381e8b 100644 --- a/src/include/catalog/pg_auth_members.h +++ b/src/include/catalog/pg_auth_members.h @@ -33,6 +33,7 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_ Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */ Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */ bool admin_option; /* granted with admin option? */ + Oid dbid BKI_LOOKUP_OPT(pg_database); /* ID of a database this mapping is effective in */ } FormData_pg_auth_members; /* ---------------- @@ -42,7 +43,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_ */ typedef FormData_pg_auth_members *Form_pg_auth_members; -DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops)); -DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops)); +DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_dbid_index, 2694, AuthMemRoleMemDbIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, dbid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_dbid_index, 2695, AuthMemMemRoleDbIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, dbid oid_ops)); +DECLARE_UNIQUE_INDEX(pg_auth_members_dbid_member_role_index, 4715, AuthMemDbMemRoleIndexId, on pg_auth_members using btree(dbid oid_ops, member oid_ops, roleid oid_ops)); #endif /* PG_AUTH_MEMBERS_H */ diff --git a/src/include/commands/user.h b/src/include/commands/user.h index d3dd8303d28c..72be6dbe2f72 100644 --- a/src/include/commands/user.h +++ b/src/include/commands/user.h @@ -33,5 +33,6 @@ extern ObjectAddress RenameRole(const char *oldname, const char *newname); extern void DropOwnedObjects(DropOwnedStmt *stmt); extern void ReassignOwnedObjects(ReassignOwnedStmt *stmt); extern List *roleSpecsToIds(List *memberNames); +extern void DropDatabaseSpecificRoles(Oid databaseId); #endif /* USER_H */ diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 98fe1abaa28a..dd7f4726cdd4 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -2449,6 +2449,8 @@ typedef struct GrantRoleStmt NodeTag type; List *granted_roles; /* list of roles to be granted/revoked */ List *grantee_roles; /* list of member roles to add/delete */ + char *database; /* name of DB this grant applies to + NULL = global, "" = current database, otherwise a named database */ bool is_grant; /* true = GRANT, false = REVOKE */ bool admin_opt; /* with admin option */ RoleSpec *grantor; /* set grantor to other than current role */ diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 48f7d72add5d..29043d569e62 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -211,7 +211,7 @@ extern int aclmembers(const Acl *acl, Oid **roleids); extern bool has_privs_of_role(Oid member, Oid role); extern bool is_member_of_role(Oid member, Oid role); extern bool is_member_of_role_nosuper(Oid member, Oid role); -extern bool is_admin_of_role(Oid member, Oid role); +extern bool is_admin_of_role(Oid member, Oid role, Oid databaseId); extern void check_is_member_of_role(Oid member, Oid role); extern Oid get_role_oid(const char *rolename, bool missing_ok); extern Oid get_role_oid_or_public(const char *rolename); diff --git a/src/include/utils/syscache.h b/src/include/utils/syscache.h index 4463ea66bea5..8973ff6ebca9 100644 --- a/src/include/utils/syscache.h +++ b/src/include/utils/syscache.h @@ -39,8 +39,9 @@ enum SysCacheIdentifier AMPROCNUM, ATTNAME, ATTNUM, - AUTHMEMMEMROLE, - AUTHMEMROLEMEM, + AUTHMEMDBMEMROLE, + AUTHMEMMEMROLEDB, + AUTHMEMROLEMEMDB, AUTHNAME, AUTHOID, CASTSOURCETARGET, diff --git a/src/test/modules/unsafe_tests/Makefile b/src/test/modules/unsafe_tests/Makefile index df582736884f..9adc988e0220 100644 --- a/src/test/modules/unsafe_tests/Makefile +++ b/src/test/modules/unsafe_tests/Makefile @@ -1,6 +1,6 @@ # src/test/modules/unsafe_tests/Makefile -REGRESS = rolenames alter_system_table guc_privs +REGRESS = rolenames alter_system_table guc_privs role_membership ifdef USE_PGXS PG_CONFIG = pg_config diff --git a/src/test/modules/unsafe_tests/expected/role_membership.out b/src/test/modules/unsafe_tests/expected/role_membership.out new file mode 100644 index 000000000000..be79092b47ca --- /dev/null +++ b/src/test/modules/unsafe_tests/expected/role_membership.out @@ -0,0 +1,541 @@ +-- +-- Tests for database-specific role memberships. +-- This is unsafe because roles and databases will added / removed / modified. +-- +CREATE ROLE role_admin LOGIN SUPERUSER; +\connect postgres role_admin +CREATE FUNCTION check_memberships() + RETURNS TABLE (role name, member name, grantor name, admin_option boolean, datname name) + AS $$ +SELECT + r.rolname as role, + m.rolname as member, + g.rolname as grantor, + admin_option, + d.datname +FROM pg_auth_members a +LEFT JOIN pg_roles r ON r.oid = a.roleid +LEFT JOIN pg_roles m ON m.oid = a.member +LEFT JOIN pg_roles g ON g.oid = a.grantor +LEFT JOIN pg_database d ON d.oid = a.dbid +WHERE + m.rolname LIKE 'role_%' +ORDER BY + 1, 2, 5 +$$ LANGUAGE SQL; +-- Populate test databases +\connect template1 +CREATE TABLE data AS SELECT generate_series(1, 3); +CREATE DATABASE db_1; +CREATE DATABASE db_2; +CREATE DATABASE db_3; +CREATE DATABASE db_4; +-- Read all cluster-wide with admin option +CREATE ROLE role_read_all_with_admin; +GRANT pg_read_all_data TO role_read_all_with_admin WITH ADMIN OPTION; +-- Read all in databases 1 and 2 +CREATE ROLE role_read_12; +GRANT pg_read_all_data TO role_read_12 IN DATABASE db_1; +GRANT pg_read_all_data TO role_read_12 IN DATABASE db_2; +-- Read all in databases 3 and 4 with admin option +CREATE ROLE role_read_34; +GRANT pg_read_all_data TO role_read_34 IN DATABASE db_3 WITH ADMIN OPTION; +GRANT pg_read_all_data TO role_read_34 IN DATABASE db_4 WITH ADMIN OPTION; +-- Inherits read all in databases 3 and 4 +CREATE ROLE role_inherited_34; +GRANT role_read_34 TO role_inherited_34; +-- Inherits read all in database 3 +CREATE ROLE role_inherited_3; +GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3; +-- No inherit +CREATE ROLE role_read_all_noinherit NOINHERIT; +GRANT role_read_all_with_admin TO role_read_all_noinherit; +-- No inherit in databases 1 and 2 +CREATE ROLE role_read_12_noinherit NOINHERIT; +GRANT role_read_12 TO role_read_12_noinherit; +-- Alternate syntax +CREATE ROLE role_read_template1; +GRANT pg_read_all_data TO role_read_template1, role_read_all_noinherit IN CURRENT DATABASE; +-- Failure due to missing database +GRANT pg_read_all_data TO role_read_template1 IN DATABASE non_existent; -- error +ERROR: database "non_existent" does not exist +-- Should warn on duplicate grants +GRANT pg_read_all_data TO role_read_all_with_admin; -- notice +NOTICE: role "role_read_all_with_admin" is already a member of role "pg_read_all_data" +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1; -- notice +NOTICE: role "role_read_template1" is already a member of role "pg_read_all_data" in database "template1" +-- Should not warn if adjusting admin option +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- silent +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- notice +NOTICE: role "role_read_template1" is already a member of role "pg_read_all_data" in database "template1" +-- Check membership table +\connect postgres role_admin +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+------------+--------------+----------- + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + pg_read_all_data | role_read_template1 | role_admin | t | template1 + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(11 rows) + +-- Test membership privileges (db_1) +\connect db_1 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_12; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_34; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_inherited_34; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- success +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET ROLE role_read_34; -- success +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET ROLE role_read_34; -- error +ERROR: permission denied to set role "role_read_34" +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +-- Test membership privileges (db_2) +\connect db_2 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_12; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_34; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_inherited_34; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- success +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET ROLE role_read_34; -- success +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET ROLE role_read_34; -- error +ERROR: permission denied to set role "role_read_34" +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +-- Test membership privileges (db_3) +\connect db_3 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_12; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_34; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_inherited_34; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_inherited_3; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12; -- error +SELECT * FROM data; -- error +ERROR: permission denied for table data +-- Test membership privileges (db_4) +\connect db_4 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_read_12; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_34; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_inherited_34; +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +ERROR: permission denied to set role "pg_read_all_data" +SET ROLE role_read_34; -- error +ERROR: permission denied to set role "role_read_34" +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + generate_series +----------------- + 1 + 2 + 3 +(3 rows) + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +ERROR: permission denied for table data +SET ROLE role_read_12; -- error +SELECT * FROM data; -- error +ERROR: permission denied for table data +\connect postgres role_admin +-- Should not warn if revoking admin option +REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent +REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+------------+--------------+----------- + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + pg_read_all_data | role_read_template1 | role_admin | f | template1 + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(11 rows) + +-- Should warn if revoking a non-existent membership +REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- success +REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- warning +WARNING: role "role_read_template1" is not a member of role "pg_read_all_data" in database "template1" +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+------------+--------------+----------- + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(10 rows) + +-- Revoke should only apply to the specified level +REVOKE pg_read_all_data FROM role_read_12; -- warning +WARNING: role "role_read_12" is not a member of role "pg_read_all_data" +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+------------+--------------+----------- + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(10 rows) + +-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases +CREATE ROLE role_granted; +SET SESSION AUTHORIZATION role_read_all_with_admin; +GRANT pg_read_all_data TO role_granted; -- success +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_1; -- success +GRANT role_read_34 TO role_granted; -- error +ERROR: must have admin option on role "role_read_34" +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+--------------------------+--------------+----------- + pg_read_all_data | role_granted | role_read_all_with_admin | f | db_1 + pg_read_all_data | role_granted | role_read_all_with_admin | f | postgres + pg_read_all_data | role_granted | role_read_all_with_admin | f | + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(13 rows) + +-- Ensure database-specific admin option can only grant within that database +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error +ERROR: must have admin option on role "pg_read_all_data" +\connect db_3 +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- notice +NOTICE: role "role_granted" is already a member of role "pg_read_all_data" in database "db_3" +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error +ERROR: must have admin option on role "pg_read_all_data" +\connect db_4 +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error +ERROR: must have admin option on role "pg_read_all_data" +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- notice +NOTICE: role "role_granted" is already a member of role "pg_read_all_data" in database "db_4" +\connect postgres role_admin +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+--------------------------+--------------+----------- + pg_read_all_data | role_granted | role_read_all_with_admin | f | db_1 + pg_read_all_data | role_granted | role_read_34 | f | db_3 + pg_read_all_data | role_granted | role_read_34 | f | db_4 + pg_read_all_data | role_granted | role_read_all_with_admin | f | postgres + pg_read_all_data | role_granted | role_read_all_with_admin | f | + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_3 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_3 | role_admin | f | db_3 + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(15 rows) + +-- Should clean up the membership table when dropping a database +\connect postgres role_admin +DROP DATABASE db_3; +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+--------------------------+--------------+----------- + pg_read_all_data | role_granted | role_read_all_with_admin | f | db_1 + pg_read_all_data | role_granted | role_read_34 | f | db_4 + pg_read_all_data | role_granted | role_read_all_with_admin | f | postgres + pg_read_all_data | role_granted | role_read_all_with_admin | f | + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_34 | role_admin | t | db_4 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_34 | role_inherited_34 | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(12 rows) + +-- Should clean up the membership table when dropping a role +DROP ROLE role_read_34; +SELECT * FROM check_memberships(); + role | member | grantor | admin_option | datname +--------------------------+--------------------------+--------------------------+--------------+----------- + pg_read_all_data | role_granted | role_read_all_with_admin | f | db_1 + pg_read_all_data | role_granted | | f | db_4 + pg_read_all_data | role_granted | role_read_all_with_admin | f | postgres + pg_read_all_data | role_granted | role_read_all_with_admin | f | + pg_read_all_data | role_read_12 | role_admin | f | db_1 + pg_read_all_data | role_read_12 | role_admin | f | db_2 + pg_read_all_data | role_read_all_noinherit | role_admin | f | template1 + pg_read_all_data | role_read_all_with_admin | role_admin | t | + role_read_12 | role_read_12_noinherit | role_admin | f | + role_read_all_with_admin | role_read_all_noinherit | role_admin | f | +(10 rows) + diff --git a/src/test/modules/unsafe_tests/sql/role_membership.sql b/src/test/modules/unsafe_tests/sql/role_membership.sql new file mode 100644 index 000000000000..c6cfde2ddcff --- /dev/null +++ b/src/test/modules/unsafe_tests/sql/role_membership.sql @@ -0,0 +1,296 @@ +-- +-- Tests for database-specific role memberships. +-- This is unsafe because roles and databases will added / removed / modified. +-- + +CREATE ROLE role_admin LOGIN SUPERUSER; + +\connect postgres role_admin + +CREATE FUNCTION check_memberships() + RETURNS TABLE (role name, member name, grantor name, admin_option boolean, datname name) + AS $$ +SELECT + r.rolname as role, + m.rolname as member, + g.rolname as grantor, + admin_option, + d.datname +FROM pg_auth_members a +LEFT JOIN pg_roles r ON r.oid = a.roleid +LEFT JOIN pg_roles m ON m.oid = a.member +LEFT JOIN pg_roles g ON g.oid = a.grantor +LEFT JOIN pg_database d ON d.oid = a.dbid +WHERE + m.rolname LIKE 'role_%' +ORDER BY + 1, 2, 5 +$$ LANGUAGE SQL; + +-- Populate test databases +\connect template1 +CREATE TABLE data AS SELECT generate_series(1, 3); + +CREATE DATABASE db_1; +CREATE DATABASE db_2; +CREATE DATABASE db_3; +CREATE DATABASE db_4; + +-- Read all cluster-wide with admin option +CREATE ROLE role_read_all_with_admin; +GRANT pg_read_all_data TO role_read_all_with_admin WITH ADMIN OPTION; + +-- Read all in databases 1 and 2 +CREATE ROLE role_read_12; +GRANT pg_read_all_data TO role_read_12 IN DATABASE db_1; +GRANT pg_read_all_data TO role_read_12 IN DATABASE db_2; + +-- Read all in databases 3 and 4 with admin option +CREATE ROLE role_read_34; +GRANT pg_read_all_data TO role_read_34 IN DATABASE db_3 WITH ADMIN OPTION; +GRANT pg_read_all_data TO role_read_34 IN DATABASE db_4 WITH ADMIN OPTION; + +-- Inherits read all in databases 3 and 4 +CREATE ROLE role_inherited_34; +GRANT role_read_34 TO role_inherited_34; + +-- Inherits read all in database 3 +CREATE ROLE role_inherited_3; +GRANT role_read_34 TO role_inherited_3 IN DATABASE db_3; + +-- No inherit +CREATE ROLE role_read_all_noinherit NOINHERIT; +GRANT role_read_all_with_admin TO role_read_all_noinherit; + +-- No inherit in databases 1 and 2 +CREATE ROLE role_read_12_noinherit NOINHERIT; +GRANT role_read_12 TO role_read_12_noinherit; + +-- Alternate syntax +CREATE ROLE role_read_template1; +GRANT pg_read_all_data TO role_read_template1, role_read_all_noinherit IN CURRENT DATABASE; + +-- Failure due to missing database +GRANT pg_read_all_data TO role_read_template1 IN DATABASE non_existent; -- error + +-- Should warn on duplicate grants +GRANT pg_read_all_data TO role_read_all_with_admin; -- notice +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1; -- notice + +-- Should not warn if adjusting admin option +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- silent +GRANT pg_read_all_data TO role_read_template1 IN DATABASE template1 WITH ADMIN OPTION; -- notice + +-- Check membership table +\connect postgres role_admin +SELECT * FROM check_memberships(); + +-- Test membership privileges (db_1) +\connect db_1 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success +SET ROLE role_read_12; +SELECT * FROM data; -- success +SET ROLE role_read_34; +SELECT * FROM data; -- error +SET ROLE role_inherited_34; +SELECT * FROM data; -- error +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error + +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- success + +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- error +SET ROLE role_read_34; -- success + +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +SET ROLE role_read_34; -- error + +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12; -- success +SELECT * FROM data; -- success + +-- Test membership privileges (db_2) +\connect db_2 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success +SET ROLE role_read_12; +SELECT * FROM data; -- success +SET ROLE role_read_34; +SELECT * FROM data; -- error +SET ROLE role_inherited_34; +SELECT * FROM data; -- error +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error + +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- success + +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- error +SET ROLE role_read_34; -- success + +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +SET ROLE role_read_34; -- error + +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12; -- success +SELECT * FROM data; -- success + +-- Test membership privileges (db_3) +\connect db_3 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success +SET ROLE role_read_12; +SELECT * FROM data; -- error +SET ROLE role_read_34; +SELECT * FROM data; -- success +SET ROLE role_inherited_34; +SELECT * FROM data; -- success +SET ROLE role_inherited_3; +SELECT * FROM data; -- success +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error + +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- error + +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success + +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success + +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12; -- error +SELECT * FROM data; -- error + +-- Test membership privileges (db_4) +\connect db_4 +SET ROLE role_read_all_with_admin; +SELECT * FROM data; -- success +SET ROLE role_read_12; +SELECT * FROM data; -- error +SET ROLE role_read_34; +SELECT * FROM data; -- success +SET ROLE role_inherited_34; +SELECT * FROM data; -- success +SET ROLE role_inherited_3; +SELECT * FROM data; -- error +SET ROLE role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12_noinherit; +SELECT * FROM data; -- error + +SET SESSION AUTHORIZATION role_read_12; +SET ROLE pg_read_all_data; -- error + +SET SESSION AUTHORIZATION role_inherited_34; +SET ROLE pg_read_all_data; -- success +SET ROLE role_read_34; -- success + +SET SESSION AUTHORIZATION role_inherited_3; +SET ROLE pg_read_all_data; -- error +SET ROLE role_read_34; -- error + +SET SESSION AUTHORIZATION role_read_all_noinherit; +SELECT * FROM data; -- error +SET ROLE pg_read_all_data; -- success +SELECT * FROM data; -- success + +SET SESSION AUTHORIZATION role_read_12_noinherit; +SELECT * FROM data; -- error +SET ROLE role_read_12; -- error +SELECT * FROM data; -- error + +\connect postgres role_admin + +-- Should not warn if revoking admin option +REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent +REVOKE ADMIN OPTION FOR pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- silent +SELECT * FROM check_memberships(); + +-- Should warn if revoking a non-existent membership +REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- success +REVOKE pg_read_all_data FROM role_read_template1 IN DATABASE template1; -- warning +SELECT * FROM check_memberships(); + +-- Revoke should only apply to the specified level +REVOKE pg_read_all_data FROM role_read_12; -- warning +SELECT * FROM check_memberships(); + +-- Ensure cluster-wide admin option can grant cluster-wide and in specific databases +CREATE ROLE role_granted; +SET SESSION AUTHORIZATION role_read_all_with_admin; +GRANT pg_read_all_data TO role_granted; -- success +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_1; -- success +GRANT role_read_34 TO role_granted; -- error +SELECT * FROM check_memberships(); + +-- Ensure database-specific admin option can only grant within that database +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- error +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error + +\connect db_3 +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- notice +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- error + +\connect db_4 +SET SESSION AUTHORIZATION role_read_34; +GRANT pg_read_all_data TO role_granted; -- error +GRANT pg_read_all_data TO role_granted IN CURRENT DATABASE; -- success +GRANT pg_read_all_data TO role_granted IN DATABASE db_3; -- error +GRANT pg_read_all_data TO role_granted IN DATABASE db_4; -- notice + +\connect postgres role_admin +SELECT * FROM check_memberships(); + +-- Should clean up the membership table when dropping a database +\connect postgres role_admin +DROP DATABASE db_3; +SELECT * FROM check_memberships(); + +-- Should clean up the membership table when dropping a role +DROP ROLE role_read_34; +SELECT * FROM check_memberships(); diff --git a/src/test/regress/expected/oidjoins.out b/src/test/regress/expected/oidjoins.out index 215eb899be3e..79fb69059b68 100644 --- a/src/test/regress/expected/oidjoins.out +++ b/src/test/regress/expected/oidjoins.out @@ -197,6 +197,7 @@ NOTICE: checking pg_tablespace {spcowner} => pg_authid {oid} NOTICE: checking pg_auth_members {roleid} => pg_authid {oid} NOTICE: checking pg_auth_members {member} => pg_authid {oid} NOTICE: checking pg_auth_members {grantor} => pg_authid {oid} +NOTICE: checking pg_auth_members {dbid} => pg_database {oid} NOTICE: checking pg_shdepend {dbid} => pg_database {oid} NOTICE: checking pg_shdepend {classid} => pg_class {oid} NOTICE: checking pg_shdepend {refclassid} => pg_class {oid}