Home > database >  Create Postgres Policy for Row Level Security on foreign key attribute
Create Postgres Policy for Row Level Security on foreign key attribute

Time:02-10

I'm trying to create a Policy on Postgres to enable Row Level Security but I'm having a bit of trouble. Many examples I've found have a direct FK on the table, but I'm trying to do it on an attribute on the foreign table.

The context here is that I'm trying to have users only be able to see other users from the same organization. The user is set on a configuration parameter organization.current_tenant, which takes on the value of the Organization.key

Setup:

CREATE TABLE organization (id serial primary key, name text, key text);
CREATE TABLE myuser (id serial primary key, name text, user_organization_id integer);

INSERT INTO organization (name, key) values ('org1', 'org1-key');
INSERT INTO organization (name, key) values ('org2', 'org2-key');

INSERT INTO myuser (name, user_organization_id) values ('org1_agent1', 1);
INSERT INTO myuser (name, user_organization_id) values ('org2_agent1', 2);
INSERT INTO myuser (name, user_organization_id) values ('org2_agent2', 2);

ALTER TABLE myuser ENABLE ROW LEVEL SECURITY;
ALTER TABLE myuser FORCE ROW LEVEL SECURITY;

When using a direct FK, organization_id:

CREATE POLICY access_tenant_data ON myuser
USING (user_organization_id::TEXT = current_setting('organization.current_tenant'));

This works since user_organization_id is the foreign key available on the user table.

However, what I want to use is instead something like user.organization.key.

CREATE POLICY access_tenant_data ON myuser
USING (
    key IN (
        SELECT key FROM 
        myuser U INNER JOIN organization O
        ON U.organization_id = O.id
    )::TEXT = current_setting('organization.current_tenant')
);

But this obviously doesn't work. I'm not sure how to proceed from here.

Testing it out:

SET organization.current_tenant = "org1-key";
SELECT * from myuser;

CodePudding user response:

That should be fairly simple:

CREATE POLICY access_tenant_data ON myuser
USING (
   EXISTS (SELECT 1 FROM organization AS o
           WHERE myuser.user_organization_id = o.id
             AND o.key = current_setting('organization.current_tenant')
          )
);

That can be fairly efficient, since PostgreSQL can compute the resulting query using a semi-join.

CodePudding user response:

You need to define a function that returns BOOLEAN. Within the function you will determine logic and return TRUE or FALSE. The vital piece is that : USING clause of a policy expects a BOOLEAN value and we can dictate the behavior of the policy (allow/disallow) based on what gets returned by the function - True or False.

Here is the sample FUNCTION to be used in the policy:

create function KeyExist(organization_id int, env_current_tenant text)  returns boolean
language plpgsql
as
$$
declare
    flag boolean;
    keyValue text;
begin
select key into keyValue from organization where id=organization_id;
if not found then
    flag := false;
else
    if keyValue = env_current_tenant then
        flag := true;
    else
        flag := false;
    end if;
end if;
return flag;
end; $$;

Policy:

CREATE POLICY access_tenant_data ON myuser for select
USING (KeyExist(user_organization_id,current_setting('organization.current_tenant')));

Demonstration:

postgres=# create function KeyExist(organization_id int, env_current_tenant text)  returns boolean
postgres-# language plpgsql
postgres-# as
postgres-# $$
postgres$# declare
postgres$# flag boolean;
postgres$# keyValue text;
postgres$# begin
postgres$# select key into keyValue from organization where id=organization_id;
postgres$# if not found then
postgres$# flag := false;
postgres$# else
postgres$# if keyValue = env_current_tenant then
postgres$#
postgres$# flag := true;
postgres$# else
postgres$#
postgres$# flag := false;
postgres$# end if;
postgres$# end if;
postgres$# return flag;
postgres$# end; $$;
CREATE FUNCTION

postgres=# CREATE POLICY access_tenant_data ON myuser for select
postgres-# USING (KeyExist(user_organization_id,current_setting('organization.current_tenant')));
CREATE POLICY

Verification:

postgres=> SET organization.current_tenant = "org1-key";
SET
postgres=> select * from myuser;
 id |    name     | user_organization_id
---- ------------- ----------------------
  1 | org1_agent1 |                    1
(1 row)


postgres=> SET organization.current_tenant = 'org2-key';
SET
postgres=> select * from myuser;
 id |    name     | user_organization_id
---- ------------- ----------------------
  2 | org2_agent1 |                    2
  3 | org2_agent2 |                    2
(2 rows)

Tables:

postgres=# \d myuser
                                   Table "public.myuser"
        Column        |  Type   | Collation | Nullable |              Default
---------------------- --------- ----------- ---------- ------------------------------------
 id                   | integer |           | not null | nextval('myuser_id_seq'::regclass)
 name                 | text    |           |          |
 user_organization_id | integer |           |          |
Indexes:
    "myuser_pkey" PRIMARY KEY, btree (id)
Policies (forced row security enabled):
    POLICY "access_tenant_data" FOR SELECT
      USING (keyexist(user_organization_id, current_setting('organization.current_tenant'::text)))

postgres=# select * from myuser;
 id |    name     | user_organization_id
---- ------------- ----------------------
  1 | org1_agent1 |                    1
  2 | org2_agent1 |                    2
  3 | org2_agent2 |                    2
(3 rows)

postgres=# select * from organization;
 id | name |   key
---- ------ ----------
  1 | org1 | org1-key
  2 | org2 | org2-key
(2 rows)
  • Related