From 08951a7c93cf0dd791ee6ac8a8cf5e4b152528e5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 13 Jul 2022 12:21:20 +0900
Subject: [PATCH] createuser: Add support for more clause types through new
 options
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The following options are added to createuser:
* --valid-until to generate a VALID UNTIL clause for the role created.
* --bypassrls/--no-bypassrls for BYPASSRLS/NOBYPASSRLS.
* -m/--member to make the new role a member of an existing role, with an
extra ROLE clause generated.  The clause generated overlaps with
-g/--role, but per discussion this was the most popular choice as option
name.
* -a/--admin for the addition of an ADMIN clause.

These option names are chosen to be completely new, so as they do not
impact anybody relying on the existing option set.  Tests are added for
the new options and extended a bit, while on it, to cover more patterns
where quotes are added to various elements of the query generated.

Author: Shinya Kato
Reviewed-by: Nathan Bossart, Daniel Gustafsson, Robert Haas, Kyotaro
Horiguchi, David G. Johnston, Przemysław Sztoch
Discussion: https://postgr.es/m/69a9851035cf0f0477bcc5d742b031a3@oss.nttdata.com
---
 doc/src/sgml/ref/createuser.sgml    | 56 ++++++++++++++++++++++
 src/bin/scripts/createuser.c        | 72 ++++++++++++++++++++++++++++-
 src/bin/scripts/t/040_createuser.pl | 32 ++++++++++++-
 3 files changed, 156 insertions(+), 4 deletions(-)

diff --git a/doc/src/sgml/ref/createuser.sgml b/doc/src/sgml/ref/createuser.sgml
index 17579e50af..c6a7c603f7 100644
--- a/doc/src/sgml/ref/createuser.sgml
+++ b/doc/src/sgml/ref/createuser.sgml
@@ -76,6 +76,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-a <replaceable class="parameter">role</replaceable></option></term>
+      <term><option>--admin=<replaceable class="parameter">role</replaceable></option></term>
+      <listitem>
+       <para>
+        Indicates role that will be immediately added as a member of the new
+        role with admin option, giving it the right to grant membership in the
+        new role to others.  Multiple roles to add as members (with admin
+        option) of the new role can be specified by writing multiple
+        <option>-a</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-c <replaceable class="parameter">number</replaceable></option></term>
       <term><option>--connection-limit=<replaceable class="parameter">number</replaceable></option></term>
@@ -204,6 +218,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-m <replaceable class="parameter">role</replaceable></option></term>
+      <term><option>--member=<replaceable class="parameter">role</replaceable></option></term>
+      <listitem>
+       <para>
+        Indicates role that will be immediately added as a member of the new
+        role.  Multiple roles to add as members of the new role can be specified
+        by writing multiple <option>-m</option> switches.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-P</option></term>
       <term><option>--pwprompt</option></term>
@@ -258,6 +284,17 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-v <replaceable class="parameter">timestamp</replaceable></option></term>
+      <term><option>--valid-until=<replaceable class="parameter">timestamp</replaceable></option></term>
+      <listitem>
+       <para>
+        Set a date and time after which the role's password is no longer valid.
+        The default is to set no password expiry date.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
        <term><option>-V</option></term>
        <term><option>--version</option></term>
@@ -268,6 +305,25 @@ PostgreSQL documentation
        </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--bypassrls</option></term>
+      <listitem>
+       <para>
+        The new user will bypass every row-level security (RLS) policy.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--no-bypassrls</option></term>
+      <listitem>
+       <para>
+        The new user will not bypass row-level security (RLS) policies.  This is
+        the default.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--replication</option></term>
       <listitem>
