From 520cad75a33e67c52c4db3f15f0b42dca139833c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 17 Mar 2017 10:09:50 +0900
Subject: [PATCH] Add clause PASSWORD (val USING protocol) to CREATE/ALTER ROLE

This clause allows users to be able to enforce with which protocol
a given password is used with. if the value given is already encrypted,
the value is used as-is. This extension is useful to support future
protocols, particularly SCRAM-SHA-256.
---
 doc/src/sgml/ref/alter_role.sgml       | 10 +++++
 doc/src/sgml/ref/create_role.sgml      | 26 +++++++++++
 src/backend/commands/user.c            | 80 +++++++++++++++++++++++++++++++---
 src/backend/parser/gram.y              |  7 +++
 src/test/regress/expected/password.out | 34 ++++++++++++++-
 src/test/regress/sql/password.sql      | 15 +++++++
 6 files changed, 166 insertions(+), 6 deletions(-)

diff --git a/doc/src/sgml/ref/alter_role.sgml b/doc/src/sgml/ref/alter_role.sgml
index da36ad9696..257e3b96bf 100644
--- a/doc/src/sgml/ref/alter_role.sgml
+++ b/doc/src/sgml/ref/alter_role.sgml
@@ -34,6 +34,7 @@ ALTER ROLE <replaceable class="PARAMETER">role_specification</replaceable> [ WIT
     | BYPASSRLS | NOBYPASSRLS
     | CONNECTION LIMIT <replaceable class="PARAMETER">connlimit</replaceable>
     | [ ENCRYPTED | UNENCRYPTED ] PASSWORD '<replaceable class="PARAMETER">password</replaceable>'
+    | PASSWORD '<replaceable class="PARAMETER">password</replaceable>' USING '<replaceable class="PARAMETER">method</replaceable>'
     | VALID UNTIL '<replaceable class="PARAMETER">timestamp</replaceable>'
 
 ALTER ROLE <replaceable class="PARAMETER">name</replaceable> RENAME TO <replaceable>new_name</replaceable>
@@ -169,6 +170,7 @@ ALTER ROLE { <replaceable class="PARAMETER">role_specification</replaceable> | A
       <term><literal>NOBYPASSRLS</literal></term>
       <term><literal>CONNECTION LIMIT</literal> <replaceable class="parameter">connlimit</replaceable></term>
       <term><literal>PASSWORD</> <replaceable class="parameter">password</replaceable></term>
+      <term><literal>PASSWORD</> '<replaceable class="parameter">password</replaceable>' USING '<replaceable class="parameter">method</replaceable>'</term>
       <term><literal>ENCRYPTED</></term>
       <term><literal>UNENCRYPTED</></term>
       <term><literal>VALID UNTIL</literal> '<replaceable class="parameter">timestamp</replaceable>'</term>
@@ -280,6 +282,14 @@ ALTER ROLE davide WITH PASSWORD 'hu8jmn3';
   </para>
 
   <para>
+   Change a role's password using MD5 hash:
+
+<programlisting>
+ALTER ROLE lionel WITH PASSWORD 'hu8jmn3' USING 'md5';
+</programlisting>
+  </para>
+
+  <para>
    Remove a role's password:
 
 <programlisting>
diff --git a/doc/src/sgml/ref/create_role.sgml b/doc/src/sgml/ref/create_role.sgml
index 99d1c8336c..77a9539442 100644
--- a/doc/src/sgml/ref/create_role.sgml
+++ b/doc/src/sgml/ref/create_role.sgml
@@ -34,6 +34,7 @@ CREATE ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replac
     | BYPASSRLS | NOBYPASSRLS
     | CONNECTION LIMIT <replaceable class="PARAMETER">connlimit</replaceable>
     | [ ENCRYPTED | UNENCRYPTED ] PASSWORD '<replaceable class="PARAMETER">password</replaceable>'
+    | PASSWORD '<replaceable class="PARAMETER">password</replaceable>' USING '<replaceable class="PARAMETER">method</replaceable>'
     | VALID UNTIL '<replaceable class="PARAMETER">timestamp</replaceable>'
     | IN ROLE <replaceable class="PARAMETER">role_name</replaceable> [, ...]
     | IN GROUP <replaceable class="PARAMETER">role_name</replaceable> [, ...]
@@ -247,6 +248,23 @@ CREATE ROLE <replaceable class="PARAMETER">name</replaceable> [ [ WITH ] <replac
      </varlistentry>
 
      <varlistentry>
+      <term><literal>PASSWORD</> '<replaceable class="parameter">password</replaceable>' USING '<replaceable class="parameter">method</replaceable>'</term>
+      <listitem>
+       <para>
+        Sets the role's password using the requested method.  (A password
+        is only of use for roles having the <literal>LOGIN</literal>
+        attribute, but you can nonetheless define one for roles without it.)
+        If you do not plan to use password authentication you can omit this
+        option. The methods supported are <literal>md5</> to enforce a password
+        to be MD5-hashed, <literal>scram</> for a SCRAM-hashed password
+        and <literal>plain</> for an non-hashed password.  If the password
+        string is already in MD5-hashed or SCRAM-hashed, then it is
+        stored as-is.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
       <term><literal>VALID UNTIL</literal> '<replaceable class="parameter">timestamp</replaceable>'</term>
       <listitem>
        <para>
@@ -428,6 +446,14 @@ CREATE USER davide WITH PASSWORD 'jw8s0F4';
   </para>
 
   <para>
+   Create a role with a MD5-hashed password:
+
+<programlisting>
+CREATE USER lionel WITH PASSWORD 'asdh7as' USING 'md5';
+</programlisting>
+  </para>
+
+  <para>
    Create a role with a password that is valid until the end of 2004.
    After one second has ticked in 2005, the password is no longer
    valid.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 14b9779144..a8729f494d 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -130,7 +130,8 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 
 		if (strcmp(defel->defname, "password") == 0 ||
 			strcmp(defel->defname, "encryptedPassword") == 0 ||
-			strcmp(defel->defname, "unencryptedPassword") == 0)
+			strcmp(defel->defname, "unencryptedPassword") == 0 ||
+			strcmp(defel->defname, "methodPassword") == 0)
 		{
 			if (dpassword)
 				ereport(ERROR,
@@ -144,9 +145,45 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 					password_type = PASSWORD_TYPE_SCRAM;
 				else
 					password_type = PASSWORD_TYPE_MD5;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
 			}
 			else if (strcmp(defel->defname, "unencryptedPassword") == 0)
+			{
 				password_type = PASSWORD_TYPE_PLAINTEXT;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
+			}
+			else if (strcmp(defel->defname, "methodPassword") == 0)
+			{
+				/*
+				 * This is a list of two elements, the password is first and
+				 * then there is the method wanted by caller.
+				 */
+				if (dpassword && dpassword->arg)
+				{
+					char *method = strVal(lsecond((List *) dpassword->arg));
+
+					password = strVal(linitial((List *) dpassword->arg));
+
+					if (strcmp(method, "md5") == 0)
+						password_type = PASSWORD_TYPE_MD5;
+					else if (strcmp(method, "plain") == 0)
+						password_type = PASSWORD_TYPE_PLAINTEXT;
+					else if (strcmp(method, "scram") == 0)
+						password_type = PASSWORD_TYPE_SCRAM;
+					else
+						ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("unsupported password method %s", method)));
+				}
+			}
+			else
+			{
+				password_type = Password_encryption;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
+			}
 		}
 		else if (strcmp(defel->defname, "sysid") == 0)
 		{
@@ -266,8 +303,6 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
 				 defel->defname);
 	}
 
