<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=RLS_example_2</id>
	<title>RLS example 2 - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://tech.uvoo.io/index.php?action=history&amp;feed=atom&amp;title=RLS_example_2"/>
	<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=RLS_example_2&amp;action=history"/>
	<updated>2026-04-24T04:39:42Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.35.2</generator>
	<entry>
		<id>https://tech.uvoo.io/index.php?title=RLS_example_2&amp;diff=5648&amp;oldid=prev</id>
		<title>Busk: Created page with &quot;Of course\! To set up Postgres Row-Level Security (RLS) based on group membership, you need a schema that links users to groups and resources to groups. The RLS policy then ch...&quot;</title>
		<link rel="alternate" type="text/html" href="https://tech.uvoo.io/index.php?title=RLS_example_2&amp;diff=5648&amp;oldid=prev"/>
		<updated>2025-09-18T15:08:26Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;Of course\! To set up Postgres Row-Level Security (RLS) based on group membership, you need a schema that links users to groups and resources to groups. The RLS policy then ch...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;Of course\! To set up Postgres Row-Level Security (RLS) based on group membership, you need a schema that links users to groups and resources to groups. The RLS policy then checks if the current user is a member of the group that owns the row.&lt;br /&gt;
&lt;br /&gt;
Here’s a complete, step-by-step example.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### \#\# 1. The Database Schema 🏛️&lt;br /&gt;
&lt;br /&gt;
First, we need tables to manage users, groups, their relationships, and the projects we want to protect.&lt;br /&gt;
&lt;br /&gt;
  * **`groups`**: A simple table for our user groups.&lt;br /&gt;
  * **`users`**: Stores user information. The `id` will match the JWT `sub` claim.&lt;br /&gt;
  * **`user_groups`**: A **junction table** that links users and groups in a many-to-many relationship. This is the key to the whole setup.&lt;br /&gt;
  * **`projects`**: The resource we want to secure. Each project belongs to one group.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;!-- end list --&amp;gt;&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- A table for user roles or groups&lt;br /&gt;
