Repository: edgedb/edgedb

Issue:
Don't allow changing user password in single user mode
Currently when postgres permissions don't allow creating users and the default user's password is changed it makes the provided `--backend-dsn` value invalid and subsequent backend connections fail. When running in single user mode changing the password should be banned. 



Discussion:
After further thoughts and tests, comparing to using the pg_user's password as the EdgeDB user's password, I think we could decouple EdgeDB user's password from the pg_user's password in single-user mode. Because EdgeDB should consistently manage EdgeDB roles and passwords in different modes: in non-single-user mode, EdgeDB creates all the roles it uses, so it's okay to assume that the pg_user's password is in sync with EdgeDB user's password, and the user is not supposed to modify pg_user's password. In single-user mode however, the pg_user's password may possibly be e.g. reset by the managing cloud system, I think the password of the EdgeDB user shouldn't be affected. I'll create a PR.

Resolving patch:
diff --git a/edb/pgsql/dbops/roles.py b/edb/pgsql/dbops/roles.py
--- a/edb/pgsql/dbops/roles.py
+++ b/edb/pgsql/dbops/roles.py
@@ -92,7 +92,10 @@ def code(self, block: base.PLBlock) -> str:
 
 class RoleCommand:
 
-    def _render(self):
+    def _role(self):
+        return f'ROLE {self.object.get_id()}'
+
+    def _attrs(self):
         attrs = []
 
         attrmap = {
@@ -116,7 +119,7 @@ def _render(self):
         elif self.object.password is not base.NotSpecified:
             attrs.append(f'PASSWORD {ql(self.object.password)}')
 
-        return f'ROLE {self.object.get_id()} {" ".join(attrs)}'
+        return " ".join(attrs)
 
 
 class CreateRole(ddl.CreateObject, RoleCommand):
@@ -132,13 +135,17 @@ def code(self, block: base.PLBlock) -> str:
             members = f'ROLE {roles}'
         else:
             members = ''
-        return f'CREATE {self._render()} {membership} {members}'
+        return f'CREATE {self._role()} {self._attrs()} {membership} {members}'
 
 
 class AlterRole(ddl.AlterObject, RoleCommand):
 
     def code(self, block: base.PLBlock) -> str:
-        return f'ALTER {self._render()}'
+        attrs = self._attrs()
+        if attrs:
+            return f'ALTER {self._role()} {attrs}'
+        else:
+            return ''
 
     def generate_extra(self, block: base.PLBlock) -> None:
         super().generate_extra(block)
diff --git a/edb/pgsql/delta.py b/edb/pgsql/delta.py
--- a/edb/pgsql/delta.py
+++ b/edb/pgsql/delta.py
@@ -5516,7 +5516,9 @@ def apply(
         kwargs = {}
         if self.has_attribute_value('password'):
             passwd = self.get_attribute_value('password')
-            kwargs['password'] = passwd
+            if backend_params.has_create_role:
+                # Only modify Postgres password of roles managed by EdgeDB
+                kwargs['password'] = passwd
             kwargs['metadata'] = dict(
                 id=str(role.id),
                 name=role_name,


Test patch:
diff --git a/tests/test_edgeql_ddl.py b/tests/test_edgeql_ddl.py
--- a/tests/test_edgeql_ddl.py
+++ b/tests/test_edgeql_ddl.py
@@ -7119,6 +7119,35 @@ async def test_edgeql_ddl_role_04(self):
             }]
         )
 
+    async def test_edgeql_ddl_role_05(self):
+        if self.has_create_role:
+            self.skipTest("create role is supported by the backend")
+        con = await self.connect()
+        try:
+            await con.execute("""
+                ALTER ROLE edgedb SET password := 'test_role_05'
+            """)
+            if self.has_create_database:
+                await con.execute("""CREATE DATABASE test_role_05""")
+        finally:
+            await con.aclose()
+        args = {'password': "test_role_05"}
+        if self.has_create_database:
+            args['database'] = "test_role_05"
+        con = await self.connect(**args)
+        try:
+            await con.execute("""
+                ALTER ROLE edgedb SET password := 'test'
+            """)
+        finally:
+            await con.aclose()
+        con = await self.connect()
+        try:
+            if self.has_create_database:
+                await con.execute("""DROP DATABASE test_role_05""")
+        finally:
+            await con.aclose()
+
     async def test_edgeql_ddl_describe_roles(self):
         if not self.has_create_role:
             self.skipTest("create role is not supported by the backend")