-	if (dpassword && dpassword->arg)
-		password = strVal(dpassword->arg);
 	if (dissuper)
 		issuper = intVal(dissuper->arg) != 0;
 	if (dinherit)
@@ -539,6 +574,7 @@ AlterRole(AlterRoleStmt *stmt)
 
 		if (strcmp(defel->defname, "password") == 0 ||
 			strcmp(defel->defname, "encryptedPassword") == 0 ||
+			strcmp(defel->defname, "methodPassword") == 0 ||
 			strcmp(defel->defname, "unencryptedPassword") == 0)
 		{
 			if (dpassword)
@@ -552,9 +588,45 @@ AlterRole(AlterRoleStmt *stmt)
 					password_type = PASSWORD_TYPE_SCRAM;
 				else
 					password_type = PASSWORD_TYPE_MD5;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
 			}
 			else if (strcmp(defel->defname, "unencryptedPassword") == 0)
+			{
 				password_type = PASSWORD_TYPE_PLAINTEXT;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
+			}
+			else if (strcmp(defel->defname, "methodPassword") == 0)
+			{
+				/*
+				 * This is a list of two elements, the password is first and
+				 * then there is the method wanted by caller.
+				 */
+				if (dpassword && dpassword->arg)
+				{
+					char *method = strVal(lsecond((List *) dpassword->arg));
+
+					if (strcmp(method, "md5") == 0)
+						password_type = PASSWORD_TYPE_MD5;
+					else if (strcmp(method, "plain") == 0)
+						password_type = PASSWORD_TYPE_PLAINTEXT;
+					else if (strcmp(method, "scram") == 0)
+						password_type = PASSWORD_TYPE_SCRAM;
+					else
+						ereport(ERROR,
+							(errcode(ERRCODE_SYNTAX_ERROR),
+							 errmsg("unsupported password method %s", method)));
+
+					password = strVal(linitial((List *) dpassword->arg));
+				}
+			}
+			else
+			{
+				password_type = Password_encryption;
+				if (dpassword && dpassword->arg)
+					password = strVal(dpassword->arg);
+			}
 		}
 		else if (strcmp(defel->defname, "superuser") == 0)
 		{
@@ -642,8 +714,6 @@ AlterRole(AlterRoleStmt *stmt)
 				 defel->defname);
 	}
 
