This is an automated email from the ASF dual-hosted git repository.

leborchuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry.git


The following commit(s) were added to refs/heads/main by this push:
     new eaf8e2569bd Add predefined role pg_manage_resource_groups (#1795)
eaf8e2569bd is described below

commit eaf8e2569bd30691bf84ee78718e5154ecd3c5fd
Author: Alena Rybakina <[email protected]>
AuthorDate: Thu Aug 27 18:16:19 2026 +0300

    Add predefined role pg_manage_resource_groups (#1795)
    
    In Cloudberry/Greenplum only a superuser can manage resource groups.
    In managed-service deployments superuser cannot be granted to the
    client, so the cloud admin has no way to tune their own CPU and
    memory limits.
    
    Add a predefined role pg_manage_resource_groups (OID 6300) in
    pg_authid.dat, following the upstream convention used by pg_monitor
    and pg_read_all_data. Members of this role can CREATE, ALTER and DROP
    resource groups and call pg_resgroup_move_query() without being a
    superuser. Delegation is done with standard SQL:
    
        GRANT pg_manage_resource_groups TO <user>;
    
    The regression and isolation2 tests exercise the new role via GRANT.
    The basic CREATE/ALTER/DROP test cases in the regression test are
    adapted from open-gpdb/gpdb commit 3ac99962ad2.
    
    Co-authored-by: Andrey Borodin <[email protected]>
---
 doc/src/sgml/user-manag.sgml                       |  21 ++++
 src/backend/commands/resgroupcmds.c                |  49 ++++++--
 src/backend/utils/resgroup/resgroup_helper.c       |  11 +-
 src/include/catalog/pg_authid.dat                  |   5 +
 .../expected/resgroup/resgroup_mdb_admin.out       | 123 ++++++++++++++++++
 .../resgroup_pg_manage_resource_groups.out         | 137 +++++++++++++++++++++
 .../isolation2/isolation2_resgroup_v1_schedule     |   1 +
 .../isolation2/isolation2_resgroup_v2_schedule     |   1 +
 .../isolation2/sql/resgroup/resgroup_mdb_admin.sql |  89 +++++++++++++
 .../resgroup_pg_manage_resource_groups.sql         |  93 ++++++++++++++
 src/test/regress/expected/resource_group.out       |  94 +++++++++++++-
 src/test/regress/sql/resource_group.sql            |  68 +++++++++-
 12 files changed, 674 insertions(+), 18 deletions(-)

diff --git a/doc/src/sgml/user-manag.sgml b/doc/src/sgml/user-manag.sgml
index 1f7d7e75ce9..46fb9679dd9 100644
--- a/doc/src/sgml/user-manag.sgml
+++ b/doc/src/sgml/user-manag.sgml
@@ -693,6 +693,16 @@ DROP ROLE doomed_role;
        database to issue
        <link linkend="sql-createsubscription"><command>CREATE 
SUBSCRIPTION</command></link>.</entry>
       </row>
+      <row>
+       <entry>pg_manage_resource_groups</entry>
+       <entry>Allow
+       <link 
linkend="sql-createresourcegroup"><command>CREATE</command></link>,
+       <link linkend="sql-alterresourcegroup"><command>ALTER</command></link> 
and
+       <link linkend="sql-dropresourcegroup"><command>DROP RESOURCE 
GROUP</command></link>,
+       and execution of <function>pg_resgroup_move_query()</function>, normally
+       restricted to superusers. The system <literal>admin_group</literal> can
+       still only be altered or dropped by a superuser.</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
@@ -727,6 +737,17 @@ DROP ROLE doomed_role;
   superuser.  See <xref linkend="functions-admin-signal"/>.
   </para>
 
+  <para>
+  The <literal>pg_manage_resource_groups</literal> role is intended to allow
+  trusted, non-superuser administrators (for example, the database owner in
+  managed deployments) to define and tune resource groups without holding
+  full superuser privileges. Members of this role can create, alter and drop
+  resource groups, and move running queries between groups with
+  <function>pg_resgroup_move_query()</function>. The system
+  <literal>admin_group</literal> is reserved for the cluster's control plane
+  and can only be altered or dropped by a real superuser.
+  </para>
+
   <para>
   The <literal>pg_read_server_files</literal>, 
<literal>pg_write_server_files</literal> and
   <literal>pg_execute_server_program</literal> roles are intended to allow 
administrators to have
diff --git a/src/backend/commands/resgroupcmds.c 
b/src/backend/commands/resgroupcmds.c
index 98545870244..af6feaf25dd 100644
--- a/src/backend/commands/resgroupcmds.c
+++ b/src/backend/commands/resgroupcmds.c
@@ -34,6 +34,7 @@
 #include "commands/resgroupcmds.h"
 #include "miscadmin.h"
 #include "nodes/pg_list.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datetime.h"
 #include "utils/fmgroids.h"
@@ -103,11 +104,15 @@ CreateResourceGroup(CreateResourceGroupStmt *stmt)
        int                     nResGroups;
        MemoryContext oldContext;
 
-       /* Permission check - only superuser can create groups. */
-       if (!superuser())
+       /*
+        * Permission check - superusers and members of the predefined role
+        * pg_manage_resource_groups can create resource groups.
+        */
+       if (!has_privs_of_role(GetUserId(), ROLE_PG_MANAGE_RESOURCE_GROUPS))
                ereport(ERROR,
                                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-                                errmsg("must be superuser to create resource 
groups")));
+                                errmsg("permission denied to create resource 
group"),
+                                errhint("Must be superuser or have privileges 
of the pg_manage_resource_groups role.")));
 
        /*
         * Check for an illegal name ('none' is used to signify no group in 
ALTER ROLE).
@@ -269,11 +274,15 @@ DropResourceGroup(DropResourceGroupStmt *stmt)
        Oid                      groupid;
        ResourceGroupCallbackContext    *callbackCtx;
 
-       /* Permission check - only superuser can drop resource groups. */
-       if (!superuser())
+       /*
+        * Permission check - superusers and members of the predefined role
+        * pg_manage_resource_groups can drop resource groups.
+        */
+       if (!has_privs_of_role(GetUserId(), ROLE_PG_MANAGE_RESOURCE_GROUPS))
                ereport(ERROR,
                                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-                                errmsg("must be superuser to drop resource 
groups")));
+                                errmsg("permission denied to drop resource 
group \"%s\"", stmt->name),
+                                errhint("Must be superuser or have privileges 
of the pg_manage_resource_groups role.")));
 
        /*
         * Check the pg_resgroup relation to be certain the resource group 
already
@@ -375,11 +384,15 @@ AlterResourceGroup(AlterResourceGroupStmt *stmt)
        ResourceGroupCallbackContext    *callbackCtx;
        MemoryContext oldContext;
 
-       /* Permission check - only superuser can alter resource groups. */
-       if (!superuser())
+       /*
+        * Permission check - superusers and members of the predefined role
+        * pg_manage_resource_groups can alter resource groups.
+        */
+       if (!has_privs_of_role(GetUserId(), ROLE_PG_MANAGE_RESOURCE_GROUPS))
                ereport(ERROR,
                                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-                                errmsg("must be superuser to alter resource 
groups")));
+                                errmsg("permission denied to alter resource 
group \"%s\"", stmt->name),
+                                errhint("Must be superuser or have privileges 
of the pg_manage_resource_groups role.")));
 
        /* Currently we only support to ALTER one limit at one time */
        Assert(list_length(stmt->options) == 1);
@@ -412,6 +425,18 @@ AlterResourceGroup(AlterResourceGroupStmt *stmt)
         */
        groupid = get_resgroup_oid(stmt->name, false);
 
+       /*
+        * The built-in system resource groups may only be altered by a real
+        * superuser, never by a member of pg_manage_resource_groups
+        */
+       if (!superuser() &&
+               (groupid == ADMINRESGROUP_OID ||
+                groupid == SYSTEMRESGROUP_OID))
+               ereport(ERROR,
+                               (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                                errmsg("permission denied to alter resource 
group \"%s\"", stmt->name),
+                                errhint("Must be superuser to alter a system 
resource group.")));
+
        if (limitType == RESGROUP_LIMIT_TYPE_CONCURRENCY &&
                value == 0 &&
                groupid == ADMINRESGROUP_OID)
@@ -500,7 +525,7 @@ AlterResourceGroup(AlterResourceGroupStmt *stmt)
                                                                          
RESGROUP_DEFAULT_CPU_WEIGHT, "");
 
                updateResgroupCapabilityEntry(pg_resgroupcapability_rel,
-                                                                         
groupid, RESGROUP_LIMIT_TYPE_CPUSET, 
+                                                                         
groupid, RESGROUP_LIMIT_TYPE_CPUSET,
                                                                          0, 
caps.cpuset);
        }
        else if (limitType == RESGROUP_LIMIT_TYPE_CPU)
@@ -1007,7 +1032,7 @@ parseStmtOptions(CreateResourceGroupStmt *stmt, 
ResGroupCaps *caps)
                else
                        mask |= 1 << type;
 
-               if (type == RESGROUP_LIMIT_TYPE_CPUSET) 
+               if (type == RESGROUP_LIMIT_TYPE_CPUSET)
                {
                        const char *cpuset = defGetString(defel);
                        strlcpy(caps->cpuset, cpuset, sizeof(caps->cpuset));
@@ -1611,7 +1636,7 @@ checkCpuSetByRole(const char *cpuset)
  * ex:
  * cpuset = "1;4"
  * then we should assign '1' to corrdinator and '4' to segment
- * 
+ *
  * cpuset = "1"
  * assign '1' to both coordinator and segment
  */
diff --git a/src/backend/utils/resgroup/resgroup_helper.c 
b/src/backend/utils/resgroup/resgroup_helper.c
index 00aaded168d..319d94e4f2b 100644
--- a/src/backend/utils/resgroup/resgroup_helper.c
+++ b/src/backend/utils/resgroup/resgroup_helper.c
@@ -15,12 +15,14 @@
 #include "funcapi.h"
 #include "libpq-fe.h"
 #include "miscadmin.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_resgroup.h"
 #include "cdb/cdbdisp_query.h"
 #include "cdb/cdbdispatchresult.h"
 #include "cdb/cdbvars.h"
 #include "commands/resgroupcmds.h"
 #include "storage/procarray.h"
+#include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/datetime.h"
 #include "utils/resgroup.h"
@@ -464,10 +466,15 @@ pg_resgroup_move_query(PG_FUNCTION_ARGS)
                                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
                                                (errmsg("resource group is not 
enabled"))));
 
-       if (!superuser())
+       /*
+        * Superusers and members of the predefined role
+        * pg_manage_resource_groups can move a query between resource groups.
+        */
+       if (!has_privs_of_role(GetUserId(), ROLE_PG_MANAGE_RESOURCE_GROUPS))
                ereport(ERROR,
                                (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-                                               (errmsg("must be superuser to 
move query"))));
+                                errmsg("permission denied to move query 
between resource groups"),
+                                errhint("Must be superuser or have privileges 
of the pg_manage_resource_groups role.")));
 
        if (Gp_role == GP_ROLE_DISPATCH)
        {
diff --git a/src/include/catalog/pg_authid.dat 
b/src/include/catalog/pg_authid.dat
index 6b4a0aaaad9..3c0ee0d1d10 100644
--- a/src/include/catalog/pg_authid.dat
+++ b/src/include/catalog/pg_authid.dat
@@ -89,6 +89,11 @@
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
   rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
   rolpassword => '_null_', rolvaliduntil => '_null_' },
+{ oid => '6312', oid_symbol => 'ROLE_PG_MANAGE_RESOURCE_GROUPS',
+  rolname => 'pg_manage_resource_groups', rolsuper => 'f', rolinherit => 't',
+  rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
+  rolreplication => 'f', rolbypassrls => 'f', rolconnlimit => '-1',
+  rolpassword => '_null_', rolvaliduntil => '_null_' },
 { oid => '6304', oid_symbol => 'ROLE_PG_CREATE_SUBSCRIPTION',
   rolname => 'pg_create_subscription', rolsuper => 'f', rolinherit => 't',
   rolcreaterole => 'f', rolcreatedb => 'f', rolcanlogin => 'f',
diff --git a/src/test/isolation2/expected/resgroup/resgroup_mdb_admin.out 
b/src/test/isolation2/expected/resgroup/resgroup_mdb_admin.out
new file mode 100644
index 00000000000..d74f382821b
--- /dev/null
+++ b/src/test/isolation2/expected/resgroup/resgroup_mdb_admin.out
@@ -0,0 +1,123 @@
+-- Tests permission checks for the mdb_admin role with
+-- resource groups enabled.
+
+-- start_matchsubs
+-- m/ERROR:  cannot find process: \d+/
+-- s/\d+/XXX/g
+-- end_matchsubs
+
+DROP ROLE IF EXISTS role_rg_admin;
+DROP
+DROP ROLE IF EXISTS role_rg_noadmin;
+DROP
+DROP ROLE IF EXISTS mdb_admin;
+DROP
+-- start_ignore
+DROP RESOURCE GROUP rg_perm_admin1;
+DROP RESOURCE GROUP rg_perm_admin2;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_revoke2;
+DROP RESOURCE GROUP rg_perm_test;
+-- end_ignore
+
+-- ---------------------------------------------------------------------
+-- Setup.  The mdb_admin role is not predefined in the catalog; it is
+-- created here the same way the control plane provisions it at runtime.
+-- ---------------------------------------------------------------------
+CREATE RESOURCE GROUP rg_perm_test WITH (concurrency=2, cpu_max_percent=10);
+CREATE
+CREATE ROLE mdb_admin;
+CREATE
+CREATE ROLE role_rg_admin RESOURCE GROUP rg_perm_test;
+CREATE
+CREATE ROLE role_rg_noadmin RESOURCE GROUP rg_perm_test;
+CREATE
+GRANT mdb_admin TO role_rg_admin;
+GRANT
+
+-- ---------------------------------------------------------------------
+-- 1. Member of mdb_admin can CREATE/ALTER/DROP resource groups
+--    (statements are dispatched to segments).
+-- ---------------------------------------------------------------------
+1: SET ROLE role_rg_admin;
+SET
+1: CREATE RESOURCE GROUP rg_perm_admin1 WITH (concurrency=1, 
cpu_max_percent=5);
+CREATE
+1: ALTER RESOURCE GROUP rg_perm_admin1 SET cpu_max_percent 6;
+ALTER
+1: DROP RESOURCE GROUP rg_perm_admin1;
+DROP
+
+-- 2. Even a member cannot ALTER or DROP the system admin_group.
+1: ALTER RESOURCE GROUP admin_group SET cpu_max_percent 99;
+ERROR:  must be superuser to alter resource group admin_group
+1: DROP RESOURCE GROUP admin_group;
+ERROR:  must be superuser to drop resource group admin_group
+1q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- 3. A non-member is rejected on every entry point.
+-- ---------------------------------------------------------------------
+2: SET ROLE role_rg_noadmin;
+SET
+2: CREATE RESOURCE GROUP rg_perm_admin2 WITH (concurrency=1, 
cpu_max_percent=5);
+ERROR:  must be mdb_admin to create resource groups
+2: ALTER RESOURCE GROUP rg_perm_test SET cpu_max_percent 7;
+ERROR:  must be mdb_admin to alter resource groups
+2: DROP RESOURCE GROUP rg_perm_test;
+ERROR:  must be mdb_admin to drop resource groups
+2q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- 4. pg_resgroup_move_query() honours the same permission check.
+--    The first call (non-member) must fail with "must be mdb_admin".
+--    The second call (member) gets past the permission gate and
+--    fails on the pid lookup (masked by start_matchsubs above).
+-- ---------------------------------------------------------------------
+3: SET ROLE role_rg_noadmin;
+SET
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+ERROR:  must be mdb_admin to move query
+3: RESET ROLE;
+RESET
+3: SET ROLE role_rg_admin;
+SET
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+ERROR:  cannot find process: XXX
+3q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- 5. Cross-session REVOKE takes effect on the granted session's
+--    next statement (the privilege is re-checked per command, not
+--    cached at SET ROLE time).
+-- ---------------------------------------------------------------------
+4: SET ROLE role_rg_admin;
+SET
+4: CREATE RESOURCE GROUP rg_perm_revoke1 WITH (concurrency=1, 
cpu_max_percent=5);
+CREATE
+5: REVOKE mdb_admin FROM role_rg_admin;
+REVOKE
+4: CREATE RESOURCE GROUP rg_perm_revoke2 WITH (concurrency=1, 
cpu_max_percent=5);
+ERROR:  must be mdb_admin to create resource groups
+4: DROP RESOURCE GROUP rg_perm_revoke1;
+ERROR:  must be mdb_admin to drop resource groups
+4q: ... <quitting>
+5q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- Cleanup. Roles must be dropped before the resource group they
+-- reference, otherwise DROP RESOURCE GROUP fails with
+-- "resource group is used by at least one role".
+-- ---------------------------------------------------------------------
+RESET ROLE;
+RESET
+DROP ROLE role_rg_admin;
+DROP
+DROP ROLE role_rg_noadmin;
+DROP
+DROP ROLE mdb_admin;
+DROP
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP
+DROP RESOURCE GROUP rg_perm_test;
+DROP
diff --git 
a/src/test/isolation2/expected/resgroup/resgroup_pg_manage_resource_groups.out 
b/src/test/isolation2/expected/resgroup/resgroup_pg_manage_resource_groups.out
new file mode 100644
index 00000000000..6d1ccd74afc
--- /dev/null
+++ 
b/src/test/isolation2/expected/resgroup/resgroup_pg_manage_resource_groups.out
@@ -0,0 +1,137 @@
+-- Tests permission checks for pg_manage_resource_groups with
+-- resource groups enabled.
+
+-- start_matchsubs
+-- m/ERROR:  cannot find process: \d+/
+-- s/\d+/XXX/g
+-- end_matchsubs
+
+DROP ROLE IF EXISTS role_rg_admin;
+DROP
+DROP ROLE IF EXISTS role_rg_noadmin;
+DROP
+-- start_ignore
+DROP RESOURCE GROUP rg_perm_admin1;
+DROP RESOURCE GROUP rg_perm_admin2;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_revoke2;
+DROP RESOURCE GROUP rg_perm_test;
+-- end_ignore
+
+-- ---------------------------------------------------------------------
+-- Setup.
+-- ---------------------------------------------------------------------
+CREATE RESOURCE GROUP rg_perm_test WITH (concurrency=2, cpu_max_percent=10);
+CREATE
+CREATE ROLE role_rg_admin RESOURCE GROUP rg_perm_test;
+CREATE
+CREATE ROLE role_rg_noadmin RESOURCE GROUP rg_perm_test;
+CREATE
+GRANT pg_manage_resource_groups TO role_rg_admin;
+GRANT
+
+-- The predefined role must be present after initdb.
+SELECT 1 AS role_exists FROM pg_roles WHERE rolname = 
'pg_manage_resource_groups';
+ role_exists 
+-------------
+ 1           
+(1 row)
+
+-- ---------------------------------------------------------------------
+-- 1. Member of pg_manage_resource_groups can CREATE/ALTER/DROP
+--    resource groups (statements are dispatched to segments).
+-- ---------------------------------------------------------------------
+1: SET ROLE role_rg_admin;
+SET
+1: CREATE RESOURCE GROUP rg_perm_admin1 WITH (concurrency=1, 
cpu_max_percent=5);
+CREATE
+1: ALTER RESOURCE GROUP rg_perm_admin1 SET cpu_max_percent 6;
+ALTER
+1: DROP RESOURCE GROUP rg_perm_admin1;
+DROP
+
+-- 2. Even a member cannot ALTER or DROP the system admin_group.
+1: ALTER RESOURCE GROUP admin_group SET cpu_max_percent 99;
+ERROR:  permission denied to alter resource group "admin_group"
+HINT:  Must be superuser to alter a system resource group.
+1: DROP RESOURCE GROUP admin_group;
+ERROR:  cannot drop default resource group "admin_group"
+1q: ... <quitting>
+
+-- 2b. A real superuser can still ALTER a system group (invariant preserved,
+--     value restored to its default afterwards).
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 9;
+ALTER
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 10;
+ALTER
+
+-- ---------------------------------------------------------------------
+-- 3. A non-member is rejected on every entry point.
+-- ---------------------------------------------------------------------
+2: SET ROLE role_rg_noadmin;
+SET
+2: CREATE RESOURCE GROUP rg_perm_admin2 WITH (concurrency=1, 
cpu_max_percent=5);
+ERROR:  permission denied to create resource group
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+2: ALTER RESOURCE GROUP rg_perm_test SET cpu_max_percent 7;
+ERROR:  permission denied to alter resource group "rg_perm_test"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+2: DROP RESOURCE GROUP rg_perm_test;
+ERROR:  permission denied to drop resource group "rg_perm_test"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+2q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- 4. pg_resgroup_move_query() honours the same permission check.
+--    The first call (non-member) must fail with "permission denied".
+--    The second call (member) gets past the permission gate and
+--    fails on the pid lookup (masked by start_matchsubs above).
+-- ---------------------------------------------------------------------
+3: SET ROLE role_rg_noadmin;
+SET
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+ERROR:  permission denied to move query between resource groups
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+3: RESET ROLE;
+RESET
+3: SET ROLE role_rg_admin;
+SET
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+ERROR:  cannot find process: XXX
+3q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- 5. Cross-session REVOKE takes effect on the granted session's
+--    next statement (the privilege is re-checked per command, not
+--    cached at SET ROLE time).
+-- ---------------------------------------------------------------------
+4: SET ROLE role_rg_admin;
+SET
+4: CREATE RESOURCE GROUP rg_perm_revoke1 WITH (concurrency=1, 
cpu_max_percent=5);
+CREATE
+5: REVOKE pg_manage_resource_groups FROM role_rg_admin;
+REVOKE
+4: CREATE RESOURCE GROUP rg_perm_revoke2 WITH (concurrency=1, 
cpu_max_percent=5);
+ERROR:  permission denied to create resource group
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+4: DROP RESOURCE GROUP rg_perm_revoke1;
+ERROR:  permission denied to drop resource group "rg_perm_revoke1"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+4q: ... <quitting>
+5q: ... <quitting>
+
+-- ---------------------------------------------------------------------
+-- Cleanup. Roles must be dropped before the resource group they
+-- reference, otherwise DROP RESOURCE GROUP fails with
+-- "resource group is used by at least one role".
+-- ---------------------------------------------------------------------
+RESET ROLE;
+RESET
+DROP ROLE role_rg_admin;
+DROP
+DROP ROLE role_rg_noadmin;
+DROP
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP
+DROP RESOURCE GROUP rg_perm_test;
+DROP
diff --git a/src/test/isolation2/isolation2_resgroup_v1_schedule 
b/src/test/isolation2/isolation2_resgroup_v1_schedule
index f5dea2d4012..756d46df1e0 100644
--- a/src/test/isolation2/isolation2_resgroup_v1_schedule
+++ b/src/test/isolation2/isolation2_resgroup_v1_schedule
@@ -34,6 +34,7 @@ test: resgroup/resgroup_move_query
 # regression tests
 test: resgroup/resgroup_recreate
 test: resgroup/resgroup_functions
+test: resgroup/resgroup_pg_manage_resource_groups
 
 # dump info
 test: resgroup/resgroup_dumpinfo
diff --git a/src/test/isolation2/isolation2_resgroup_v2_schedule 
b/src/test/isolation2/isolation2_resgroup_v2_schedule
index 0190efcd3d8..252d9deafcf 100644
--- a/src/test/isolation2/isolation2_resgroup_v2_schedule
+++ b/src/test/isolation2/isolation2_resgroup_v2_schedule
@@ -35,6 +35,7 @@ test: resgroup/resgroup_io_limit
 # regression tests
 test: resgroup/resgroup_recreate
 test: resgroup/resgroup_functions
+test: resgroup/resgroup_pg_manage_resource_groups
 
 # parallel tests
 #test: resgroup/restore_default_resgroup
diff --git a/src/test/isolation2/sql/resgroup/resgroup_mdb_admin.sql 
b/src/test/isolation2/sql/resgroup/resgroup_mdb_admin.sql
new file mode 100644
index 00000000000..3eedba5bc4e
--- /dev/null
+++ b/src/test/isolation2/sql/resgroup/resgroup_mdb_admin.sql
@@ -0,0 +1,89 @@
+-- Tests permission checks for the mdb_admin role with
+-- resource groups enabled.
+
+-- start_matchsubs
+-- m/ERROR:  cannot find process: \d+/
+-- s/\d+/XXX/g
+-- end_matchsubs
+
+DROP ROLE IF EXISTS role_rg_admin;
+DROP ROLE IF EXISTS role_rg_noadmin;
+DROP ROLE IF EXISTS mdb_admin;
+-- start_ignore
+DROP RESOURCE GROUP rg_perm_admin1;
+DROP RESOURCE GROUP rg_perm_admin2;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_revoke2;
+DROP RESOURCE GROUP rg_perm_test;
+-- end_ignore
+
+-- ---------------------------------------------------------------------
+-- Setup.  The mdb_admin role is not predefined in the catalog; it is
+-- created here the same way the control plane provisions it at runtime.
+-- ---------------------------------------------------------------------
+CREATE RESOURCE GROUP rg_perm_test WITH (concurrency=2, cpu_max_percent=10);
+CREATE ROLE mdb_admin;
+CREATE ROLE role_rg_admin RESOURCE GROUP rg_perm_test;
+CREATE ROLE role_rg_noadmin RESOURCE GROUP rg_perm_test;
+GRANT mdb_admin TO role_rg_admin;
+
+-- ---------------------------------------------------------------------
+-- 1. Member of mdb_admin can CREATE/ALTER/DROP resource groups
+--    (statements are dispatched to segments).
+-- ---------------------------------------------------------------------
+1: SET ROLE role_rg_admin;
+1: CREATE RESOURCE GROUP rg_perm_admin1 WITH (concurrency=1, 
cpu_max_percent=5);
+1: ALTER RESOURCE GROUP rg_perm_admin1 SET cpu_max_percent 6;
+1: DROP RESOURCE GROUP rg_perm_admin1;
+
+-- 2. Even a member cannot ALTER or DROP the system admin_group.
+1: ALTER RESOURCE GROUP admin_group SET cpu_max_percent 99;
+1: DROP RESOURCE GROUP admin_group;
+1q:
+
+-- ---------------------------------------------------------------------
+-- 3. A non-member is rejected on every entry point.
+-- ---------------------------------------------------------------------
+2: SET ROLE role_rg_noadmin;
+2: CREATE RESOURCE GROUP rg_perm_admin2 WITH (concurrency=1, 
cpu_max_percent=5);
+2: ALTER RESOURCE GROUP rg_perm_test SET cpu_max_percent 7;
+2: DROP RESOURCE GROUP rg_perm_test;
+2q:
+
+-- ---------------------------------------------------------------------
+-- 4. pg_resgroup_move_query() honours the same permission check.
+--    The first call (non-member) must fail with "must be mdb_admin".
+--    The second call (member) gets past the permission gate and
+--    fails on the pid lookup (masked by start_matchsubs above).
+-- ---------------------------------------------------------------------
+3: SET ROLE role_rg_noadmin;
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+3: RESET ROLE;
+3: SET ROLE role_rg_admin;
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+3q:
+
+-- ---------------------------------------------------------------------
+-- 5. Cross-session REVOKE takes effect on the granted session's
+--    next statement (the privilege is re-checked per command, not
+--    cached at SET ROLE time).
+-- ---------------------------------------------------------------------
+4: SET ROLE role_rg_admin;
+4: CREATE RESOURCE GROUP rg_perm_revoke1 WITH (concurrency=1, 
cpu_max_percent=5);
+5: REVOKE mdb_admin FROM role_rg_admin;
+4: CREATE RESOURCE GROUP rg_perm_revoke2 WITH (concurrency=1, 
cpu_max_percent=5);
+4: DROP RESOURCE GROUP rg_perm_revoke1;
+4q:
+5q:
+
+-- ---------------------------------------------------------------------
+-- Cleanup. Roles must be dropped before the resource group they
+-- reference, otherwise DROP RESOURCE GROUP fails with
+-- "resource group is used by at least one role".
+-- ---------------------------------------------------------------------
+RESET ROLE;
+DROP ROLE role_rg_admin;
+DROP ROLE role_rg_noadmin;
+DROP ROLE mdb_admin;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_test;
diff --git 
a/src/test/isolation2/sql/resgroup/resgroup_pg_manage_resource_groups.sql 
b/src/test/isolation2/sql/resgroup/resgroup_pg_manage_resource_groups.sql
new file mode 100644
index 00000000000..e0347610fbf
--- /dev/null
+++ b/src/test/isolation2/sql/resgroup/resgroup_pg_manage_resource_groups.sql
@@ -0,0 +1,93 @@
+-- Tests permission checks for pg_manage_resource_groups with
+-- resource groups enabled.
+
+-- start_matchsubs
+-- m/ERROR:  cannot find process: \d+/
+-- s/\d+/XXX/g
+-- end_matchsubs
+
+DROP ROLE IF EXISTS role_rg_admin;
+DROP ROLE IF EXISTS role_rg_noadmin;
+-- start_ignore
+DROP RESOURCE GROUP rg_perm_admin1;
+DROP RESOURCE GROUP rg_perm_admin2;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_revoke2;
+DROP RESOURCE GROUP rg_perm_test;
+-- end_ignore
+
+-- ---------------------------------------------------------------------
+-- Setup.
+-- ---------------------------------------------------------------------
+CREATE RESOURCE GROUP rg_perm_test WITH (concurrency=2, cpu_max_percent=10);
+CREATE ROLE role_rg_admin RESOURCE GROUP rg_perm_test;
+CREATE ROLE role_rg_noadmin RESOURCE GROUP rg_perm_test;
+GRANT pg_manage_resource_groups TO role_rg_admin;
+
+-- The predefined role must be present after initdb.
+SELECT 1 AS role_exists FROM pg_roles WHERE rolname = 
'pg_manage_resource_groups';
+
+-- ---------------------------------------------------------------------
+-- 1. Member of pg_manage_resource_groups can CREATE/ALTER/DROP
+--    resource groups (statements are dispatched to segments).
+-- ---------------------------------------------------------------------
+1: SET ROLE role_rg_admin;
+1: CREATE RESOURCE GROUP rg_perm_admin1 WITH (concurrency=1, 
cpu_max_percent=5);
+1: ALTER RESOURCE GROUP rg_perm_admin1 SET cpu_max_percent 6;
+1: DROP RESOURCE GROUP rg_perm_admin1;
+
+-- 2. Even a member cannot ALTER or DROP the system admin_group.
+1: ALTER RESOURCE GROUP admin_group SET cpu_max_percent 99;
+1: DROP RESOURCE GROUP admin_group;
+1q:
+
+-- 2b. A real superuser can still ALTER a system group (invariant preserved,
+--     value restored to its default afterwards).
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 9;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 10;
+
+-- ---------------------------------------------------------------------
+-- 3. A non-member is rejected on every entry point.
+-- ---------------------------------------------------------------------
+2: SET ROLE role_rg_noadmin;
+2: CREATE RESOURCE GROUP rg_perm_admin2 WITH (concurrency=1, 
cpu_max_percent=5);
+2: ALTER RESOURCE GROUP rg_perm_test SET cpu_max_percent 7;
+2: DROP RESOURCE GROUP rg_perm_test;
+2q:
+
+-- ---------------------------------------------------------------------
+-- 4. pg_resgroup_move_query() honours the same permission check.
+--    The first call (non-member) must fail with "permission denied".
+--    The second call (member) gets past the permission gate and
+--    fails on the pid lookup (masked by start_matchsubs above).
+-- ---------------------------------------------------------------------
+3: SET ROLE role_rg_noadmin;
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+3: RESET ROLE;
+3: SET ROLE role_rg_admin;
+3: SELECT pg_resgroup_move_query(999999999, 'admin_group');
+3q:
+
+-- ---------------------------------------------------------------------
+-- 5. Cross-session REVOKE takes effect on the granted session's
+--    next statement (the privilege is re-checked per command, not
+--    cached at SET ROLE time).
+-- ---------------------------------------------------------------------
+4: SET ROLE role_rg_admin;
+4: CREATE RESOURCE GROUP rg_perm_revoke1 WITH (concurrency=1, 
cpu_max_percent=5);
+5: REVOKE pg_manage_resource_groups FROM role_rg_admin;
+4: CREATE RESOURCE GROUP rg_perm_revoke2 WITH (concurrency=1, 
cpu_max_percent=5);
+4: DROP RESOURCE GROUP rg_perm_revoke1;
+4q:
+5q:
+
+-- ---------------------------------------------------------------------
+-- Cleanup. Roles must be dropped before the resource group they
+-- reference, otherwise DROP RESOURCE GROUP fails with
+-- "resource group is used by at least one role".
+-- ---------------------------------------------------------------------
+RESET ROLE;
+DROP ROLE role_rg_admin;
+DROP ROLE role_rg_noadmin;
+DROP RESOURCE GROUP rg_perm_revoke1;
+DROP RESOURCE GROUP rg_perm_test;
diff --git a/src/test/regress/expected/resource_group.out 
b/src/test/regress/expected/resource_group.out
index 0bcf2b14474..655cd4bc0db 100644
--- a/src/test/regress/expected/resource_group.out
+++ b/src/test/regress/expected/resource_group.out
@@ -4,9 +4,9 @@
 -- this test creates resource group objects and roles associated with
 -- resource groups so pg_dumpall/pg_upgrade can dump those objects at
 -- the end of ICW.
--- 
+--
 -- NOTE: please always put this test in the end of this file and do not
--- drop them.
+-- drop the rg_dump_test* objects.
 -- start_ignore
 DROP ROLE IF EXISTS role_dump_test1;
 NOTICE:  role "role_dump_test1" does not exist, skipping
@@ -14,13 +14,33 @@ DROP ROLE IF EXISTS role_dump_test2;
 NOTICE:  role "role_dump_test2" does not exist, skipping
 DROP ROLE IF EXISTS role_dump_test3;
 NOTICE:  role "role_dump_test3" does not exist, skipping
+DROP ROLE IF EXISTS rg_admin_test;
+NOTICE:  role "rg_admin_test" does not exist, skipping
+DROP ROLE IF EXISTS rg_noadmin_test;
+NOTICE:  role "rg_noadmin_test" does not exist, skipping
 DROP RESOURCE GROUP rg_dump_test1;
 ERROR:  resource group "rg_dump_test1" does not exist
 DROP RESOURCE GROUP rg_dump_test2;
 ERROR:  resource group "rg_dump_test2" does not exist
 DROP RESOURCE GROUP rg_dump_test3;
 ERROR:  resource group "rg_dump_test3" does not exist
+DROP RESOURCE GROUP rg_admin_test1;
+ERROR:  resource group "rg_admin_test1" does not exist
+DROP RESOURCE GROUP rg_admin_test2;
+ERROR:  resource group "rg_admin_test2" does not exist
 -- end_ignore
+-- The predefined role must be present after initdb.
+SELECT 1 AS role_exists
+  FROM pg_roles
+ WHERE rolname = 'pg_manage_resource_groups';
+ role_exists 
+-------------
+           1
+(1 row)
+
+-- The predefined role is pinned and cannot be dropped, even by a superuser.
+DROP ROLE pg_manage_resource_groups;
+ERROR:  cannot drop role pg_manage_resource_groups because it is required by 
the database system
 CREATE RESOURCE GROUP rg_dump_test1 WITH (concurrency=2, cpu_max_percent=5);
 WARNING:  resource group is disabled
 HINT:  To enable set gp_resource_manager=group
@@ -42,3 +62,73 @@ CREATE ROLE role_dump_test3 RESOURCE GROUP rg_dump_test3;
 NOTICE:  resource queue required -- using default resource queue "pg_default"
 WARNING:  resource group is disabled
 HINT:  To enable set gp_resource_manager=group
+-- Test that members of the predefined role pg_manage_resource_groups
+-- can manage resource groups without superuser, but cannot touch
+-- the system admin_group.
+CREATE ROLE rg_admin_test RESOURCE GROUP rg_dump_test1;
+NOTICE:  resource queue required -- using default resource queue "pg_default"
+WARNING:  resource group is disabled
+HINT:  To enable set gp_resource_manager=group
+CREATE ROLE rg_noadmin_test RESOURCE GROUP rg_dump_test1;
+NOTICE:  resource queue required -- using default resource queue "pg_default"
+WARNING:  resource group is disabled
+HINT:  To enable set gp_resource_manager=group
+GRANT pg_manage_resource_groups TO rg_admin_test;
+SET ROLE rg_admin_test;
+CREATE RESOURCE GROUP rg_admin_test1 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+WARNING:  resource group is disabled
+HINT:  To enable set gp_resource_manager=group
+CREATE RESOURCE GROUP rg_admin_test2 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+WARNING:  resource group is disabled
+HINT:  To enable set gp_resource_manager=group
+ALTER RESOURCE GROUP rg_admin_test1 SET cpu_max_percent 2;
+DROP RESOURCE GROUP rg_admin_test1;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 2;
+ERROR:  permission denied to alter resource group "admin_group"
+HINT:  Must be superuser to alter a system resource group.
+DROP RESOURCE GROUP admin_group;
+ERROR:  cannot drop default resource group "admin_group"
+-- But a real superuser can still ALTER a system resource group (restored 
after).
+RESET ROLE;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 9;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 10;
+SET ROLE rg_noadmin_test;
+CREATE RESOURCE GROUP rg_admin_test1 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+ERROR:  permission denied to create resource group
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 2;
+ERROR:  permission denied to alter resource group "rg_admin_test2"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+DROP RESOURCE GROUP rg_admin_test2;
+ERROR:  permission denied to drop resource group "rg_admin_test2"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+DROP RESOURCE GROUP admin_group;
+ERROR:  permission denied to drop resource group "admin_group"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+-- After REVOKE the role loses its privileges immediately.
+RESET ROLE;
+REVOKE pg_manage_resource_groups FROM rg_admin_test;
+SET ROLE rg_admin_test;
+CREATE RESOURCE GROUP rg_admin_test3 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+ERROR:  permission denied to create resource group
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 3;
+ERROR:  permission denied to alter resource group "rg_admin_test2"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+DROP RESOURCE GROUP rg_admin_test2;
+ERROR:  permission denied to drop resource group "rg_admin_test2"
+HINT:  Must be superuser or have privileges of the pg_manage_resource_groups 
role.
+-- Transitive membership through an intermediate role must also work.
+RESET ROLE;
+CREATE ROLE rg_admin_grp;
+GRANT pg_manage_resource_groups TO rg_admin_grp;
+GRANT rg_admin_grp TO rg_admin_test;
+SET ROLE rg_admin_test;
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 4;
+DROP RESOURCE GROUP rg_admin_test2;
+RESET ROLE;
+REVOKE rg_admin_grp FROM rg_admin_test;
+REVOKE pg_manage_resource_groups FROM rg_admin_grp;
+DROP ROLE rg_admin_grp;
+DROP ROLE rg_admin_test;
+DROP ROLE rg_noadmin_test;
diff --git a/src/test/regress/sql/resource_group.sql 
b/src/test/regress/sql/resource_group.sql
index 1411eb0d61f..2795b43068c 100644
--- a/src/test/regress/sql/resource_group.sql
+++ b/src/test/regress/sql/resource_group.sql
@@ -4,20 +4,32 @@
 -- this test creates resource group objects and roles associated with
 -- resource groups so pg_dumpall/pg_upgrade can dump those objects at
 -- the end of ICW.
--- 
+--
 -- NOTE: please always put this test in the end of this file and do not
--- drop them.
+-- drop the rg_dump_test* objects.
 
 -- start_ignore
 DROP ROLE IF EXISTS role_dump_test1;
 DROP ROLE IF EXISTS role_dump_test2;
 DROP ROLE IF EXISTS role_dump_test3;
+DROP ROLE IF EXISTS rg_admin_test;
+DROP ROLE IF EXISTS rg_noadmin_test;
 
 DROP RESOURCE GROUP rg_dump_test1;
 DROP RESOURCE GROUP rg_dump_test2;
 DROP RESOURCE GROUP rg_dump_test3;
+DROP RESOURCE GROUP rg_admin_test1;
+DROP RESOURCE GROUP rg_admin_test2;
 -- end_ignore
 
+-- The predefined role must be present after initdb.
+SELECT 1 AS role_exists
+  FROM pg_roles
+ WHERE rolname = 'pg_manage_resource_groups';
+
+-- The predefined role is pinned and cannot be dropped, even by a superuser.
+DROP ROLE pg_manage_resource_groups;
+
 CREATE RESOURCE GROUP rg_dump_test1 WITH (concurrency=2, cpu_max_percent=5);
 CREATE RESOURCE GROUP rg_dump_test2 WITH (concurrency=2, cpu_max_percent=5);
 CREATE RESOURCE GROUP rg_dump_test3 WITH (concurrency=2, cpu_max_percent=5);
@@ -25,3 +37,55 @@ CREATE RESOURCE GROUP rg_dump_test3 WITH (concurrency=2, 
cpu_max_percent=5);
 CREATE ROLE role_dump_test1 RESOURCE GROUP rg_dump_test1;
 CREATE ROLE role_dump_test2 RESOURCE GROUP rg_dump_test2;
 CREATE ROLE role_dump_test3 RESOURCE GROUP rg_dump_test3;
+
+-- Test that members of the predefined role pg_manage_resource_groups
+-- can manage resource groups without superuser, but cannot touch
+-- the system admin_group.
+CREATE ROLE rg_admin_test RESOURCE GROUP rg_dump_test1;
+CREATE ROLE rg_noadmin_test RESOURCE GROUP rg_dump_test1;
+GRANT pg_manage_resource_groups TO rg_admin_test;
+
+SET ROLE rg_admin_test;
+
+CREATE RESOURCE GROUP rg_admin_test1 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+CREATE RESOURCE GROUP rg_admin_test2 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+ALTER RESOURCE GROUP rg_admin_test1 SET cpu_max_percent 2;
+DROP RESOURCE GROUP rg_admin_test1;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 2;
+DROP RESOURCE GROUP admin_group;
+
+-- But a real superuser can still ALTER a system resource group (restored 
after).
+RESET ROLE;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 9;
+ALTER RESOURCE GROUP admin_group SET cpu_max_percent 10;
+
+SET ROLE rg_noadmin_test;
+
+CREATE RESOURCE GROUP rg_admin_test1 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 2;
+DROP RESOURCE GROUP rg_admin_test2;
+DROP RESOURCE GROUP admin_group;
+
+-- After REVOKE the role loses its privileges immediately.
+RESET ROLE;
+REVOKE pg_manage_resource_groups FROM rg_admin_test;
+SET ROLE rg_admin_test;
+CREATE RESOURCE GROUP rg_admin_test3 WITH (concurrency=2, cpu_max_percent=5, 
memory_quota=5);
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 3;
+DROP RESOURCE GROUP rg_admin_test2;
+
+-- Transitive membership through an intermediate role must also work.
+RESET ROLE;
+CREATE ROLE rg_admin_grp;
+GRANT pg_manage_resource_groups TO rg_admin_grp;
+GRANT rg_admin_grp TO rg_admin_test;
+SET ROLE rg_admin_test;
+ALTER RESOURCE GROUP rg_admin_test2 SET cpu_max_percent 4;
+DROP RESOURCE GROUP rg_admin_test2;
+
+RESET ROLE;
+REVOKE rg_admin_grp FROM rg_admin_test;
+REVOKE pg_manage_resource_groups FROM rg_admin_grp;
+DROP ROLE rg_admin_grp;
+DROP ROLE rg_admin_test;
+DROP ROLE rg_noadmin_test;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to