CREATE TABLE groups (&lt;br /&gt;
    id SERIAL PRIMARY KEY,&lt;br /&gt;
    name TEXT NOT NULL UNIQUE&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- A table for users, where 'id' corresponds to the JWT 'sub'&lt;br /&gt;
CREATE TABLE users (&lt;br /&gt;
    id TEXT PRIMARY KEY, -- Stores the JWT 'sub' claim&lt;br /&gt;
    username TEXT NOT NULL UNIQUE&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- The junction table linking users to groups&lt;br /&gt;
CREATE TABLE user_groups (&lt;br /&gt;
    user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,&lt;br /&gt;
    group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE,&lt;br /&gt;
    PRIMARY KEY (user_id, group_id) -- Ensures a user can't be in the same group twice&lt;br /&gt;
);&lt;br /&gt;
&lt;br /&gt;
-- The table with data we want to protect with RLS&lt;br /&gt;
CREATE TABLE projects (&lt;br /&gt;
    id SERIAL PRIMARY KEY,&lt;br /&gt;
    name TEXT NOT NULL,&lt;br /&gt;
    group_id INTEGER NOT NULL REFERENCES groups(id) ON DELETE CASCADE&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### \#\# 2. Populating with Sample Data 📝&lt;br /&gt;
&lt;br /&gt;
Let's add some data to make the example tangible.&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Create groups&lt;br /&gt;
INSERT INTO groups (name) VALUES ('Admins'), ('Developers'), ('Viewers');&lt;br /&gt;
&lt;br /&gt;
-- Create users&lt;br /&gt;
INSERT INTO users (id, username) VALUES&lt;br /&gt;
('auth0|user123', 'alice'), -- Alice is a Developer&lt;br /&gt;
('auth0|user456', 'bob'),   -- Bob is an Admin and a Developer&lt;br /&gt;
('auth0|user789', 'charlie'); -- Charlie is a Viewer&lt;br /&gt;
&lt;br /&gt;
-- Assign users to groups&lt;br /&gt;
INSERT INTO user_groups (user_id, group_id) VALUES&lt;br /&gt;
('auth0|user123', 2), -- Alice is in 'Developers'&lt;br /&gt;
('auth0|user456', 1), -- Bob is in 'Admins'&lt;br /&gt;
('auth0|user456', 2), -- Bob is also in 'Developers'&lt;br /&gt;
('auth0|user789', 3); -- Charlie is in 'Viewers'&lt;br /&gt;
&lt;br /&gt;
-- Create projects and assign them to groups&lt;br /&gt;
INSERT INTO projects (name, group_id) VALUES&lt;br /&gt;
('Project Alpha', 2),     -- Belongs to 'Developers'&lt;br /&gt;
('Project Omega', 2),     -- Belongs to 'Developers'&lt;br /&gt;
('Admin Dashboard', 1),   -- Belongs to 'Admins'&lt;br /&gt;
('Public Analytics', 3);  -- Belongs to 'Viewers'&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### \#\# 3. Creating the RLS Policy 🛡️&lt;br /&gt;
&lt;br /&gt;
Now, we'll enable RLS on the `projects` table and create a policy. The policy will check if the current user's ID exists in the `user_groups` table associated with the project's `group_id`.&lt;br /&gt;
&lt;br /&gt;
This is the most important step. We'll use a subquery in the `USING` clause.&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- 1. Enable Row-Level Security on the 'projects' table&lt;br /&gt;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;&lt;br /&gt;
&lt;br /&gt;
-- 2. Force RLS for the table owner as well (good practice)&lt;br /&gt;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;&lt;br /&gt;
&lt;br /&gt;
-- 3. Create the policy&lt;br /&gt;
CREATE POLICY project_access_based_on_group&lt;br /&gt;
ON projects&lt;br /&gt;
FOR ALL  -- The policy applies to SELECT, INSERT, UPDATE, and DELETE&lt;br /&gt;
USING (&lt;br /&gt;
    -- This subquery checks if a link exists in our junction table&lt;br /&gt;
    -- between the current user and the project's group.&lt;br /&gt;
    EXISTS (&lt;br /&gt;
        SELECT 1&lt;br /&gt;
        FROM user_groups&lt;br /&gt;
        WHERE&lt;br /&gt;
            -- Match the user_id in the junction table with the current user's JWT sub&lt;br /&gt;
            user_groups.user_id = current_setting('request.jwt.claim.sub', true)&lt;br /&gt;
            -- And match the group_id with the group of the project row being checked&lt;br /&gt;
            AND user_groups.group_id = projects.group_id&lt;br /&gt;
    )&lt;br /&gt;
);&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**What this policy does:** For any query on the `projects` table, Postgres will evaluate each row against the `USING` clause. A row is only visible or modifiable if the `EXISTS` subquery returns `true`, meaning the current user is a member of the group that owns that specific project.&lt;br /&gt;
&lt;br /&gt;
-----&lt;br /&gt;
&lt;br /&gt;
### \#\# 4. Testing the Policy ✅&lt;br /&gt;
&lt;br /&gt;
We can simulate requests from different users by setting the `request.jwt.claim.sub` variable.&lt;br /&gt;
&lt;br /&gt;
#### **Test 1: As Alice (Developer)**&lt;br /&gt;
&lt;br /&gt;
Alice is in the 'Developers' group (ID 2). She should only see 'Project Alpha' and 'Project Omega'.&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Simulate a request from Alice&lt;br /&gt;
SET request.jwt.claim.sub = 'auth0|user123';&lt;br /&gt;
&lt;br /&gt;
-- Query the projects table&lt;br /&gt;
SELECT * FROM projects;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**Result:**&lt;br /&gt;
| id | name | group\_id |&lt;br /&gt;
| :-- | :-- | :--- |&lt;br /&gt;
| 1 | Project Alpha | 2 |&lt;br /&gt;
| 2 | Project Omega | 2 |&lt;br /&gt;
&lt;br /&gt;
#### **Test 2: As Bob (Admin &amp;amp; Developer)**&lt;br /&gt;
&lt;br /&gt;
Bob is in two groups. He should see the Admin Dashboard and the Developer projects.&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Simulate a request from Bob&lt;br /&gt;
SET request.jwt.claim.sub = 'auth0|user456';&lt;br /&gt;
&lt;br /&gt;
-- Query the projects table&lt;br /&gt;
SELECT * FROM projects;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**Result:**&lt;br /&gt;
| id | name | group\_id |&lt;br /&gt;
| :-- | :-- | :--- |&lt;br /&gt;
| 1 | Project Alpha | 2 |&lt;br /&gt;
| 2 | Project Omega | 2 |&lt;br /&gt;
| 3 | Admin Dashboard | 1 |&lt;br /&gt;
&lt;br /&gt;
#### **Test 3: As a user not in any groups**&lt;br /&gt;
&lt;br /&gt;
If we simulate a user who hasn't been added to `user_groups`, they should see nothing.&lt;br /&gt;
&lt;br /&gt;
```sql&lt;br /&gt;
-- Simulate a request from an unassigned user&lt;br /&gt;
SET request.jwt.claim.sub = 'auth0|unassigned999';&lt;br /&gt;
&lt;br /&gt;
-- Query the projects table&lt;br /&gt;
SELECT * FROM projects;&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
**Result:**&lt;br /&gt;
(0 rows)&lt;br /&gt;
&lt;br /&gt;
This pattern provides a powerful and scalable way to manage permissions for your application directly within the database.&lt;/div&gt;</summary>
		<author><name>Busk</name></author>
	</entry>
</feed>