Repository: edgedb/edgedb

Issue:
ISE when using group by in access policy
- EdgeDB Version: 3.0-dev.6753+local

Steps to Reproduce:

```
alter type Account {
  create access policy two_decades
    allow insert, update
    using (
      count((
        group .watchlist
        using decade := .release_year // 10
        by decade
      )) <= 2
    )
}

# InternalServerError: normalize: cannot handle <edb.edgeql.ast.GroupQuery object at 0x7f8ca4fc3610>
```

Schema: Web UI example schema



Discussion:

Resolving patch:
diff --git a/edb/edgeql/compiler/normalization.py b/edb/edgeql/compiler/normalization.py
--- a/edb/edgeql/compiler/normalization.py
+++ b/edb/edgeql/compiler/normalization.py
@@ -111,7 +111,7 @@ def _normalize_recursively(
             modaliases=modaliases,
             localnames=localnames,
         )
-    elif isinstance(value, list):
+    elif isinstance(value, (tuple, list)):
         if value and isinstance(value[0], qlast.Base):
             for el in value:
                 normalize(
@@ -155,6 +155,7 @@ def normalize_DDL(
 def _normalize_with_block(
     node: qlast.Statement,
     *,
+    field: str='aliases',
     schema: s_schema.Schema,
     modaliases: Mapping[Optional[str], str],
     localnames: AbstractSet[str] = frozenset(),
@@ -164,7 +165,8 @@ def _normalize_with_block(
     modaliases = dict(modaliases)
     newaliases: List[Union[qlast.AliasedExpr, qlast.ModuleAliasDecl]] = []
 
-    for alias in (node.aliases or ()):
+    aliases: Optional[List[qlast.AliasedExpr]] = getattr(node, field)
+    for alias in (aliases or ()):
         if isinstance(alias, qlast.ModuleAliasDecl):
             if alias.alias:
                 modaliases[alias.alias] = alias.module
@@ -408,7 +410,39 @@ def normalize_GroupQuery(
     modaliases: Mapping[Optional[str], str],
     localnames: AbstractSet[str] = frozenset(),
 ) -> None:
-    raise AssertionError(f'normalize: cannot handle {node!r}')
+    # Process WITH block
+    modaliases, localnames = _normalize_with_block(
+        node,
+        schema=schema,
+        modaliases=modaliases,
+        localnames=localnames,
+    )
+
+    # Process the result expression
+    localnames = _normalize_aliased_field(
+        node,
+        'subject',
+        schema=schema,
+        modaliases=modaliases,
+        localnames=localnames,
+    )
+
+    modaliases, localnames = _normalize_with_block(
+        node,
+        field='using',
+        schema=schema,
+        modaliases=modaliases,
+        localnames=localnames,
+    )
+
+    _normalize_recursively(
+        node,
+        'by',
+        node.by,
+        schema=schema,
+        modaliases=modaliases,
+        localnames=localnames,
+    )
 
 
 def _normalize_objref(
diff --git a/edb/pgsql/compiler/dml.py b/edb/pgsql/compiler/dml.py
--- a/edb/pgsql/compiler/dml.py
+++ b/edb/pgsql/compiler/dml.py
@@ -898,6 +898,12 @@ def force_policy_checks(
                 stmt.where_clause, scan
             )
 
+    # If there aren't any update/insert queries to put it into
+    # (because it is just an update with a -=, probably), make it a
+    # normal check CTE.
+    if not queries:
+        ctx.env.check_ctes.append(policy_cte)
+
 
 def insert_needs_conflict_cte(
     ir_stmt: irast.MutatingStmt,
diff --git a/edb/schema/policies.py b/edb/schema/policies.py
--- a/edb/schema/policies.py
+++ b/edb/schema/policies.py
@@ -276,7 +276,8 @@ def validate_object(
                 # before actually doing the link table inserts.
                 # TODO: Fix this.
                 if (
-                    obj.is_link_property(schema)
+                    obj.get_source(schema)
+                    and obj.is_link_property(schema)
                     and obj.get_default(schema)
                     and any(
                         # XXX: apparently we don't do this coercion


Test patch:
diff --git a/tests/test_edgeql_group.py b/tests/test_edgeql_group.py
--- a/tests/test_edgeql_group.py
+++ b/tests/test_edgeql_group.py
@@ -1110,3 +1110,52 @@ async def test_edgeql_group_of_for_01(self):
                 }
             ])
         )
+
+    async def test_edgeql_group_policies_01(self):
+        await self.con.execute('''
+            with module cards
+            alter type User {
+                create access policy ok allow select, delete, update read;
+                create access policy two_elements allow insert, update write
+                  using (count((group .deck by .element)) = 2);
+            }
+        ''')
+
+        async with self.assertRaisesRegexTx(
+                edgedb.InvalidValueError,
+                r"access policy violation on insert"):
+            await self.con.query('''
+                with module cards
+                insert User {
+                    name := 'Sully',
+                    deck := (select Card filter .element = 'Water')
+                };
+            ''')
+
+        await self.con.query('''
+            with module cards
+            insert User {
+                name := 'Sully',
+                deck := (select Card filter .element = {'Water', 'Air'})
+            };
+        ''')
+
+        async with self.assertRaisesRegexTx(
+                edgedb.InvalidValueError,
+                r"access policy violation on update"):
+            await self.con.query('''
+                with module cards
+                update User filter .name = 'Sully' set {
+                    deck += (select Card filter .element = 'Earth')
+                };
+            ''')
+
+        async with self.assertRaisesRegexTx(
+                edgedb.InvalidValueError,
+                r"access policy violation on update"):
+            await self.con.query('''
+                with module cards
+                update User filter .name = 'Sully' set {
+                    deck -= (select Card filter .element = 'Water')
+                };
+            ''')