1

Postgres seems to have some weird semantics around which user is actually invoking check functions defined in RLS policies. Specifically, if the check function is SECURITY INVOKER, it appears that the function permissions are applied with respect to CURRENT_USER, irrespective of whether the policy applies to a different user (as might be the case if the query goes via a view with a different owner). If the check function is SECURITY DEFINER, it appears that the functions are executed by the policy user.

First, some context:

  • I have a database that uses RLS to gate access to data (PG v13, if that's relevant).
  • There is a main schema which holds all the underlying data.
  • There is an api schema, which holds views over the underlying data. In the simplest case, these views are just pass-thrus (e.g. `CREATE VIEW api.stuff AS SELECT a, b, c FROM main.stuff)
  • Objects in the api schema are owned by an api_owner user
  • The client that accesses the API is api_user, which is granted access to these views via GRANT
  • RLS policies on tables in the main schema apply to api_owner, because api_owner owns the views that allow access to data stored on main
  • RLS policies typically involve a check function, e.g. CREATE POLICY api_owner_select ON main.stuff FOR SELECT TO api_owner USING (main.current_user_id() = user_id);.

So, data access from a client's perspective looks something like the following:

[Client]    
GET /stuff 
    ↓
api_user (current_user)
SELECT * FROM api.stuff
    ↓
api_owner (policy user, via api.stuff query)
SELECT * FROM main.stuff
    ↓
Process query:
  - Check RLS policies on main.stuff that apply to api_owner
    - e.g. USING (main.current_user_id() = user_id)
    - filter rows that don't pass
  - Return resulting rows to client ⤴ 

So far, so good. However, the security status of the check function behaves very unintuitively:

  • If the check function (main.current_user_id() in my examples above) is SECURITY DEFINER, everything works as expected. In particular, there is no issue with being able to call main.current_user_id(). I assume this is because api_owner has USAGE on the main schema, the policies apply to api_owner, and therefore the function is executed by api_owner.
  • However, if the check function is SECURITY INVOKER, then I get the following error:
     ERROR:  permission denied for schema main
     LINE 3:   FROM main.users
                    ^
     QUERY:
       SELECT id
       FROM main.users
       WHERE users.email = current_setting('some_injected_state.email')
    
    The query here is from the internals of the current_user_id().

This leads to a bit of a paradox:

  • If the policy is being executed as the user accessing the data (i.e. api_owner via the view), then why should there be any issue? api_owner does have permission to access the main schema (and indeed has SELECT access on the users table), so the query should execute fine.
  • If the policy is instead being executed as CURRENT_USER (i.e. api_user), then why does setting the policy to SECURITY DEFINER work? api_user doesn't have direct access to main, so I would expect an issue with accessing objects (including functions) on the schema.
    • Indeed, if I SET ROLE api_user and try to call main.current_user_id(), I get exactly the error I expect:
      my_db=# ALTER FUNCTION main.current_user_id SECURITY DEFINER;
      ALTER FUNCTION
      my_db=> SET ROLE api_user;
      SET
      my_db=> SELECT main.current_user_id();
      ERROR:  permission denied for schema main
      LINE 1: SELECT main.current_user_id();
      

So, can anyone shed any light on the rules that determine which user Postgres RLS policies are executed as?

11
  • If current_user_id() belongs to the owner of schema main. that would explain the behavior. Commented Dec 5, 2023 at 16:09
  • @LaurenzAlbe Yeah, current_user_id() does indeed belong to the owner of main. Sorry if I'm being a bit thick here, but I guess I still don't see how that explains the (to my mind) paradoxical behaviour of how the policies appear to be executed. In particular, making the function SECURITY DEFINER appears to not only allow the body of the function to be executed with elevated permissions, but also bypasses the need for a GRANT on the function, which is very counterintuitive (and, I'm sure, an incorrect explanation of what's happening). Commented Dec 6, 2023 at 10:08
  • From the error message, the problem is not that the function cannot be called (it is called), but that the function tries to select from a table in a schema where the invoker (the user querying the data) has no access. With SECURITY DEFINER, the function is running with the security context of the owner of main, so the SQL statements in the function have no problem accessing tables in main. Commented Dec 6, 2023 at 10:15
  • 1
    No, the view owner is just used to check the permissions on the objects used in the view (and to determine the RLS policies that apply). But the function runs with with the user who did the SELECT. Even if that user does not have permissions to execute the function. Commented Dec 6, 2023 at 20:34
  • 1
    Oh no, a "why" question. Views and functions are different things. Functions can be executed, views cannot. With a sufficiently complex setup like yours (which I didn't fully understand, which is the reason why I couldn't write a comprehensive answer), you can easily end up with non-intuitive behavior. Commented Dec 8, 2023 at 17:39

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.