Repository: edgedb/edgedb

Issue:
Setting enforce_access_policies to false doesn't work inside functions
Functions are precompiled, so setting enforce_access_policies to false doesn't affect their behavior at all.

Fixing this is not too hard: functions already take an extra json argument that encodes global variables, and we can just put the value of enforce_access_policies in it, and check that when evaluating access policies inside the function


Discussion:
I was wondering if this flag to ignore access policies for a particular function will be exposed for schema use too.  I've been wanting to extract portions of my access policies into functions but this is complicated or made impossible by the functions themselves respecting access policies.  

A contrived example:
```
function complex_check(obj: A, car: Car) -> bool {
    using (...);
}

type Car {
    access policy deny_everything
        deny all;
}

type A {
    link parent -> A;
    link sibling -> A;
    link car -> Car;

    access policy complicated_policy
        allow all using (
            complex_check(.parent, .car) and complex_check(.sibling, .car);
        );
}
```

Is there some way to achieve the above or am I missing an easier option?
@zerosym that's a good point. I'll think about it.

One simple option might be for us to expose the "current function" as a built-in global, and then you can use that in policies to "bless" certain functions. We'd probably want to do a bit of special casing to optimize, maybe?
I don't know.

I now realize though that using functions in insert/update constraints is perilous: newly inserted/updated data is not visible to functions (see #5172)

Resolving patch:
diff --git a/edb/edgeql/compiler/policies.py b/edb/edgeql/compiler/policies.py
--- a/edb/edgeql/compiler/policies.py
+++ b/edb/edgeql/compiler/policies.py
@@ -144,6 +144,37 @@ def compile_pol(
         return dispatch.compile(expr, ctx=dctx)
 
 
+def get_extra_function_rewrite_filter(ctx: context.ContextLevel) -> qlast.Expr:
+    # Functions need to check whether access policies are disabled,
+    # which is signalled through a field in globals json object.
+    # It's only populated when policies are disabled.
+    #
+    # We could also have done this by checking
+    # cfg::Config.apply_access_policies, but that's probably slower,
+    # and we have this mechanism anyway.
+    json_type = qlast.TypeName(maintype=qlast.ObjectRef(
+        module='__std__', name='json'))
+    glob_set = setgen.get_func_global_json_arg(ctx=ctx)
+    func_override = qlast.FunctionCall(
+        func=('__std__', 'json_get'),
+        args=[
+            ctx.create_anchor(glob_set, 'a'),
+            qlast.StringConstant(value="__disable_access_policies"),
+        ],
+        kwargs={
+            'default': qlast.TypeCast(
+                expr=qlast.BooleanConstant(value="false"),
+                type=json_type,
+            )
+        },
+    )
+    return qlast.TypeCast(
+        expr=func_override,
+        type=qlast.TypeName(maintype=qlast.ObjectRef(
+            module='__std__', name='bool'))
+    )
+
+
 def get_rewrite_filter(
     stype: s_objtypes.ObjectType, *,
     mode: qltypes.AccessKind,
@@ -162,7 +193,7 @@ def get_rewrite_filter(
             continue
 
         ir_set = compile_pol(pol, ctx=ctx)
-        expr = ctx.create_anchor(ir_set)
+        expr: qlast.Expr = ctx.create_anchor(ir_set)
 
         is_allow = pol.get_action(schema) == qltypes.AccessPolicyAction.Allow
         if is_allow:
@@ -170,6 +201,9 @@ def get_rewrite_filter(
         else:
             deny.append(expr)
 
+    if ctx.env.options.func_params is not None:
+        allow.append(get_extra_function_rewrite_filter(ctx))
+
     if allow:
         filter_expr = astutils.extend_binop(None, *allow, op='OR')
     else:
diff --git a/edb/edgeql/compiler/setgen.py b/edb/edgeql/compiler/setgen.py
--- a/edb/edgeql/compiler/setgen.py
+++ b/edb/edgeql/compiler/setgen.py
@@ -1955,7 +1955,7 @@ def get_globals_as_json(
     with ctx.new() as subctx:
         subctx.anchors = subctx.anchors.copy()
         normal_els = []
-        full_objs = []
+        full_objs: list[qlast.Expr] = []
 
         json_type = qlast.TypeName(maintype=qlast.ObjectRef(
             module='__std__', name='json'))
@@ -2000,11 +2000,25 @@ def get_globals_as_json(
                     )
                 ))
 
+        # If access policies are disabled, stick a value in the blob
+        # to indicate that.  We do this using a full object so it
+        # works in constraints and the like, where the tuple->json cast
+        # isn't supported yet.
+        if (
+            not ctx.env.options.apply_user_access_policies
+            or not ctx.env.options.apply_query_rewrites
+        ):
+            full_objs.append(qlast.FunctionCall(
+                func=('__std__', 'to_json'),
+                args=[qlast.StringConstant(
+                    value='{"__disable_access_policies": true}'
+                )],
+            ))
+
         full_expr: qlast.Expr
         if not normal_els and not full_objs:
             full_expr = null_expr
         else:
-
             simple_obj = None
             if normal_els or not full_objs:
                 simple_obj = qlast.TypeCast(
diff --git a/edb/pgsql/dbops/indexes.py b/edb/pgsql/dbops/indexes.py
--- a/edb/pgsql/dbops/indexes.py
+++ b/edb/pgsql/dbops/indexes.py
@@ -100,6 +100,8 @@ def creation_code(self, block: base.PLBlock) -> str:
 
         kwargs = self.metadata.get('kwargs')
         if kwargs is not None:
+            # Escape the expression first
+            expr = expr.replace('{', '{{').replace('}', '}}')
             expr = re.sub(r'(__kw_(\w+?)__)', r'{\2}', expr)
             expr = expr.format(**kwargs)
 


Test patch:
diff --git a/tests/test_edgeql_policies.py b/tests/test_edgeql_policies.py
--- a/tests/test_edgeql_policies.py
+++ b/tests/test_edgeql_policies.py
@@ -65,6 +65,7 @@ class TestEdgeQLPolicies(tb.QueryTestCase):
                       (global cur_user IN __subject__.watchers.name) ?? false
                   )
             };
+            create function count_Issue() -> int64 using (count(Issue));
 
             create type CurOnly extending Dictionary {
                 create access policy cur_only allow all
@@ -1020,3 +1021,20 @@ async def test_edgeql_policies_namespace(self):
             r'''select X''',
             [{}],
         )
+
+    async def test_edgeql_policies_function_01(self):
+        await self.con.execute('''
+            set global filter_owned := true;
+        ''')
+        await self.assert_query_result(
+            r'''select (count(Issue), count_Issue())''',
+            [(0, 0)],
+        )
+
+        await self.con.execute('''
+            configure session set apply_access_policies := false;
+        ''')
+        await self.assert_query_result(
+            r'''select (count(Issue), count_Issue())''',
+            [(4, 4)],
+        )