Repository: edgedb/edgedb

Issue:
Implement `CREATE ROLE SET password_hash`
This is needed for [`DESCRIBE ROLES`](https://github.com/edgedb/edgedb/issues/1491) to emit complete statements.


Discussion:

Resolving patch:
diff --git a/edb/schema/roles.py b/edb/schema/roles.py
--- a/edb/schema/roles.py
+++ b/edb/schema/roles.py
@@ -50,6 +50,13 @@ class Role(so.GlobalObject, so.InheritingObject,
         allow_ddl_set=True,
         inheritable=False)
 
+    password_hash = so.SchemaField(
+        str,
+        default=None,
+        allow_ddl_set=True,
+        ephemeral=True,
+        inheritable=False)
+
 
 class RoleCommandContext(
         sd.ObjectCommandContext[Role],
@@ -73,9 +80,25 @@ def _process_role_body(
     ) -> None:
         password = cmd.get_attribute_value('password')
         if password is not None:
+            if cmd.get_attribute_value('password_hash') is not None:
+                raise errors.EdgeQLSyntaxError(
+                    'cannot specify both `password` and `password_hash` in'
+                    ' the same statement',
+                    context=astnode.context,
+                )
             salted_password = scram.build_verifier(password)
             cmd.set_attribute_value('password', salted_password)
 
+        password_hash = cmd.get_attribute_value('password_hash')
+        if password_hash is not None:
+            try:
+                scram.parse_verifier(password_hash)
+            except ValueError as e:
+                raise errors.InvalidValueError(
+                    e.args[0],
+                    context=astnode.context)
+            cmd.set_attribute_value('password', password_hash)
+
     @classmethod
     def _classbases_from_ast(
         cls,


Test patch:
diff --git a/tests/test_server_auth.py b/tests/test_server_auth.py
--- a/tests/test_server_auth.py
+++ b/tests/test_server_auth.py
@@ -93,3 +93,63 @@ async def test_server_auth_01(self):
             await self.con.fetchall('''
                 DROP ROLE foo;
             ''')
+
+        # Basically the second test, but we can't run it concurrently
+        # because disabling Auth above conflicts with the following test
+
+        await self.con.fetchall('''
+            CREATE SUPERUSER ROLE bar {
+                SET password_hash := 'SCRAM-SHA-256$4096:SHzNmIppMwXnPSWgY2yMvg==$5zmnXMm9+mn2nseKPF1NTKvuoBPVSWgxHrnptxpQgcU=:/c1vJV+MmS7v9vv6CDVo56OyOJkNd3F+m3JIBB1U7ho=';
+            }
+        ''')  # noqa
+
+        try:
+            conn = await self.connect(
+                user='bar',
+                password='bar-pass',
+            )
+            await conn.aclose()
+
+            await self.con.fetchall('''
+                ALTER ROLE bar {
+                    SET password_hash := 'SCRAM-SHA-256$4096:mWDBY53yzQ4aDet5erBmbg==$ZboQEMuUhC6+1SChp2bx1qSRBZGAnyV4I8T/iK+qeEs=:B7yF2k10tTH2RHayOg3rw4Q6wqf+Fj5CuXR/9CyZ8n8=';
+                }
+            ''')  # noqa
+
+            conn = await self.connect(
+                user='bar',
+                password='bar-pass-2',
+            )
+            await conn.aclose()
+
+            # bad (old) password
+            with self.assertRaisesRegex(
+                    edgedb.AuthenticationError,
+                    'authentication failed'):
+                await self.connect(
+                    user='bar',
+                    password='bar-pass',
+                )
+
+            with self.assertRaisesRegex(
+                    edgedb.EdgeQLSyntaxError,
+                    'cannot specify both `password` and `password_hash`'
+                    ' in the same statement'):
+                await self.con.fetchall('''
+                    CREATE SUPERUSER ROLE bar1 {
+                        SET password := 'hello';
+                        SET password_hash := 'SCRAM-SHA-256$4096:SHzNmIppMwXnPSWgY2yMvg==$5zmnXMm9+mn2nseKPF1NTKvuoBPVSWgxHrnptxpQgcU=:/c1vJV+MmS7v9vv6CDVo56OyOJkNd3F+m3JIBB1U7ho=';
+                    }
+                ''')  # noqa
+
+            with self.assertRaisesRegex(
+                    edgedb.InvalidValueError,
+                    'invalid SCRAM verifier'):
+                await self.con.fetchall('''
+                    CREATE SUPERUSER ROLE bar2 {
+                        SET password_hash := 'SCRAM-BLAKE2B$4096:SHzNmIppMwXnPSWgY2yMvg==$5zmnXMm9+mn2nseKPF1NTKvuoBPVSWgxHrnptxpQgcU=:/c1vJV+MmS7v9vv6CDVo56OyOJkNd3F+m3JIBB1U7ho=';
+                    }
+                ''')  # noqa
+
+        finally:
+            await self.con.fetchall("DROP ROLE bar")