How to reference query values in Postgres res policy - postgresql

I am trying to build a system with Postgres in which users can connect with each other by sending requests. I am handling the security logic with RLS. However, I am having some trouble with the structure of one of my policies.
Here are the tables of concern, stripped of any nonessential columns:
CREATE TABLE profile (
id UUID PRIMARY KEY,
name text
);
CREATE TABLE request (
id serial PRIMARY KEY,
sender_id UUID,
recipient_id UUID
);
CREATE TABLE connection (
id serial PRIMARY KEY,
owner_id UUID,
contact_id UUID
);
Users are only allowed to add a connection if:
There is a row which references their profile id in recipient_id in the request table.
There is no existing connection that involves both users (no matter which one is the owner_id and which is the contact_id)
Here is the Policy I've written:
CREATE POLICY "Participants INSERT" ON public.connection FOR
INSERT
WITH CHECK (
auth.uid() = owner_id
AND NOT EXISTS(
SELECT
*
FROM
public.connection c
WHERE
(
(
c.owner_id = owner_id --> Problem: How to reference query
AND c.contact_id = contact_id
)
OR (
c.contact_id = owner_id
AND c.owner_id = contact_id
)
)
)
AND EXISTS(
SELECT
*
FROM
public.request r
WHERE
(
r.sender_id = contact_id
AND r.recipient_id = owner_id
)
)
);
However, whenever I try to create a contact with a user whose id is already present in any row in the connection table (irrespective if it exists together with the correct contact_id), I get a policy violation error.
I think I might be referencing owner_id and contact_id wrong in the subquery because if I replace them manually with the appropriate id as a string, it works.
I'd really appreciate everyones input.

I found the answer. The parent query can be accessed through the name of the table. So all I had to do was to access owner_id and contact_id like this:
connection.owner_id
connection.contact_id
On a side-note: The query values can NOT be accessed like this:
public.connection.owner_id
public.connection.contact_id

Related

PostgreSQL: Is there a way to insert into multiple tables at once in order to satisfy RLS policies?

I have tables that represent relationships between different persons (legal and natural) by membership. The typical case being people being members of organizations. So, I did this:
CREATE TABLE
person (
id uuid PRIMARY KEY,
created_at timestamp with time zone DEFAULT now(),
modified_at timestamp with time zone,
card jsonb -- this contains the details in JSON inspired by vCard, e.g. '{ "fn": "Full Name", "url":"http://my.site.org/" }
);
CREATE TABLE
member (
organization uuid references person (id) on delete cascade not null,
member uuid references person (id) on delete cascade not null,
begin timestamp with time zone DEFAULT (now() AT TIME ZONE 'utc'),
"end" timestamp with time zone,
primary key (organization, member)
);
I am using RLS and I want to allow a user to insert a person into any organization they have write-permission to. For that, I have an SQL function can() that gives me all the organizations (person row IDs) they may write to. So, I wrote the following policy:
CREATE POLICY "Person can be created in organization with write-permission." ON person
FOR INSERT WITH CHECK (
EXISTS( SELECT 1 FROM member WHERE can('write') ? organization::text AND member = id )
);
My question: In order to fulfill this policy, I need to simultaneously insert the new person and the corresponding member link. Is this even possible?
I tried both
WITH new_member AS (
INSERT INTO member (organization, member)
VALUES ( 'cb00aaa9-4c95-4f1a-862e-21406d4d10e0', gen_random_uuid() )
RETURNING member )
INSERT INTO person ( id, card )
SELECT member, '{"fn":"Coherent Insert?"}'
FROM new_member;
and
WITH new_person as (
INSERT INTO person (id, card)
VALUES (gen_random_uuid(), '{"fn":"Coherent Insert?"}')
RETURNING id )
INSERT INTO member ( organization, member )
SELECT 'cb00aaa9-4c95-4f1a-862e-21406d4d10e0', id from new_person;
but both complain about:
ERROR: new row violates row-level security policy for table "person"
I could of course put both inserts into a SECURITY definer function without RLS and check privileges manually there but I am wondering if there is another way.