-	if (dpassword && dpassword->arg)
-		password = strVal(dpassword->arg);
 	if (dissuper)
 		issuper = intVal(dissuper->arg);
 	if (dinherit)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 6316688a88..3b42e9129a 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -988,6 +988,13 @@ AlterOptRoleElem:
 				{
 					$$ = makeDefElem("password", NULL, @1);
 				}
+			| PASSWORD Sconst USING Sconst
+				{
+					$$ = makeDefElem("methodPassword",
+									 (Node *)list_make2(makeString($2),
+														makeString($4)),
+									 @1);
+				}
 			| ENCRYPTED PASSWORD Sconst
 				{
 					$$ = makeDefElem("encryptedPassword",
diff --git a/src/test/regress/expected/password.out b/src/test/regress/expected/password.out
index c503e43abe..eb88f420cc 100644
--- a/src/test/regress/expected/password.out
+++ b/src/test/regress/expected/password.out
@@ -63,6 +63,12 @@ ALTER ROLE regress_passwd4 ENCRYPTED PASSWORD 'scram-sha-256:VLK4RMaQLCvNtQ==:40
 SET password_encryption = 'scram';
 ALTER ROLE  regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
 CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
+-- PASSWORD 'value' USING 'method'
+CREATE ROLE regress_passwd7 PASSWORD 'role_pwd7' USING 'plain';
+CREATE ROLE regress_passwd8 PASSWORD 'role_pwd8' USING 'md5';
+CREATE ROLE regress_passwd9 PASSWORD 'role_pwd9' USING 'scram';
+CREATE ROLE regress_passwd10 PASSWORD 'role_pwd10' USING 'novalue'; --error
+ERROR:  unsupported password method novalue
 SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
@@ -75,7 +81,30 @@ SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):
  regress_passwd4 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
  regress_passwd5 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
  regress_passwd6 | md53725413363ab045e20521bf36b8d8d7f
-(6 rows)
+ regress_passwd7 | role_pwd7
+ regress_passwd8 | md571f6e76fa74cf5716b886f35ad945663
+ regress_passwd9 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
+(9 rows)
+
+ALTER ROLE regress_passwd7 PASSWORD 'role_pwd7' USING 'plain';
+ALTER ROLE regress_passwd8 PASSWORD 'role_pwd8' USING 'md5';
+ALTER ROLE regress_passwd9 PASSWORD 'role_pwd9' USING 'scram';
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_passwd%'
+    ORDER BY rolname, rolpassword;
+     rolname     |                rolpassword_masked                 
+-----------------+---------------------------------------------------
+ regress_passwd1 | foo
+ regress_passwd2 | md5dfa155cadd5f4ad57860162f3fab9cdb
+ regress_passwd3 | md5530de4c298af94b3b9f7d20305d2a1bf
+ regress_passwd4 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
+ regress_passwd5 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
+ regress_passwd6 | md53725413363ab045e20521bf36b8d8d7f
+ regress_passwd7 | role_pwd7
+ regress_passwd8 | md571f6e76fa74cf5716b886f35ad945663
+ regress_passwd9 | scram-sha-256:<salt>:4096:<storedkey>:<serverkey>
+(9 rows)
 
 DROP ROLE regress_passwd1;
 DROP ROLE regress_passwd2;
@@ -83,6 +112,9 @@ DROP ROLE regress_passwd3;
 DROP ROLE regress_passwd4;
 DROP ROLE regress_passwd5;
 DROP ROLE regress_passwd6;
+DROP ROLE regress_passwd7;
+DROP ROLE regress_passwd8;
+DROP ROLE regress_passwd9;
 -- all entries should have been removed
 SELECT rolname, rolpassword
     FROM pg_authid
diff --git a/src/test/regress/sql/password.sql b/src/test/regress/sql/password.sql
index f4b3a9ac3a..6210552234 100644
--- a/src/test/regress/sql/password.sql
+++ b/src/test/regress/sql/password.sql
@@ -54,6 +54,18 @@ SET password_encryption = 'scram';
 ALTER ROLE  regress_passwd5 ENCRYPTED PASSWORD 'foo'; -- create SCRAM verifier
 CREATE ROLE regress_passwd6 ENCRYPTED PASSWORD 'md53725413363ab045e20521bf36b8d8d7f'; -- encrypted with MD5, use as it is
 
+-- PASSWORD 'value' USING 'method'
+CREATE ROLE regress_passwd7 PASSWORD 'role_pwd7' USING 'plain';
+CREATE ROLE regress_passwd8 PASSWORD 'role_pwd8' USING 'md5';
+CREATE ROLE regress_passwd9 PASSWORD 'role_pwd9' USING 'scram';
+CREATE ROLE regress_passwd10 PASSWORD 'role_pwd10' USING 'novalue'; --error
+SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
+    FROM pg_authid
+    WHERE rolname LIKE 'regress_passwd%'
+    ORDER BY rolname, rolpassword;
+ALTER ROLE regress_passwd7 PASSWORD 'role_pwd7' USING 'plain';
+ALTER ROLE regress_passwd8 PASSWORD 'role_pwd8' USING 'md5';
+ALTER ROLE regress_passwd9 PASSWORD 'role_pwd9' USING 'scram';
 SELECT rolname, regexp_replace(rolpassword, '(scram-sha-256):([a-zA-Z0-9+/]+==):(\d+):(\w+):(\w+)', '\1:<salt>:\3:<storedkey>:<serverkey>') as rolpassword_masked
     FROM pg_authid
     WHERE rolname LIKE 'regress_passwd%'
@@ -65,6 +77,9 @@ DROP ROLE regress_passwd3;
 DROP ROLE regress_passwd4;
 DROP ROLE regress_passwd5;
 DROP ROLE regress_passwd6;
+DROP ROLE regress_passwd7;
+DROP ROLE regress_passwd8;
+DROP ROLE regress_passwd9;
 
 -- all entries should have been removed
 SELECT rolname, rolpassword
-- 
2.12.0

