mulungood

Setting Row-Level-Security (RLS) with Functions on Supabase

How to use Postgres functions to improve the writing and maintenance of Row-Level-Security (RLS) in Supabase.

fernanda's photofernanda

RLS functions can be a nifty way to reuse code in the PostgreSQL world to keep your database secure.

In this article, I’ll discuss them with the following example project and goals:

What is Row-Level-Security (RLS)?

From the Supabase docs:

Postgres Row Level Security (RLS) is a feature of Postgres that allows you to control which users are permitted to perform SELECT/INSERT/UPDATE/DELETE statements on specific rows within tables and views.

Let’s say our example consists of schools, their teachers and their students. School 1, Sunnydale High School, cannot access the data from School 2, the Neptune High School, and so on. RLS allows us to restrict their access based on their membership and user role by using the user id generated by the Authentication process.

If you’re completely new to RLS, I suggest reading the Supabase doc linked above before continuing, or keep it open to help fill in your knowledge gap.

Supabase Authentication

We have two basic authentication roles:

anon: an anonymous request (the user is not logged in)
authenticated: an authenticated request (the user is logged in)

If a user is logged in, we will receive their user_id when we call auth.uid() in our RLS policy.

Why use RLS Functions?

As with functions elsewhere, one big advantage of using RLS functions is to write reusable code, thus minimizing what you have to write.

A common example: a user can do three actions out of the four available (SELECT/INSERT/UPDATE/DELETE). Unfortunately you will need to write three policies, one for each action. Instead of writing the logic three times, you invoke the reusable function.

Besides, if something changes in the logic, that change will be immediately “propagated” too all policies evoking that function.

Implementing a RLS Function

Step 1 - Enable RLS

RLS is not automatically enabled by default on Postgres (Supabase is a Postgres database — Why you should always enable RLS). To enable it via SQL editor:

ALTER TABLE "public"."schools" ENABLE ROW LEVEL SECURITY;

😳 I have in the past set policies and forgot to enable the RLS, so always make sure it’s active!

Step 2 - Basic Police

I have a schools table and I want all authenticated users to be able to read (equivalent to SELECT*) the data on this table, which includes Name, Address, Principal etc.

CREATE POLICY "Authenticated users can select" ON "public"."schools" FOR SELECT TO authenticated USING (TRUE);

You probably know this already, but in case you need a refresher: SELECT = read, INSERT = create, UPDATE and DELETE, well, those are obvious.

Simple, no?

To complicate things a bit — I have a table called school_teacher_link (a many-to-many table), linking teachers to their school. Teachers should only have access to their school’s data.

**Example of a many-to-many table**:


```sql
CREATE TABLE
  public.school_teacher_link (
    teacher_id UUID NOT NULL,
    school_id UUID NULL,
    CONSTRAINT school_teacher_link_teacher_id_fkey FOREIGN KEY (teacher_id) REFERENCES auth.users (id) ON UPDATE CASCADE ON DELETE CASCADE,
    CONSTRAINT school_teacher_link_school_id_fkey FOREIGN KEY (school_id) REFERENCES organizations (id) ON UPDATE CASCADE ON DELETE CASCADE
  ) TABLESPACE pg_default;

CREATE UNIQUE INDEX school_teacher_link_pk ON public.organizations_members USING btree (user_id, organization_id);

alter table "public"."school_teacher_link" add constraint "organizations_members_pk" PRIMARY KEY using index "organizations_members_pk";
```


**Example of a table with data from multiple schools**:


```sql
CREATE TABLE
public.class_schedule (
	id UUID NOT NULL
	school_id UUID NOT NULL,
	subject TEXT NOT NULL,
	start_time TIMESTAMP WITH TIME ZONE NOT NULL,
// etc
)
```


_With RLS, we will use the_ _`school_id`_ _and the table_ _`school_teacher_link`_ _to find out whether a given teacher has access to a specific_ _`class_schedule`_ _row._

Step 3: Custom Policy

I need to consider the different roles:

To simplify this example, we’re keeping to 3 levels of access.