Reference a Column From Another Table in PostgreSQL

I want to create the following tables (simplified to the keys for example):
CREATE TABLE a (
TestVer VARCHAR(50) PRIMARY KEY,
TestID INT NOT NULL
);
CREATE TABLE b (
RunID SERIAL PRIMARY KEY,
TestID INT NOT NULL
);
Where TestID is not unique, but I want table b's TestID to only contain values from table a's `TestID'.
I'm fairly certain I can't make it a foreign key, as the target of a foreign key has to be either a key or unique, and found that supported by this post.
It appears possible with Triggers according to this post where mine on insert would look something like:
CREATE TRIGGER id_constraint FOR b
BEFORE INSERT
POSITION 0
AS BEGIN
IF (NOT EXISTS(
SELECT TestID
FROM a
WHERE TestID = NEW.TestID)) THEN
EXCEPTION my_exception 'There is no Test with id=' ||
NEW.TestID;
END
But I would rather not use a trigger. What are other ways to do this if any?
A trigger is the only way to continuously maintain such a constraint, however you can delete all unwanted rows as part of a query that uses table b:
with clean_b as (
delete from b
where not exists (select from a where a.TestID = b.TestID)
)
select *
from b
where ...

Insert data into strongly normalized DB and maintain the integrity (Postgres)

I'm trying to develop a simple database for the phonebook. This is what I wrote:
CREATE TABLE phone
(
phone_id SERIAL PRIMARY KEY,
phone CHAR(15),
sub_id INT, -- subscriber id --
cat_id INT -- category id --
);
CREATE TABLE category
(
cat_id SERIAL PRIMARY KEY, -- category id --
cat_name CHAR(15) -- category name --
);
CREATE TABLE subscriber
(
sub_id SERIAL PRIMARY KEY,
name CHAR(20),
fname CHAR(20), -- first name --
lname CHAR(20), -- last name --
);
CREATE TABLE address
(
addr_id SERIAL PRIMARY KEY,
country CHAR(20),
city CHAR(20),
street CHAR(20),
house_num INT,
apartment_num INT
);
-- many-to-many relation --
CREATE TABLE sub_link
(
sub_id INT REFERENCES subscriber(sub_id),
addr_id INT
);
I created a link table for many-to-many relation because few people can live at the same address and one person can live in different locations at different times.
But I cannot figure out how to add data in strongly normalized DB like this and maintain the integrity of the data.
The first improvement was that I added inique key on address table bacause this table should not contain duplicated data:
CREATE TABLE address
(
addr_id SERIAL PRIMARY KEY,
country CHAR(20),
city CHAR(20),
street CHAR(20),
house_num INT,
apartment_num INT,
UNIQUE (country, city, street, house_num, apartment_num)
);
Now the problem is how to add a new record about some person into DB. I think I should use the next order of actions:
Insert a record into subscriber table, because sub_link and phone tables must use id of a new subscriber.
Insert a record into address table because addr_id must exist before adding record into sub_link.
Link last records from subscriber and address in sub_link table. But at this step I have a new problem: how can I get sub_id and addr_id from steps 1) and 2) in PostgreSQL effectively?
Then I need to insert a record into the phone table. As at 3) step I dont know how to get sub_id from previous queries effectively.
I read about WITH block in the Postgres but I cannot figure out how to use it in my case.
UPDATE
I've done like ASL suggested:
-- First record --
WITH t0 AS (
WITH t1 AS (
INSERT INTO subscriber
VALUES(DEFAULT, 'Twilight Sparkle', NULL, NULL)
RETURNING sub_id
),
t2 AS (
INSERT INTO address
VALUES(DEFAULT, 'Equestria', 'Ponyville', NULL, NULL, NULL)
RETURNING addr_id
)
INSERT INTO sub_link
VALUES((SELECT sub_id FROM t1), (SELECT addr_id FROM t2))
)
INSERT INTO phone
VALUES (DEFAULT, '000000', (SELECT sub_id FROM t1), 1);
But I have an error: WITH clause containing a data-modifying statement must be at the top level
LINE 2: WITH t1 AS (INSERT INTO subscriber VALUES(DEFAULT,
You can do it all in one query using a WITH block with a RETURNING clause. See PostgreSQL docs on INSERT. For example:
WITH t1 AS (INSERT INTO subscriber VALUES ... RETURNING sub_id),
t2 AS (INSERT INTO address VALUES ... RETURNING addr_id)
INSERT INTO sub_link VALUES ((SELECT sub_id FROM t1), (SELECT addr_id FROM t2))
Note that this simple form will only work when inserting a single row into each table.
This is somewhat off the topic of your question, but I suggest you also consider making sub_id and cat_id columns in the phone table foreign keys (use REFERENCES).
You got the idea. Insert data from topmost tables so that you have their IDs before inserting references to them.
In PostgreSQL you can use INSERT/UPDATE ... RETURNING id construct. If you are not using some ORM which do it automatically, this may be useful.
The only thing here is that in step 2 you probably want to check if the address already exists before inserting:
SELECT addr_id FROM address WHERE country = ? AND city = ? ...

Retrieve values from 2 tables given a value in a third map/join table

I have a table for lawyers:
CREATE TABLE lawyers (
id SERIAL PRIMARY KEY,
name VARCHAR,
name_url VARCHAR,
pic_url VARCHAR(200)
);
Imagine the whole table looks like this:
And a table for firms:
CREATE TABLE firms (
id SERIAL PRIMARY KEY,
name VARCHAR,
address JSONb
);
Whole table:
Then to map many to many relationship I'm using a map table lawyers_firms:
CREATE TABLE lawyers_firms (
lawyer_id INTEGER,
firm_id INTEGER
);
I'm not sure how to retrieve values from lawyersand from firmsgiven a lawyers_firms.firm_id.
For example:
1. SELECT name, name_url and pic_url FROM lawyers.
2. also SELECT name and address FROM firms.
3. WHERE `lawyers_firms.firm_id` = 1.
Try this:
SELECT l.name, l.name_url, l.pic_url, f.name, f.address
FROM lawyers l
inner join lawyers_firms lf
on lf.lawyer_id = l.id
inner join firms f
on f.id = lf.firm_id
WHERE lf.firm_id = 1;

How to insert a primary/foreign key row in PostgreSQL

I'm populating a database in PostgreSQL for a Newspaper Online. Now my doubt lies on how to insert a value into a table which only attribute is both a primary and a foreign key.
In this context, the admin is the first person to ever register an account. So idAdmin = idA = 1:
CREATE TABLE AUTENTICADO (
idA serial NOT NULL ,
login VARCHAR(45) NOT NULL,
password VARCHAR(45) NOT NULL,
email VARCHAR(40) NOT NULL,
PRIMARY KEY (idA) );
CREATE TABLE ADMIN (
idAdmin INT NOT NULL REFERENCES AUTENTICADO (idA),
PRIMARY KEY (idAdmin) );
It would be logical to insert values into 'ADMIN' as I tried below, although it is obviously not possible considering 'idAdmin' is a primary key (and a foreign key).
INSERT INTO AUTENTICADO VALUES ('john','adadfsfsdfs', 'john#random.com')
INSERT INTO ADMIN VALUES (1)
Is there a way to register that the first user to create an account (idA = 1) is the admin (idAdmin = idA = 1) ?
although it is obviously not possible considering 'idAdmin' is a
primary key (and a foreign key).
So what?
If you fix the first query to list the columns and use a returning clause to get the auto-generated value for the SERIAL ID, it just works:
INSERT INTO AUTENTICADO(login,password,email)
VALUES ('john','adadfsfsdfs', 'john#random.com')
returning idA;
Result:
ida
-----
1
(1 row)
Second query:
insert into admin values(1);
select * from admin;
Result:
idadmin
---------
1