Row Level Security (RLS) without RLS. Intrigued? Yes? Then keep reading


From a Security or Multi-Tenant perspective, having the ability to limit access on a row-by-row basis is a must, What happens though if your database of choice does not provide this feature out of the box? At Cockroach Labs we partner with a security provider called Satori to enable this feature when using our database. Still, sometimes there is a desire for our customers to try and do it themselves without the need for another vendor's product to be added to their solutions architecture stack.

This blog provides us with another way of achieving RLS using permissions and RBAC controls


Open a client and connect to a CockroachDB instance.

We will create a table and views for each user and show how only users that need to see their data are the only ones that can!



CREATE TABLE public.customers (
     id UUID NOT NULL DEFAULT gen_random_uuid(),
     forename STRING NULL,
     surname STRING NULL,
     “tenant” STRING NOT NULL,
     CONSTRAINT customers_pkey PRIMARY KEY (id ASC),
INDEX customers_forename_surname_idx (forename ASC, surname ASC) WHERE “tenant” = ‘adam’:::STRING,
INDEX customers_forename_surname_idx1 (forename ASC, surname ASC) WHERE “tenant” = ‘dan’:::STRING,
INDEX customers_forename_surname_idx2 (forename ASC, surname ASC) WHERE “tenant” = ‘gareth’:::STRING
 );

The above table gives us some basic customer data but also allocates a tenant column which is what we base our permissions on to give us the ability to exclude data from the people who don't need to see it.



 CREATE VIEW public.dan_customers (
     id,
     forename,
     surname,
     “tenant”
 ) AS SELECT
         customers.id, customers.forename, customers.surname, customers.tenant
     FROM
         p_test.public.customers
     WHERE
         “tenant” = ‘dan’;

 CREATE VIEW public.gareth_customers (
     id,
     forename,
     surname,
     “tenant”
 ) AS SELECT
         customers.id, customers.forename, customers.surname, customers.tenant
     FROM
         p_test.public.customers
     WHERE
         “tenant” = ‘gareth’;

What we have done is created views that we grant access to based on who needs to see that data and then we will secure access to the views.


select * from customers;
                  id                  | forename | surname  | tenant
---------------------------------------+----------+----------+---------
 02cc3f89-73b0-47e2-b098-d30562ab8d4b | Adam     | Spindler | dan
 31fd05cc-ce39-4229-97a8-c3b8a883c79e | Max      | Bashlawi | gareth
 90cc0fdc-cabe-4dd1-8f6b-2cd906d36850 | Dan      | Holt     | dan
 be72b4f1-17b2-4f4f-954b-7f6e39e8581c | Chris    | Dixon    | gareth
 d5f95021-aaa7-4fca-9336-414407f910bd | Paul     | Lovett   | dan


We add Grants to Select from dan_customers for our user account called dan. We also only allow user dan to INSERT into the customer's table too. We can see this implemented below.

show grants for dan;

 database_name | schema_name | relation_name | grantee | privilege_type | is_grantable
----------------+-------------+---------------+---------+----------------+---------------
 p_test        | public      | customers     | dan     | INSERT         |      f
 p_test        | public      | dan_customers | dan     | SELECT         |      f
show grants for gareth;
 database_name | schema_name |  relation_name   | grantee | privilege_type | is_grantable
----------------+-------------+------------------+---------+----------------+---------------
 p_test        | public      | customers        | gareth  | INSERT         |      f
 p_test        | public      | gareth_customers | gareth  | SELECT         |      f


Now let us run an explain analyze of the query to see what is happening beneath.

EXPLAIN ANALYZE Select * from dan_customer s;

info
-------------------------------------------------------------------------------------------------
 planning time: 706µs
 execution time: 3ms
 distribution: full
 vectorized: true
 rows read from KV: 3 (185 B, 1 gRPC calls)
 cumulative time spent in KV: 350µs
 maximum memory usage: 50 KiB
 network usage: 800 B (3 messages)
 sql cpu time: 119µs
 regions: eu-west-2  • render
 │
 └── • scan
       nodes: n10
       regions: eu-west-2
       actual row count: 3
       KV time: 350µs
       KV contention time: 0µs
       KV rows read: 3
       KV bytes read: 185 B
       KV gRPC calls: 1
       estimated max memory allocated: 20 KiB
       sql cpu time: 30µs
       estimated row count: 3 (60% of the table; stats collected 17 minutes               ago; using stats forecast for 16 minutes ago)
       table: customers@customers_forename_surname_idx1 (partial index)
       spans: FULL SCAN

There you have it. It even utilises the indexes we created too!

Give it a whirl..........