First, you will need to understand how Row Level Security works in Postgres and this application. This application is a simple Rails application that demonstrates how to use Postgres Row Level Security with a Rails application. This application is a simple CRM application with a few users and a few sales leads. The application is designed to show how to use Postgres Row Level Security to restrict access to certain rows in the database based on the user's role.
The application has a few users:
admin
- a user with the ability to create new usersapp_user
- a user who can see all sales leadssalesmanager
- a user who can see all sales leads for their teamsalesperson
- a user who can see all sales leads they are listed as the owner of
The application connects to the database using the app_user
role. The
role app_user
has permissions to switch to roles salesmanager
and
salesperson
. When a user authenticates to the web app, the Rails
application will switch to the permission level associated with that
user.
Don't understand yet? It's okay. This application is designed to help you understand how to use Postgres Row Level Security in a Rails application.
-
ADMIN_DATABASE_URL
: a Postgres connection string as an environmental variable with the pattern that looks likepostgres://user:pass@host:port/postgres
. This role does not need to be a superuser, but does need to have the ability to create new users. -
APP_DATABASE_URL
: similar to above, but a user does not need to exist yet, as it will be created by a following command. This will be the connection string used primarily by the application. The host and port should be the same as theADMIN_DATABASE_URL
above, but the database should be the one intended to be used by the application. -
Run
rake db_roles:create_app_user
, which is a rake command in thelib/tasks/db_roles.rake
file. This command will connect to the database using theADMIN_DATABASE_URL
and create the necessary application user. -
Run
rails db:create
to create the database -
Run
rake db_roles:create_nested_roles
, which will create roles forsalesperson
andsalesmanager
. Then, it will grant the ability for the app user to use those roles. -
Run
rails db:migrate
orrails db:schema:load
to build the database's data structures. If you look at the migrations, you'll see how Row Level Security was implemented in an existing Rails application in the filedb/migrate/20240214170118_add_rls_policies_to_tables.rb
. -
Run
rake db_roles:refresh_permissions
to rerun someGRANT
statements after creating new tables in the database. -
Run
rails db:seed
to add some data to the database. -
Run
rails s
to start your app, and load the app in the browser.
- Click on any of the users to authenticate as that user.
- At the top, you will not see a notice indicating you have authenticated as that person
- See how the the list of "Team Opportunities" changes as you move from Salesmanager to Salesperson. Yet, see how the query sent from Rails does not change.
- Now, click on "Back to all teams". If you are still authenticated, you will not be able to see the other team members. Click "Logout" and all of the team members will show.
First, if you don't know about Postgres Row Level Security functionality, check out this Tutorial on Postgres Row Level Security. This Rails application leverages the underlying database for this type of technology.
This application uses three roles for the security:
app_user
role which has access to all rowssalesmanager
role which has access to all rows for a teamsalesperson
role which has access to all rows they are listed as the owner of
In the list of users above, app_user
is least restrictive and
salesperson
is most restrictive. Three tiers is a bit complex, but two
tiers would be common in a SaaS multi-tenant world where one role is the
end user and another role is an admin.
If you look in the ApplicationController
, you will see before_action :set_rls_if_authenticated
. The set_rls_if_authenicated
method find
the current authenticated user and sets a variable on the Postgres
connection
If you are using a connection pooler,. connection poolers may not maintain the same session settings for a specific command. Because the sessions are set at the beginning of the controller action and not directly adjacent to
If you are using asynchronous threads,