From 7c4595b77c9caa09b5a043aeb322e1f9230cdba4 Mon Sep 17 00:00:00 2001 From: "David G. Johnston" Date: Thu, 8 Sep 2022 15:38:02 +0000 Subject: [PATCH] Implement psql meta-commands \dr[rgu][S] Role graphs, system views and meta-commands. --- doc/src/sgml/ref/psql-ref.sgml | 20 ++ doc/src/sgml/system-views.sgml | 261 +++++++++++++++++++++ src/backend/catalog/pg_role_graph.plpgsql | 274 ++++++++++++++++++++++ src/backend/catalog/system_views.sql | 259 ++++++++++++++++++++ src/bin/psql/command.c | 12 + src/bin/psql/describe.c | 108 +++++++++ src/bin/psql/describe.h | 9 + 7 files changed, 943 insertions(+) create mode 100644 src/backend/catalog/pg_role_graph.plpgsql diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml index 186f8c506a..6e24c67eb8 100644 --- a/doc/src/sgml/ref/psql-ref.sgml +++ b/doc/src/sgml/ref/psql-ref.sgml @@ -1863,6 +1863,26 @@ testdb=> + + \dr[rgu][S] [ pattern ] + + + Present role memberships, recursively. + + + The mnemonic for the base command is: describe the role-graph for (r)oles, (g)groups + , or (u)sers. The traditional S suffix controls whether the default (group) roles + (i.e., those prefixed with pg_) are displayed. + If pattern is specified, + only those roles whose names match the pattern are listed. + + + Please see the description for the pg_role_graph system view for details + regarding the interpretation of the information presented herein. + + + + \dRp[+] [ pattern ] diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 44aa70a031..3887bebc2f 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -2464,6 +2464,267 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + <structname>pg_roles</structname> + + + pg_role_graph + + + + The view pg_role_graph provides, for each role in + the system, those roles it is a member of and those that are a member of it. + This includes both direct and indirect membership grants that can be traversed + via SET ROLE. To aid in comprehension of this graph of data it is divided + both along the memberof/member dimension and whether the role itself is a + user or group role, as determined by holding the explicit login attribute. + Additionally, WITH GRANT OPTION is handled via a separate Administration column. + + + + (While the concepts of users and groups have been + unified within the formal syntax grammar the two concepts remain valid + when it comes to the practical administration of a system. These views segregate + information by groups and users when appropriate to aid in comprehension. A group + is simply any role that is not a user, while a user is a role that explicitly has + has the login attribute.) + + + + The view presents, for a named role, grant information using a somewhat condensed coding scheme. + The coding scheme is the name of some other role in the system, quoted if necessary, and then either the word + "from" or "via". A "from" means that the grant in question has been directly recorded in the system catalog. + A "via" means the grant results from some chain of grants. In this situation there may be an optional bracketed + suffix (e.g., [+2]) indicating how many additional indirect grants are present. In both cases the grantor owning + the grant is reported. In the "from" case the comma-separated list of roles following the "from" are these grantors. + In the "via" case the grantor is separated from the other role with a forward slash. Additionally, if multiple + paths result in the same grant, the individual preceding paths will all be listed with newlines separating them. + + + + There is an Administration column that shows, including indirectly, WITH ADMIN OPTION grants. + Roles having this on the named role are denoted by a prefix of "by" combined with the grant specification + described above. A prefix of "of" is used if the named role is the one that is able to change the membership + of the role in the grant specification. This information is a duplicated subset of the grant specifications + recorded in the remaining four columns. + + + + The remaining four columns present the grant specifications related to the named role but divided among the + two possible options in the role type (Users, Groups) and direction (Member of, Member) dimensions. + The use of separate columns communicates the same information that "of" and "by" communicate in the + Administration column and so those prefixes are omitted here in the interest of space. + + + + +GOOD ENOUGH FOR REVIEW - WILL BE UPDATED List of role graphs +Role name | Administration | Member of Groups | Group Members | User Members | Member of Users +-----------+--------------------------------+-----------------------------+--------------------+------------------------+------------------- +usr1 | | grp1 from vagrant +| | usr1a from vagrant | + | | grp2 from usr2, vagrant | | | +usr1a | | "group 3" from vagrant +| | | usr1 from vagrant + | | grp1 via usr1/vagrant +| | | + | | grp2 via usr1/usr2 +| | | + | | usr1/vagrant | | | +usr5 | of grp5c via grp5b/vagrant[+1]+| grp5a from vagrant +| | | + | of grp5d via grp5c/vagrant[+2] | grp5b via grp5a/vagrant +| | | + | | grp5c via grp5b/vagrant[+1]+| | | + | | grp5d via grp5c/vagrant[+2] | | | +grp5b | of grp5c from vagrant +| grp5c from vagrant +| grp5a from vagrant | usr5 via grp5a/vagrant | + | of grp5d via grp5c/vagrant | grp5d via grp5c/vagrant | | | + + User usr1 has received membership in grp2 from both usr2 and vagrant; and user usr1a is (unusually) a member of it. + Since usr1a is a member of usr1 the two paths to membership in grp2 also apply to them. + grp5b is granted with admin option of grp5c by vagrant, thus all member roles of grp5b, like usr5, can assume this + capability, which extends indirectly to grp5d of which grp5c is a member. Thus usr5, a member of grp5b indirectly + via its direct membership in grp5a, is shown as administrator of grp5c and grp5d two and three steps removed respectively. + + + + The graph is built using the traversal logic in the pg_role_relationship system view. + + + + <structname>pg_role_graph</structname> Columns + + + + + Column Type + + + Description + + + + + + + + rolname name + + + Role name + + + + + + administration text + + + The listed roles can either be administered "by" this role, or this + is an administer "of" the listed roles. + + + + + + memberof_users text + + + User role that this role is a member of. + + + + + + memberof_groups text + + + Group roles that this role is a member of. + + + + + + member_users text + + + User roles that are members of this role. + + + + + + member_groups text + + + Group roles that are members of this role. + + + + + + role_type text + + + Report "Group" or "User" as appropriate for this role. + + + + + + oid oid + + + ID of role. + + + + + +
+
+ + + <structname>pg_role_relationship</structname> + + + pg_role_relationship + + + + The view pg_role_relationship produces one row + for every role reachable by another role. + + + + <structname>pg_role_relationship</structname> Columns + + + + + Column Type + + + Description + + + + + + + + leaf_node oid + + + The starting role for the upward-only (member of) graph traversal. + + + + + + group_node oid + + + The group role that our leaf role is a member of, possibly indirectly + + + + + + grantor oid + + + The role responsible for granting the direct grant this membership derives from. + + + + + + level int4 + + + The number of memberships traversed to get to this one. 1 means a direct grant. + 0 is the starting point where a role is treated as a member of itself. + + + + + + via oid[] + + + The inclusive path of role OIDs from the leaf_node to this group_node. + + + + + + got_auth bool + + + True if any of the grants in the via path are granted with admin option. + + + + + +
+
+ <structname>pg_roles</structname> diff --git a/src/backend/catalog/pg_role_graph.plpgsql b/src/backend/catalog/pg_role_graph.plpgsql new file mode 100644 index 0000000000..3c89991b6f --- /dev/null +++ b/src/backend/catalog/pg_role_graph.plpgsql @@ -0,0 +1,274 @@ +-- SQL code generator for the pg_role_graph and pg_role_relation system views recorded in system_views.sql +DROP VIEW IF EXISTS role_graph; +DROP VIEW IF EXISTS role_relationship; + +DO $$ +DECLARE + create_view_rr_part text; + role_relationship_cte text; + main_rr_select text; + + create_view_rg_part text; + role_graph_detail_cte text; + role_graph_join_template text; + role_graph text; + role_graph_lead text; + role_graph_foot text; + role_graph_template text; + role_graph_prefix text; + role_graph_having text; + role_graph_core text; + main_rg_select text; + leaf_col_name text := 'leaf_node'; + group_col_name text := 'group_node'; + col_member_user text := 'member_users'; + col_member_group text := 'member_groups'; + col_memberof_user text := 'memberof_users'; + col_memberof_group text := 'memberof_groups'; +BEGIN +create_view_rr_part := E'CREATE VIEW role_relationship AS\n'; + +role_relationship_cte := format(E' +WITH RECURSIVE cte_role_relationship AS ( + -- Select all known roles on the system; we consider every role as a potential + -- leaf node for a tree. Recursively ascend through the tree producing a + -- new row relating every group role encountered to the leaf role that we started from + SELECT + r.oid AS %1$s, -- leaf node + r.oid::oid AS %2$s, -- The leaf is a member of itself to kick-start tree ascent + NULL::oid AS grantor, -- But no one grants this implicit membership + 0 AS level, + ARRAY[]::oid[] AS via, + FALSE AS got_auth + FROM + pg_catalog.pg_roles AS r + UNION ALL -- The system prevents cycles from being created so no special logic required here to avoid infinite recursion + SELECT + a.%1$s, -- I am still the leaf + m.roleid AS %2$s, -- And groups that I am a member of now get evaluated as being potential members in their own right + m.grantor, -- who is responsible for this grant + a.level + 1, + a.via || m.roleid, -- add here to how I got here (TODO: don''t include the current node) + a.got_auth OR m.admin_option -- does this sub-tree enable me to get admin option and thus add others to it? + FROM + cte_role_relationship AS a + JOIN pg_catalog.pg_auth_members AS m ON (m.member = a.%2$s) -- The last iteration''s group node now becomes a member +)\n', leaf_col_name, group_col_name); + +main_rr_select := 'SELECT * FROM cte_role_relationship;'; + +EXECUTE create_view_rr_part || role_relationship_cte || main_rr_select; + +create_view_rg_part := E'CREATE VIEW role_graph AS\n'; + +role_graph_detail_cte := E'WITH role_graph_detail AS ('; +role_graph_detail_cte := role_graph_detail_cte || format(E' +SELECT + r.oid, + r.rolname, + CASE WHEN r.rolcanlogin THEN + ''User'' + ELSE + ''Group'' + END AS role_type, + %1$s, + %2$s, + %3$s, + %4$s, + r.rolsuper, + r.rolcreaterole +FROM + pg_catalog.pg_roles AS r +', col_memberof_user, col_memberof_group, col_member_user, col_member_group); + +role_graph_join_template := E' + JOIN LATERAL ( + -- Compute an array of distinct pg_role.oid values that a given leaf can reach. + -- We subdivide the result space into four quadrants + -- 1) Our leaf is a member whose containers are users + -- 2) Our leaf is a member whose containers are groups + -- 3) Our leaf is a container whose members are users + -- 4) Our leaf is a container whose members are groups + -- Input 1 is where our leaf oid resides, which depends on whether it is a member or container + -- Input 2 is the other thing + SELECT + array_agg(DISTINCT a.%2$s ORDER BY a.%2$s) -- output the other thing + FROM + role_relationship AS a + JOIN pg_catalog.pg_roles AS u ON u.oid = a.%2$s -- get the other thing''s rolcanlogin + AND %4$s u.rolcanlogin -- to decide whether it meets the users/group criteria for the quadrant + WHERE + a.%1$s = r.oid -- lateral result from the graph scoped to our leaf only + AND r.oid <> a.%2$s -- but exclude the implicit level 0 self-membership entry + ) AS %5$s (%3$s) ON TRUE'; + +role_graph_detail_cte := role_graph_detail_cte || format(role_graph_join_template, leaf_col_name, group_col_name, col_memberof_user, '', 'mou'); -- 1 +role_graph_detail_cte := role_graph_detail_cte || format(role_graph_join_template, leaf_col_name, group_col_name, col_memberof_group, 'not', 'mog'); -- 2 +role_graph_detail_cte := role_graph_detail_cte || format(role_graph_join_template, group_col_name, leaf_col_name, col_member_user, '', 'mu'); -- 3 +role_graph_detail_cte := role_graph_detail_cte || format(role_graph_join_template, group_col_name, leaf_col_name, col_member_group, 'not', 'mg'); -- 4 +role_graph_detail_cte := role_graph_detail_cte || E'\n)\n'; + +-- The next two fragments deal with considering the relationships strictly within the context +-- of whether with admin option is possible. The base query to compute the relationships +-- has different presentation requirements when use in this context and so these can +-- be appended to the in-query string builder in the administration case but left +-- as the empty string in the general case. +role_graph_prefix := E' + -- since the administration column considered both the thing holding with admin option + -- and the thing upon which with admin option is held we prefix the membership string + -- to indicate which direction the relationship is going. + CASE WHEN bool_or(grant_instance.got_auth) + THEN ''%1$s '' + ELSE '''' + END || +'; + +role_graph_having := E' +-- we only care about subtree relationships that result in a with admin option +-- being possible. +HAVING + bool_or(grant_instance.got_auth) +'; + +-- Now the general template that will compute a newline separated listing of membership relationships +-- including grantor information. +role_graph_template := E' +SELECT + -- placeholder for the optional administration prefix (with admin option) + %4$s + CASE WHEN cardinality(grant_instance.via) > 1 + -- Since we have got here via intermediate memberships we indicate that this membership is indirect + -- In the case of a single intermediate step we can simply show the indirect and enabling grant explicitly + -- In the case of multiple intermediate steps we indicate how many additional steps are required to get + -- to the enabling grant. + -- Multiple subtrees can introduce the same membership so we aggregate; specifically newline with padding. + THEN format(''%%I via %%s'', + other_role.rolname, + string_agg( + quote_ident(ancestor_role.rolname) || + E''/'' || -- used for consistency with object grant representation of grantor + quote_ident(grant_role.rolname) || + CASE WHEN grant_instance.level > 2 + THEN ''[+'' || grant_instance.level - 2 || '']'' + ELSE '''' + END, + E''\\n'' || repeat('' '', length(other_role.rolname) + 5) -- pad after the newline so each grantor starts at the same place + ) + ) + -- Direct grants are simply represented as the membership from grantor + -- Nothing comes before the membership as the column in which the link is presented determines + -- the relationship direction. + ELSE format(''%%I from %%s'', + other_role.rolname, + string_agg(quote_ident(grant_role.rolname), '', '') + ) + END +FROM + -- explode the unique arrays we just built, merge in some contextual data, then re-assemble as strings + unnest(leaf_role.%1$s) AS other + + JOIN pg_catalog.pg_roles AS other_role ON other_role.oid = other.oid + + -- Pick the row(s) from the graph based upon whether the leaf is supposed to be the container or member + + -- Each unnested row in other can match mulitple rows in role_relationship but basically + -- we want to report each other.oid once (newline separation) and aggregate any repeating here + -- separately (nested newlines with indents for other) + -- XXX: I think this nested description is right - but this query tries to combine them in a single group by... + JOIN role_relationship AS grant_instance ON grant_instance.%2$s = leaf_role.oid + AND grant_instance.%3$s = other.oid + + JOIN pg_catalog.pg_roles AS grant_role ON grant_role.oid = grant_instance.grantor + + -- Since the tip of the subtree (which is the rr row we are working on) is placed onto the via array the + -- second-to-last entry designates how we got here + -- If this doesn''t result in a match we just assume we got here via direct grant which triggers the first + -- case branch output. + LEFT JOIN pg_catalog.pg_roles AS ancestor_role ON ancestor_role.oid = grant_instance.via[cardinality(grant_instance.via) - 1] + +GROUP BY + other_role.rolname, -- Only show each membership once... + grant_instance.via -- XXX: not totally sure on this; also a bit concerned about loops. + +-- and we introduce a HAVING clause if we want to limit ourselves to only entries involving with admin option +%5$s +'; + +role_graph_lead := E' +, cte_role_graph AS ( +SELECT + leaf_role.oid, + leaf_role.role_type, + leaf_role.rolname, + leaf_role.rolsuper, +'; + +role_graph_core := format(E' +array_to_string(ARRAY( + SELECT + * + FROM ( + VALUES (''Superuser'')) vals (v) + WHERE + leaf_role.rolsuper + UNION ALL + SELECT + * + FROM ( + VALUES (''Create Role'')) vals (v) + WHERE + leaf_role.rolcreaterole + UNION ALL + %1$s + UNION ALL + %3$s + UNION ALL + %5$s + UNION ALL + %7$s +), E''\\n'') AS administration, +array_to_string(ARRAY(%2$s), E''\\n'') AS %9$s, +array_to_string(ARRAY(%4$s), E''\\n'') AS %10$s, +array_to_string(ARRAY(%6$s), E''\\n'') AS %11$s, +array_to_string(ARRAY(%8$s), E''\\n'') AS %12$s +', +format(role_graph_template, col_memberof_user, leaf_col_name, group_col_name, format(role_graph_prefix, 'of'), role_graph_having), +format(role_graph_template, col_memberof_user, leaf_col_name, group_col_name, '', ''), +format(role_graph_template, col_memberof_group, leaf_col_name, group_col_name, format(role_graph_prefix, 'of'), role_graph_having), +format(role_graph_template, col_memberof_group, leaf_col_name, group_col_name, '', ''), +format(role_graph_template, col_member_user, group_col_name, leaf_col_name, format(role_graph_prefix, 'by'), role_graph_having), +format(role_graph_template, col_member_user, group_col_name, leaf_col_name, '', ''), +format(role_graph_template, col_member_group, group_col_name, leaf_col_name, format(role_graph_prefix, 'by'), role_graph_having), +format(role_graph_template, col_member_group, group_col_name, leaf_col_name, '', ''), +col_memberof_user, +col_memberof_group, +col_member_user, +col_member_group +); + +role_graph_foot := E'FROM role_graph_detail AS leaf_role\n)\n'; + +role_graph := role_graph_lead || role_graph_core || role_graph_foot; + +main_rg_select := format(E' +SELECT + rolname, administration, %1$s, %2$s, %3$s, %4$s, + role_type, oid, + row_number() OVER (ORDER BY + role_type, + CASE WHEN rolsuper THEN oid::integer END ASC nulls LAST, + CASE WHEN rolname ~ ''pg_'' THEN 0 ELSE 1 END, + rolname) AS seq +FROM + cte_role_graph +ORDER BY + seq; +',col_memberof_user, col_memberof_group, col_member_user, col_member_group); + +EXECUTE create_view_rg_part || role_graph_detail_cte || role_graph || main_rg_select; + +END; +$$; + +SELECT * FROM role_relationship;-- LIMIT 1; +SELECT * FROM role_graph;-- LIMIT 1; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 55f7ec79e0..adaea51148 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -1310,3 +1310,262 @@ CREATE VIEW pg_stat_subscription_stats AS ss.stats_reset FROM pg_subscription as s, pg_stat_get_subscription_stats(s.oid) as ss; + +/* Built using a plpgsql dynamic SQL generator, pg_dump result copied here with name change. */ +/* See: pg_role_graph.plpgsql */ +CREATE VIEW pg_role_relationship AS + WITH RECURSIVE cte_role_relationship AS ( + SELECT r.oid AS leaf_node, + r.oid AS group_node, + NULL::oid AS grantor, + 0 AS level, + ARRAY[]::oid[] AS via, + false AS got_auth + FROM pg_roles r + UNION ALL + SELECT a.leaf_node, + m.roleid AS group_node, + m.grantor, + (a.level + 1), + (a.via || m.roleid), + (a.got_auth OR m.admin_option) + FROM (cte_role_relationship a + JOIN pg_auth_members m ON ((m.member = a.group_node))) + ) + SELECT cte_role_relationship.leaf_node, + cte_role_relationship.group_node, + cte_role_relationship.grantor, + cte_role_relationship.level, + cte_role_relationship.via, + cte_role_relationship.got_auth + FROM cte_role_relationship; + +CREATE VIEW pg_role_graph AS + WITH role_graph_detail AS ( + SELECT r.oid, + r.rolname, + CASE + WHEN r.rolcanlogin THEN 'User'::text + ELSE 'Group'::text + END AS role_type, + mou.memberof_users, + mog.memberof_groups, + mu.member_users, + mg.member_groups, + r.rolsuper, + r.rolcreaterole + FROM ((((pg_roles r + JOIN LATERAL ( SELECT array_agg(DISTINCT a.group_node ORDER BY a.group_node) AS array_agg + FROM (pg_role_relationship a + JOIN pg_roles u ON (((u.oid = a.group_node) AND u.rolcanlogin))) + WHERE ((a.leaf_node = r.oid) AND (r.oid <> a.group_node))) mou(memberof_users) ON (true)) + JOIN LATERAL ( SELECT array_agg(DISTINCT a.group_node ORDER BY a.group_node) AS array_agg + FROM (pg_role_relationship a + JOIN pg_roles u ON (((u.oid = a.group_node) AND (NOT u.rolcanlogin)))) + WHERE ((a.leaf_node = r.oid) AND (r.oid <> a.group_node))) mog(memberof_groups) ON (true)) + JOIN LATERAL ( SELECT array_agg(DISTINCT a.leaf_node ORDER BY a.leaf_node) AS array_agg + FROM (pg_role_relationship a + JOIN pg_roles u ON (((u.oid = a.leaf_node) AND u.rolcanlogin))) + WHERE ((a.group_node = r.oid) AND (r.oid <> a.leaf_node))) mu(member_users) ON (true)) + JOIN LATERAL ( SELECT array_agg(DISTINCT a.leaf_node ORDER BY a.leaf_node) AS array_agg + FROM (pg_role_relationship a + JOIN pg_roles u ON (((u.oid = a.leaf_node) AND (NOT u.rolcanlogin)))) + WHERE ((a.group_node = r.oid) AND (r.oid <> a.leaf_node))) mg(member_groups) ON (true)) + ), cte_role_graph AS ( + SELECT leaf_role.oid, + leaf_role.role_type, + leaf_role.rolname, + leaf_role.rolsuper, + array_to_string(ARRAY( SELECT vals.v + FROM ( VALUES ('Superuser'::text)) vals(v) + WHERE leaf_role.rolsuper + UNION ALL + SELECT vals.v + FROM ( VALUES ('Create Role'::text)) vals(v) + WHERE leaf_role.rolcreaterole + UNION ALL + SELECT ( + CASE + WHEN bool_or(grant_instance.got_auth) THEN 'of '::text + ELSE ''::text + END || + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END) + FROM ((((unnest(leaf_role.memberof_users) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.leaf_node = leaf_role.oid) AND (grant_instance.group_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via + HAVING bool_or(grant_instance.got_auth) + UNION ALL + SELECT ( + CASE + WHEN bool_or(grant_instance.got_auth) THEN 'of '::text + ELSE ''::text + END || + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END) + FROM ((((unnest(leaf_role.memberof_groups) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.leaf_node = leaf_role.oid) AND (grant_instance.group_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via + HAVING bool_or(grant_instance.got_auth) + UNION ALL + SELECT ( + CASE + WHEN bool_or(grant_instance.got_auth) THEN 'by '::text + ELSE ''::text + END || + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END) + FROM ((((unnest(leaf_role.member_users) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.group_node = leaf_role.oid) AND (grant_instance.leaf_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via + HAVING bool_or(grant_instance.got_auth) + UNION ALL + SELECT ( + CASE + WHEN bool_or(grant_instance.got_auth) THEN 'by '::text + ELSE ''::text + END || + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END) + FROM ((((unnest(leaf_role.member_groups) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.group_node = leaf_role.oid) AND (grant_instance.leaf_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via + HAVING bool_or(grant_instance.got_auth)), ' +'::text) AS administration, + array_to_string(ARRAY( SELECT + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END AS format + FROM ((((unnest(leaf_role.memberof_users) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.leaf_node = leaf_role.oid) AND (grant_instance.group_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via), ' +'::text) AS memberof_users, + array_to_string(ARRAY( SELECT + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END AS format + FROM ((((unnest(leaf_role.memberof_groups) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.leaf_node = leaf_role.oid) AND (grant_instance.group_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via), ' +'::text) AS memberof_groups, + array_to_string(ARRAY( SELECT + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END AS format + FROM ((((unnest(leaf_role.member_users) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.group_node = leaf_role.oid) AND (grant_instance.leaf_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via), ' +'::text) AS member_users, + array_to_string(ARRAY( SELECT + CASE + WHEN (cardinality(grant_instance.via) > 1) THEN format('%I via %s'::text, other_role.rolname, string_agg((((quote_ident((ancestor_role.rolname)::text) || '/'::text) || quote_ident((grant_role.rolname)::text)) || + CASE + WHEN (grant_instance.level > 2) THEN (('[+'::text || (grant_instance.level - 2)) || ']'::text) + ELSE ''::text + END), (' +'::text || repeat(' '::text, (length((other_role.rolname)::text) + 5))))) + ELSE format('%I from %s'::text, other_role.rolname, string_agg(quote_ident((grant_role.rolname)::text), ', '::text)) + END AS format + FROM ((((unnest(leaf_role.member_groups) other(other) + JOIN pg_roles other_role ON ((other_role.oid = other.other))) + JOIN pg_role_relationship grant_instance ON (((grant_instance.group_node = leaf_role.oid) AND (grant_instance.leaf_node = other.other)))) + JOIN pg_roles grant_role ON ((grant_role.oid = grant_instance.grantor))) + LEFT JOIN pg_roles ancestor_role ON ((ancestor_role.oid = grant_instance.via[(cardinality(grant_instance.via) - 1)]))) + GROUP BY other_role.rolname, grant_instance.via), ' +'::text) AS member_groups + FROM role_graph_detail leaf_role + ) + SELECT cte_role_graph.rolname, + cte_role_graph.administration, + cte_role_graph.memberof_users, + cte_role_graph.memberof_groups, + cte_role_graph.member_users, + cte_role_graph.member_groups, + cte_role_graph.role_type, + cte_role_graph.oid, + row_number() OVER (ORDER BY cte_role_graph.role_type, + CASE + WHEN cte_role_graph.rolsuper THEN (cte_role_graph.oid)::integer + ELSE NULL::integer + END, + CASE + WHEN (cte_role_graph.rolname ~ 'pg_'::text) THEN 0 + ELSE 1 + END, cte_role_graph.rolname) AS seq + FROM cte_role_graph + ORDER BY (row_number() OVER (ORDER BY cte_role_graph.role_type, + CASE + WHEN cte_role_graph.rolsuper THEN (cte_role_graph.oid)::integer + ELSE NULL::integer + END, + CASE + WHEN (cte_role_graph.rolname ~ 'pg_'::text) THEN 0 + ELSE 1 + END, cte_role_graph.rolname)); diff --git a/src/bin/psql/command.c b/src/bin/psql/command.c index a141146e70..77730643c4 100644 --- a/src/bin/psql/command.c +++ b/src/bin/psql/command.c @@ -881,6 +881,18 @@ exec_command_d(PsqlScanState scan_state, bool active_branch, const char *cmd) free(pattern2); } + else if (cmd[2] == 'r') + { + success = describeRoleGraph(pattern, show_verbose, show_system); + } + else if (cmd[2] == 'u') + { + success = describeUsers(pattern, show_verbose, show_system); + } + else if (cmd[2] == 'g') + { + success = describeGroups(pattern, show_verbose, show_system); + } else status = PSQL_CMD_UNKNOWN; break; diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index c645d66418..bb56e54a01 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -36,6 +36,7 @@ static bool describeOneTableDetails(const char *schemaname, bool verbose); static void add_tablespace_footer(printTableContent *const cont, char relkind, Oid tablespace, const bool newline); +static bool query_and_print_role_graph(const char *pattern, bool verbose, bool showSystem, char *header, char *roleType); static void add_role_attribute(PQExpBuffer buf, const char *const str); static bool listTSParsersVerbose(const char *pattern); static bool describeOneTSParser(const char *oid, const char *nspname, @@ -3744,6 +3745,113 @@ describeRoles(const char *pattern, bool verbose, bool showSystem) return true; } +/* + * \drr + * Describes the role graph for all roles + */ +bool +describeRoleGraph(const char *pattern, bool verbose, bool showSystem) +{ + return query_and_print_role_graph(pattern, verbose, showSystem, _("List of role graphs"), NULL); +} + +/* + * \dru + * Describes roles that behave as users, specifically, they can login. + * The design of this output assumes they are also a member of one ormore roles. + */ +bool +describeUsers(const char *pattern, bool verbose, bool showSystem) +{ + return query_and_print_role_graph(pattern, verbose, showSystem, _("List of user graphs"), "User"); +} + +/* + * \drg + * Describes roles that behave as groups, specifically they have one or + * more member roles. + */ +bool +describeGroups(const char *pattern, bool verbose, bool showSystem) +{ + return query_and_print_role_graph(pattern, verbose, showSystem, _("List of group graphs"), "Group"); +} + +static bool +query_and_print_role_graph(const char *pattern, bool verbose, bool showSystem, char *header, char *roleType) +{ + PQExpBufferData buf; + PGresult *res; + printTableContent cont; + printTableOpt myopt = pset.popt.topt; + int ncols = 6; + int nrows = 0; + int i; + const char align = 'l'; + myopt.default_footer = false; + + initPQExpBuffer(&buf); + + appendPQExpBufferStr(&buf, + "SELECT rg.rolname,\n" + " rg.administration,\n" + " rg.memberof_groups, rg.member_groups, rg.member_users, rg.memberof_users\n" + "FROM pg_catalog.pg_roles r\n" + "JOIN pg_catalog.pg_role_graph rg ON rg.oid = r.oid\n" + "WHERE "); + + if (roleType) + appendPQExpBuffer(&buf, "rg.role_type = '%s'\n", roleType); + else + appendPQExpBufferStr(&buf, "true\n"); + + if (!showSystem && !pattern) + appendPQExpBufferStr(&buf, "AND r.rolname !~ '^pg_'\n"); + + if (!validateSQLNamePattern(&buf, pattern, true, false, + NULL, "rg.rolname", NULL, NULL, + NULL, 1)) + { + termPQExpBuffer(&buf); + return false; + } + + appendPQExpBufferStr(&buf, + "ORDER BY rg.seq\n" + ";"); + + res = PSQLexec(buf.data); + if (!res) + return false; + + nrows = PQntuples(res); + + termPQExpBuffer(&buf); + + printTableInit(&cont, &myopt, header, ncols, nrows); + + printTableAddHeader(&cont, gettext_noop("Role name"), true, align); + printTableAddHeader(&cont, gettext_noop("Administration"), true, align); + printTableAddHeader(&cont, gettext_noop("Member of Groups"), true, align); + printTableAddHeader(&cont, gettext_noop("Group Members"), true, align); + printTableAddHeader(&cont, gettext_noop("User Members"), true, align); + printTableAddHeader(&cont, gettext_noop("Member of Users"), true, align); + + for (i = 0; i < nrows; i++) + { + printTableAddCell(&cont, PQgetvalue(res, i, 0), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 1), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 2), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 3), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 4), false, false); + printTableAddCell(&cont, PQgetvalue(res, i, 5), false, false); + } + + printTable(&cont, pset.queryFout, false, pset.logfile); + printTableCleanup(&cont); + return true; +} + static void add_role_attribute(PQExpBuffer buf, const char *const str) { diff --git a/src/bin/psql/describe.h b/src/bin/psql/describe.h index 7872c71f58..3c06069072 100644 --- a/src/bin/psql/describe.h +++ b/src/bin/psql/describe.h @@ -34,6 +34,15 @@ extern bool describeOperators(const char *oper_pattern, /* \du, \dg */ extern bool describeRoles(const char *pattern, bool verbose, bool showSystem); +/* \drr */ +extern bool describeRoleGraph(const char *pattern, bool verbose, bool showSystem); + +/* \dru */ +extern bool describeUsers(const char *pattern, bool verbose, bool showSystem); + +/* \drg */ +extern bool describeGroups(const char *pattern, bool verbose, bool showSystem); + /* \drds */ extern bool listDbRoleSettings(const char *pattern, const char *pattern2); -- 2.25.1