How to implement Column and Row level security in PostgreSQL
How to implement Column and Row level security in PostgreSQL
1. Column-level security
2. Row-level security
3. How to combine row-level security with column grants
4. Application users vs. row-level security
5. Row-level security performance
In this article, we are going to talk about security at a more granular level,
where a column or a row of a table can be secured from a user who has
access to that table but whom we don’t want to allow to see a particular
column or a particular row. So let’s explore these options.
1. Column-level security
2. Row-level security
Let’s explore column-level security first.
Column-level security
What is column-level security?
As the name suggests, at this level of security we want to allow the user to
view only a particular column or set of columns, making all other columns
private by blocking access to them, so users can not see or use those
columns when selecting or sorting. Now let’s see how we can implement this.
CREATE ROLE
postgres=# create table employee ( empno int, ename text, address text,
salary int, account_number text );
CREATE TABLE
postgres=# insert into employee values (1, 'john', '2 down str', 20000,
'HDFC-22001' );
INSERT 0 1
postgres=# insert into employee values (2, 'clark', '132 south avn', 80000,
'HDFC-23029' );
INSERT 0 1
postgres=# insert into employee values (3, 'soojie', 'Down st 17th', 60000,
'ICICI-19022' );
INSERT 0 1
-------+--------+---------------+--------+----------------
(3 rows)
An admin user with full access to the employee table can currently access
salary information, so the first thing we want to do here is to revoke the
admin user’s access to the employee table, then create a view with only
required columns—empno, ename and address—and provide this view
access to the admin user instead.
REVOKE
CREATE VIEW
postgres=# grant SELECT on emp_info TO admin;
GRANT
-------+--------+---------------
(3 rows)
As we can see, admin can find employee information via the emp_info view,
but cannot access the salary and account_number columns from the table.
Column-level permissions
Another good option for securing a column is to grant access to particular
columns only to the intended user. In the above example, we don’t want the
admin user to access the salary and account_number columns of the
employee table. Instead of creating views, we can instead provide access to
all columns except salary and account_number.
Example
Let’s take a look at how this works using queries. We have already revoked
SELECT privileges on the employee table, so admin cannot access
employees.
Now let’s give SELECT permission on all columns except salary and
account_number:
GRANT
-------+--------+---------------
As we see, the admin user has access to the employee table’s columns
except for salary and account_number.
An important thing to remember in this case is that the user should not have
GRANT access on table. You must revoke SELECT access on the table and
provide column access with only columns you want the user to access.
Column access to particular columns will not work if users already have
SELECT access on the whole table.
Column-level encryption
Another way to secure a column is to encrypt just the column data, so the
user can access the column but can not see the actual data. PostgreSQL has
a pgcrypto module for this purpose. Let’s explore this option with the help of
a basic example.
Example
Here we want user admin to see the account_number column, but not the
exact data from that column; at the same time, we want another user,
finance, to be able to access the actual account_number information. To
accomplish this, we will insert data in the employee table using pgcrypto
functions and a secret key.
CREATE ROLE
GRANT
CREATE EXTENSION
postgres=# TRUNCATE TABLE employee;
TRUNCATE TABLE
postgres=# insert into employee values (1, 'john', '2 down str', 20000,
pgp_sym_encrypt('HDFC-22001','emp_sec_key'));
INSERT 0 1
postgres=# insert into employee values (2, 'clark', '132 south avn', 80000,
pgp_sym_encrypt('HDFC-23029', 'emp_sec_key'));
INSERT 0 1
postgres=# insert into employee values (3, 'soojie', 'Down st 17th', 60000,
pgp_sym_encrypt('ICICI-19022','emp_sec_key'));
INSERT 0 1
-------+--------+---------------+--------
+----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-----
(3 rows)
REVOKE
-------+--------+---------------
+----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-----
(3 rows)
If the table owner wants to share actual data with the finance user, the key
can be shared, and finance can view actual data:
-------+--------+---------------
+----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-----
(3 rows)
-------+--------+---------------+-----------------
(3 rows)
When a user who does not have a key tries to see data with a random key,
they get an error:
The method shown above is highly based on trust. The pgcrypto module has
other methods that use private and public keys to do the same work.
Row-level security
What is row-level security?
Row-level security (RLS for short) is an important feature in the PostgreSQL
security context. This feature enables database administrators to define a
policy on a table such that it can control viewing and manipulation of data on
a per user basis. A row-level policy can be understood as an additional filter;
when a user tries to perform an operation on a table, this filter is applied
before any query condition or filtering, and data is shrunk down or access is
denied based on the specific policy.
Row-level security policies can be created specific to a command, such as
SELECT or DML commands (INSERT/UPDATE/DELETE), or with ALL. Row-level
security policies can also be created on a particular role or multiple roles.
Example
As we saw above, we can protect columns and column data from other users
like admin, but we can also protect data at the row level so that only a user
whose data that row contains can view it. So let’s drop the employee table
and recreate it with new data:
DROP TABLE
postgres=# create table employee ( empno int, ename text, address text,
salary int, account_number text );
CREATE TABLE
postgres=# insert into employee values (1, 'john', '2 down str', 20000,
'HDFC-22001' );
INSERT 0 1
postgres=# insert into employee values (2, 'clark', '132 south avn', 80000,
'HDFC-23029' );
INSERT 0 1
postgres=# insert into employee values (3, 'soojie', 'Down st 17th', 60000,
'ICICI-19022' );
INSERT 0 1
-------+--------+---------------+--------+----------------
(3 rows)
Employee john can view only rows that have john’s information. Similarly,
employees clark and soojie can only view information in their respective row,
while the superuser or table owner can view all the information. Now let’s
look at how we can achieve this user-level security using row-level security
policies.
First, create users based on entries in rows and provide table access to
them:
CREATE ROLE
postgres=# grant select on employee to john;
GRANT
CREATE ROLE
GRANT
CREATE ROLE
GRANT
-------+--------+---------------+--------+----------------
(3 rows)
(3 rows)
-------+--------+---------------+--------+----------------
(3 rows)
CREATE POLICY
-------+--------+---------------+--------+----------------
(3 rows)
As we can see, john is still able to view all rows, because creating the policy
alone is not sufficient; we must explicitly enable it. Let’s see how to enable
or disable a policy
ALTER TABLE
Now let’s see what each user can view from the employee table:
current_user
--------------
edb
(1 row)
-------+--------+---------------+--------+----------------
(3 rows)
current_user
--------------
john
(1 row)
-------+-------+------------+--------+----------------
(1 row)
current_user
--------------
clark
(1 row)
(1 row)
current_user
--------------
soojie
(1 row)
-------+--------+--------------+--------+----------------
(1 row)
As we can see, the current_user can only access his or her own row.
If you want one of the users to be able to access all data—for example, let’s
assume soojie is in HR and needs to access all other employee data—let’s
see how to achieve this.
ALTER ROLE
-------+--------+---------------+--------+----------------
(3 rows)
Drop a policy
Let’s take a look at how to drop a policy.
DROP POLICY
The syntax is simple: just provide the policy name and table name to drop
the policy from that table. Now, let’s try to access the data:
current_user
--------------
john
(1 row)
-------+-------+---------+--------+----------------
(0 rows)
As we can see, though we have dropped the policy, user john is still not able
to view any data. This is because the row-level security policy is still enabled
on the employee table.
ALTER TABLE
-------+--------+---------------+--------+----------------
(3 rows)
For example, in the table above, all employees can view only their own
information only, but let’s say we don’t want to show financial information to
employees. We can apply column-level permissions on the employee level as
well.
Right now john can see all of the information, as the policy has been deleted
and row-level security is disabled.
-------+--------+---------------+--------+----------------
(3 rows)
Let’s create a policy and enable row-level security. Now, john can view only
his information:
CREATE POLICY
ALTER TABLE
-------+-------+------------+--------+----------------
(1 row
Next, let’s remove access to the employee table from john and give access
to all columns except the salary and account_number columns. Now, john
can view all his details except for financial information.
postgres=> \c postgres edb
REVOKE
GRANT
-------+-------+------------
(1 row)
Example
Let’s add some more data in our employee table:
postgres=> \c postgres edb
postgres=# insert into employee values (4, 'smith', 'ash dwn str', 85000,
'HDFC-22121' );
INSERT 0 1
postgres=# insert into employee values (5, 'mark', 'lake river south',
61000, 'ICICI-11119' );
INSERT 0 1
postgres=#
-------+--------+------------------+--------+----------------
(5 rows)
So first let’s grant select access to PUBLIC, drop the old policy, and create a
new policy with session variables.
GRANT
postgres=# DROP POLICY emp_rls_policy ON employee;
DROP POLICY
CREATE POLICY
ALTER TABLE
postgres=#
SET
-------+-------+-------------+--------+----------------
(1 row)
SET
-------+-------+---------+--------+----------------
(0 rows)
Row-level security has an additional CHECK clause, which adds yet another
condition, so keep in mind the larger you make your policy, the more
performance impact you may face. Just like optimizing any simple SQL query,
RLS can be optimized by carefully designing these CHECK expressions.