Hands-on with PostgreSQL Authorization, Part 2: Row-Level Security
Explore user authorization using PostgreSQL, moving on to row-level security.
Mar 16, 2022
Greg Schaberg
Senior Infrastructure and Web

Welcome to another exploration of PostgreSQL’s authorization tools! If you’re interested in why you might want to learn about or use PostgreSQL authorization, check out the first couple sections of the first article. Here’s where we are in the series:
Row-Level Security (this article)
Performance of Row-Level Security (coming soon!)
In the first article, we looked into how database roles and granted privileges can be used to control what database objects (e.g. tables, views, functions) can be accessed and modified (with SELECT, INSERT, UPDATE, and DELETE queries) by different users. That article ended with a bit of a cliffhanger: if you build a multi-user application using only roles and GRANTs for authorization, then your users will be able to delete each others’ data or even delete each other! We need another mechanism to limit users to reading and mutating only their own data — that mechanism is row-level security (RLS) policies.
In this article, we’ll use an example-driven approach to dive into the basics of understanding and using RLS policies.
Hands-On With an Example Schema
The best way to understand RLS is to use it! We’ll continue using the previous article’s database schema, roles, and privilege grants (with the addition of a new “songs” table) — we’re modeling an example app akin to Bandcamp, where musical artists can publish albums and songs and fans can discover and follow artists.

Sample schema relating musical artists, albums, songs, and followers (fans).
You can follow along using Docker and the schema at: https://gitlab.com/tangram-vision/oss/tangram-visions-blog/-/tree/main/2022.03.16_PostgreSQLAuthorizationRowLevelSecurity
As described in the repo's README, you can run the command below, which uses the official Postgres Docker image to run a PostgreSQL database locally. The first volume mount will load the schema.sql
file, which will populate your database with the tables diagrammed above.
To open a psql
prompt in that container, run the following in another terminal:
Row-Level Security
What is Row-Level Security (RLS)? It’s a way for PostgreSQL to limit what rows of a table are visible to a query. Typically, if you SELECT * FROM mytable
then PostgreSQL will return all the columns and all the rows from the table. With Row-Level Security enabled on the table, PostgreSQL won’t return any rows (unless you’re querying from a database role that is a superuser, the owner of the table, or has the BYPASSRLS option).
Basic Policies
In order to return rows from a table with RLS enabled, we need to write a policy. Policies use an SQL expression (returning a boolean result) that is evaluated for every row in the table, which determines which rows are accessible to the user submitting the query. Policies can apply to specific roles and/or specific commands (e.g. SELECT, INSERT, ...).
As a first, simple example, let’s add some data to our database and enable row-level security on a table:
Now, let’s try out a couple basic RLS policies:
Policies Based on the Querying User
Let’s get a little more realistic: we want artists to be able to change their own name and be unable to change other artists’ names. To model this situation, we need to know the artist identity that is querying the database — issuing queries from a generic “artist” role (as in the above examples) doesn’t give us enough information. One way to assume a specific identity within the “artist” role/group is to create another database role and make it a member of the “artist” role:
Now, if you log into the database as “artist:1”, then you’ll have all the same privileges as the “artist” group. By using specific, single-user logins (and corresponding usernames), we can write a policy that uses the user name to identify which rows in the database belong to that user.
We successfully implemented permissions that allow an artist to update their name and nobody else’s! This example also introduced:
Limiting a policy to a specific command (e.g. SELECT, UPDATE)
Limiting a policy to a specific role/group (e.g. artist)
By default, policies apply to all commands and to all roles. If a query is issued and there is no policy with matching command and role, then no policies apply and no rows will be visible or affected by the query.
💡 You may see that
artist_id = substr(current_user, 8)::int
expression in the update policy and frown a bit or raise an eyebrow. If so, good! This example was written to be (hopefully) easy to follow, but in a real app you probably wouldn’t want to use usernames that are a string-concatenation of the role/group name with an ID, because then you have tightly-coupled pieces of data that require string operations to extract every time you need them! A better approach might be to log in with a UUID username, which keys into a “users” table that “fans” and “artists” tables join to. There are a few different ways to design database usernames and tie them into RLS policies, which we may explore in a future article.
Policies That Span Tables
Let’s take it one step further! We want artists to be able to create/edit/delete albums (but only their own!) and create/edit/delete songs on those albums. The USING expression in RLS policies can contain any SQL expression, so we can use foreign-key relationships in our data model to span tables when checking permissions.
The notable piece of the above is the policy on the “songs” table. When we issue an INSERT, UPDATE, or DELETE command on that table, the RLS policy ensures that we can only insert, modify, or delete songs in an album associated with the artist issuing the query.
We used an EXISTS subquery for the USING expression in the “songs” table’s policy above, but there are a variety of ways that permission could be implemented. Which way is most performant? Great question! We’ll dig deeper into that in the “Performance of Row-Level Security” article later in this series (make sure to subscribe to our newsletter or blog RSS to get notified).
Interactions of Multiple Policies
We’ve seen that policies can apply:
to all commands (the default) or specific commands (SELECT, INSERT, UPDATE, DELETE)
to all users (the default) or to specific users (e.g. artist, fan)
but one more critical aspect of how RLS policies work is how multiple policies can be combined. Policies can interact in two main ways:
A table can have multiple policies
A policy can query another table that has its own policy
Tables with Multiple Policies
When a table has multiple policies, pay attention to whether the policies are declared as PERMISSIVE (the default) or RESTRICTIVE. A PERMISSIVE policy must exist on a table for any rows to be accessible. If there are multiple PERMISSIVE policies, a row is accessible if any PERMISSIVE policy evaluates to true. On the other hand, RESTRICTIVE policies must all evaluate to true for a row to be accessible. We can explore this behavior by adding a new feature to our sample app — we want to allow artists to create albums with a future release date, but only the owning artist should be able to see these not-yet-released albums.
By combining a PERMISSIVE policy and RESTRICTIVE policies targeting different roles (fans and artists), we’ve made future-release albums visible only to the owning artist. Arguably, a better way to represent this logic would be to use only PERMISSIVE policies, as follows:
Now, the viewable_by_all
policy lets everyone see albums with a release date in the past, and the affect_own_albums
policy lets artists do anything (SELECT, INSERT, etc.) to albums they own.
Querying Other Tables with Their Own Policies
The other way that multiple policies can interact is when a policy’s USING expression queries another table that has its own policy. In our sample app, we can use the policy on the albums table to determine whether the songs in that album should be visible:
Finally, let’s test out our new policies to make sure that the correct roles/groups see (or cannot see!) albums with future release dates and their songs.
Success! We’ll conclude this overview of Row-Level Security in PostgreSQL here; there’s lots more to learn (check out the docs on Row Security Policies and the CREATE POLICY command), but we’ve covered the basics. So far, I’ve left a big question unanswered: what are the performance implications of using policies, especially when they contain complex USING expressions (e.g. querying other tables, using joins, calling functions)? We’ll dig into that question in the next article!
Thanks for reading! I hope you learned something new and will return for the next article in the series. If you have any corrections to this article or suggestions/questions for future articles, please let me know or send us a tweet, and if you’re curious to learn more about how we improve perception sensors, visit us at Tangram Vision.