Step 3.a: Optional) Create a reusable function for basic access control based on role

Let’s tackle the Client’s access first as their access is expansive and only dependent on their role.

In the Platform Admin (PA) case, I’ll need to repeat the access logic for most tables, so it’s wonderful that RLS Policies allow you to use functions.

// My policy is simple and quick CREATE POLICY "PA can see, edit, update and delete" ON "public"."class_schedule" FOR ALL // SELECT, INSERT, UPDATE, DELETE TO authenticated USING ( // auth.uid() will return the authenticated user's id public.is_user_allowed (auth.uid (), 'platform_admin') );

Here’s the function that I can reuse on all tables:

CREATE OR REPLACE FUNCTION public.is_user_allowed (user_id UUID, allowed_role public.role_type) RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE is_user_allowed BOOLEAN; BEGIN select exists ( select 1 from "public"."user_profiles" where id = user_id and role_id = allowed_role ) INTO is_user_allowed; return is_user_allowed; END $$;

For role_id we use the ENUM role_type . You can read more about ENUM here.

So for all tables that the Platform Admin can have full access, I will simply paste this code (making sure to change the table name).


This function can be used for the other roles.

Example: I want a Principal to be able to access the other schools directory, but they shouldn’t be able to make any changes:

CREATE POLICY "Principal can select" ON "public"."schools" FOR SELECT // Changed to be select only TO authenticated USING ( public.is_user_allowed (auth.uid (), 'organization_admin') );

Unfortunately, if I want to create a policy that allows a user to only SELECT, INSERT and UPDATE, I will need to create three policies since Postgres will only accept one at a time (ALL is considered one in this case).

Step 3.b: Optional) Create a reusable function for access based on user’s organization

What if I want to check if a given user can access a school’s specific data?


In the table `field_trip_schedule` , principals and teachers should only be able to interact with data related to their school. In this example, we have a column `school_id` ([one-to-many relationship](https://www.youtube.com/watch?v=5VrF9OVQ6rg)).


I create a reusable function:


```sql
CREATE
OR REPLACE FUNCTION public.get_user_schools (current_user_id UUID) 
RETURNS UUID[] 
LANGUAGE plpgsql 
SECURITY DEFINER AS $$
DECLARE 
  user_teams UUID[];
BEGIN
  SELECT ARRAY (
		SELECT team_id
    FROM teams_user_profiles_link
    WHERE user_id = current_user_id
) INTO user_teams;

  RETURN user_teams; // returns an array of UUID's
END $$;
```


_Here, I’m purposefully using schools plural because, in this example, a teacher might work in two different schools._


When I call `get_user_schools`, I also use one of Supabase’s helper function — our friend `auth.uid()`:


```sql
CREATE POLICY "Principals and teachers can see their school data" 
ON "public"."field_trip_schedule" 
FOR SELECT
TO authenticated 
USING (
		-- Allow selecting a field trip if its school_id is in the users' access list
    school_id = ANY (public.get_user_schools (auth.uid ()))
  );
```


---


In some cases, you will want to combine functions.


**Example**: Only principals should have access to the school budget table


```sql
CREATE POLICY "Principals can see their school budget" 
ON "public"."school_budget" 
FOR SELECT
TO authenticated 
USING (
    school_id = ANY (public.get_user_schools (auth.uid ()))
		AND public.is_user_allowed (auth.uid (), 'organization_admin')
  );
```

Keep in mind

If you use service_role to create your admin client, <u>RLS will be bypassed</u>. You will have to create your own verification system to ensure that only those with certain privileges can access the API route where the admin client is instantiated.

⚠️ **Never** **expose your** **`service_role`** **on the client side.**

Conclusion

RLS functions offer a significant advantage similar to functions elsewhere: the ability to write reusable code, reducing the overall amount of code you need to write. Hopefully, this article demystified RLS functions, particularly for those who find PostgreSQL intimidating.

I offered one potential solution, so if you have alternative approaches or insights on handling these scenarios, I'm eager to hear from you.