diff --git a/src/bin/scripts/createuser.c b/src/bin/scripts/createuser.c
index f31d82f24c..f8ff133f53 100644
--- a/src/bin/scripts/createuser.c
+++ b/src/bin/scripts/createuser.c
@@ -28,6 +28,7 @@ int
 main(int argc, char *argv[])
 {
 	static struct option long_options[] = {
+		{"admin", required_argument, NULL, 'a'},
 		{"connection-limit", required_argument, NULL, 'c'},
 		{"createdb", no_argument, NULL, 'd'},
 		{"no-createdb", no_argument, NULL, 'D'},
@@ -39,6 +40,7 @@ main(int argc, char *argv[])
 		{"no-inherit", no_argument, NULL, 'I'},
 		{"login", no_argument, NULL, 'l'},
 		{"no-login", no_argument, NULL, 'L'},
+		{"member", required_argument, NULL, 'm'},
 		{"port", required_argument, NULL, 'p'},
 		{"pwprompt", no_argument, NULL, 'P'},
 		{"createrole", no_argument, NULL, 'r'},
@@ -46,11 +48,14 @@ main(int argc, char *argv[])
 		{"superuser", no_argument, NULL, 's'},
 		{"no-superuser", no_argument, NULL, 'S'},
 		{"username", required_argument, NULL, 'U'},
+		{"valid-until", required_argument, NULL, 'v'},
 		{"no-password", no_argument, NULL, 'w'},
 		{"password", no_argument, NULL, 'W'},
 		{"replication", no_argument, NULL, 1},
 		{"no-replication", no_argument, NULL, 2},
 		{"interactive", no_argument, NULL, 3},
+		{"bypassrls", no_argument, NULL, 4},
+		{"no-bypassrls", no_argument, NULL, 5},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -62,6 +67,8 @@ main(int argc, char *argv[])
 	char	   *port = NULL;
 	char	   *username = NULL;
 	SimpleStringList roles = {NULL, NULL};
+	SimpleStringList members = {NULL, NULL};
+	SimpleStringList admins = {NULL, NULL};
 	enum trivalue prompt_password = TRI_DEFAULT;
 	ConnParams	cparams;
 	bool		echo = false;
@@ -69,6 +76,7 @@ main(int argc, char *argv[])
 	int			conn_limit = -2;	/* less than minimum valid value */
 	bool		pwprompt = false;
 	char	   *newpassword = NULL;
+	char	   *pwexpiry = NULL;
 
 	/* Tri-valued variables.  */
 	enum trivalue createdb = TRI_DEFAULT,
@@ -76,7 +84,8 @@ main(int argc, char *argv[])
 				createrole = TRI_DEFAULT,
 				inherit = TRI_DEFAULT,
 				login = TRI_DEFAULT,
-				replication = TRI_DEFAULT;
+				replication = TRI_DEFAULT,
+				bypassrls = TRI_DEFAULT;
 
 	PQExpBufferData sql;
 
@@ -89,11 +98,14 @@ main(int argc, char *argv[])
 
 	handle_help_version_opts(argc, argv, "createuser", help);
 
-	while ((c = getopt_long(argc, argv, "c:dDeEg:h:iIlLp:PrRsSU:wW",
+	while ((c = getopt_long(argc, argv, "a:c:dDeEg:h:iIlLm:p:PrRsSU:v:wW",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
 		{
+			case 'a':
+				simple_string_list_append(&admins, optarg);
+				break;
 			case 'c':
 				if (!option_parse_int(optarg, "-c/--connection-limit",
 									  -1, INT_MAX, &conn_limit))
@@ -129,6 +141,9 @@ main(int argc, char *argv[])
 			case 'L':
 				login = TRI_NO;
 				break;
+			case 'm':
+				simple_string_list_append(&members, optarg);
+				break;
 			case 'p':
 				port = pg_strdup(optarg);
 				break;
@@ -150,6 +165,9 @@ main(int argc, char *argv[])
 			case 'U':
 				username = pg_strdup(optarg);
 				break;
+			case 'v':
+				pwexpiry = pg_strdup(optarg);
+				break;
 			case 'w':
 				prompt_password = TRI_NO;
 				break;
@@ -165,6 +183,12 @@ main(int argc, char *argv[])
 			case 3:
 				interactive = true;
 				break;
+			case 4:
+				bypassrls = TRI_YES;
+				break;
+			case 5:
+				bypassrls = TRI_NO;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -304,8 +328,17 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(&sql, " REPLICATION");
 	if (replication == TRI_NO)
 		appendPQExpBufferStr(&sql, " NOREPLICATION");
+	if (bypassrls == TRI_YES)
+		appendPQExpBufferStr(&sql, " BYPASSRLS");
+	if (bypassrls == TRI_NO)
+		appendPQExpBufferStr(&sql, " NOBYPASSRLS");
 	if (conn_limit >= -1)
 		appendPQExpBuffer(&sql, " CONNECTION LIMIT %d", conn_limit);
+	if (pwexpiry != NULL)
+	{
+		appendPQExpBufferStr(&sql, " VALID UNTIL ");
+		appendStringLiteralConn(&sql, pwexpiry, conn);
+	}
 	if (roles.head != NULL)
 	{
 		SimpleStringListCell *cell;
@@ -320,6 +353,35 @@ main(int argc, char *argv[])
 				appendPQExpBufferStr(&sql, fmtId(cell->val));
 		}
 	}
+	if (members.head != NULL)
+	{
+		SimpleStringListCell *cell;
+
+		appendPQExpBufferStr(&sql, " ROLE ");
+
+		for (cell = members.head; cell; cell = cell->next)
+		{
+			if (cell->next)
+				appendPQExpBuffer(&sql, "%s,", fmtId(cell->val));
+			else
+				appendPQExpBufferStr(&sql, fmtId(cell->val));
+		}
+	}
+	if (admins.head != NULL)
+	{
+		SimpleStringListCell *cell;
+
+		appendPQExpBufferStr(&sql, " ADMIN ");
+
+		for (cell = admins.head; cell; cell = cell->next)
+		{
+			if (cell->next)
+				appendPQExpBuffer(&sql, "%s,", fmtId(cell->val));
+			else
+				appendPQExpBufferStr(&sql, fmtId(cell->val));
+		}
+	}
+
 	appendPQExpBufferChar(&sql, ';');
 
 	if (echo)
@@ -346,6 +408,8 @@ help(const char *progname)
 	printf(_("Usage:\n"));
 	printf(_("  %s [OPTION]... [ROLENAME]\n"), progname);
 	printf(_("\nOptions:\n"));
+	printf(_("  -a, --admin=ROLE          this role will be a member of new role with admin\n"
+			 "                            option\n"));
 	printf(_("  -c, --connection-limit=N  connection limit for role (default: no limit)\n"));
 	printf(_("  -d, --createdb            role can create new databases\n"));
 	printf(_("  -D, --no-createdb         role cannot create databases (default)\n"));
@@ -356,14 +420,18 @@ help(const char *progname)
 	printf(_("  -I, --no-inherit          role does not inherit privileges\n"));
 	printf(_("  -l, --login               role can login (default)\n"));
 	printf(_("  -L, --no-login            role cannot login\n"));
+	printf(_("  -m, --member=ROLE         this role will be a member of new role\n"));
 	printf(_("  -P, --pwprompt            assign a password to new role\n"));
 	printf(_("  -r, --createrole          role can create new roles\n"));
 	printf(_("  -R, --no-createrole       role cannot create roles (default)\n"));
 	printf(_("  -s, --superuser           role will be superuser\n"));
 	printf(_("  -S, --no-superuser        role will not be superuser (default)\n"));
+	printf(_("  -v, --valid-until         password expiration date for role\n"));
 	printf(_("  -V, --version             output version information, then exit\n"));
 	printf(_("  --interactive             prompt for missing role name and attributes rather\n"
 			 "                            than using defaults\n"));
+	printf(_("  --bypassrls               role can bypass row-level security (RLS) policy\n"));
+	printf(_("  --no-bypassrls            role cannot bypass row-level security (RLS) policy\n"));
 	printf(_("  --replication             role can initiate replication\n"));
 	printf(_("  --no-replication          role cannot initiate replication\n"));
 	printf(_("  -?, --help                show this help, then exit\n"));
diff --git a/src/bin/scripts/t/040_createuser.pl b/src/bin/scripts/t/040_createuser.pl
index 2a34be81cf..834d258bf8 100644
--- a/src/bin/scripts/t/040_createuser.pl
+++ b/src/bin/scripts/t/040_createuser.pl
@@ -25,13 +25,41 @@ $node->issues_sql_like(
 	qr/statement: CREATE ROLE regress_role1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOLOGIN;/,
 	'create a non-login role');
 $node->issues_sql_like(
-	[ 'createuser', '-r', 'regress_user2' ],
-	qr/statement: CREATE ROLE regress_user2 NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN;/,
+	[ 'createuser', '-r', 'regress user2' ],
+	qr/statement: CREATE ROLE "regress user2" NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN;/,
 	'create a CREATEROLE user');
 $node->issues_sql_like(
 	[ 'createuser', '-s', 'regress_user3' ],
 	qr/statement: CREATE ROLE regress_user3 SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;/,
 	'create a superuser');
+$node->issues_sql_like(
+	[
+		'createuser',    '-a',
+		'regress_user1', '-a',
+		'regress user2', 'regress user #4'
+	],
+	qr/statement: CREATE ROLE "regress user #4" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN ADMIN regress_user1,"regress user2";/,
+	'add a role as a member with admin option of the newly created role');
+$node->issues_sql_like(
+	[
+		'createuser',      '-m',
+		'regress_user3',   '-m',
+		'regress user #4', 'REGRESS_USER5'
+	],
+	qr/statement: CREATE ROLE "REGRESS_USER5" NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN ROLE regress_user3,"regress user #4";/,
+	'add a role as a member of the newly created role');
+$node->issues_sql_like(
+	[ 'createuser', '-v', '2029 12 31', 'regress_user6' ],
+	qr/statement: CREATE ROLE regress_user6 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN VALID UNTIL \'2029 12 31\';/,
+	'create a role with a password expiration date');
+$node->issues_sql_like(
+	[ 'createuser', '--bypassrls', 'regress_user7' ],
+	qr/statement: CREATE ROLE regress_user7 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN BYPASSRLS;/,
+	'create a BYPASSRLS role');
+$node->issues_sql_like(
+	[ 'createuser', '--no-bypassrls', 'regress_user8' ],
+	qr/statement: CREATE ROLE regress_user8 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOBYPASSRLS;/,
+	'create a role without BYPASSRLS');
 
 $node->command_fails([ 'createuser', 'regress_user1' ],
 	'fails if role already exists');