PostgreSQL row-level security involving foreign key with other table - sql

I wonder if the following is possible in PostgreSQL using RLS (or any other mechanism). I want a user to be able to get certain rows of a table if its id matches a column in another table.
For e.g. we have following tables:
"user" table:
columns: id, name
| id | name |
| --- | --- |
| 1 | one |
| 2 | two |
| 3 | three|
| 4 | four |
"tenant" table:
columns: id, name
| id | name |
| --- | --- |
| 1 | t1 |
| 2 | t2 |
"user_tenant" table:
columns: user_id, tenant_id
| user_id | tenant_id|
| --- | --- |
| 1 | t1 |
| 2 | t2 |
| 3 | t1 |
| 4 | t2 |
Now I want only users who has same tenant_id.
output:
| id | name |
| --- | --- |
| 1 | one |
| 3 | three|
To achieve this, I need to create policy something like this:
CREATE POLICY tenant_policy ON "user" USING (tenant_id = current_setting('my_user.current_tenant')::uuid);
but with above policy it's not working as I am getting all users.
Note: user & tenant table have many-to-many relationship.
P.S. I know we can do this either by join or some other condition. But I want to achieve the above output using PostgreSQL using RLS(row level security)
Thanks in advance!!

If row level security is not working that may be because one of the following applies:
you didn't enable row level security:
ALTER TABLE "user" ENABLE ROW LEVEL SECURITY;
the user owns the table
You can enable row level security for the owner with
ALTER TABLE "user" FORCE ROW LEVEL SECURITY;
you are a superuser, which is always exempt from RLS
you are a user defines with BYPASSRLS
the parameter row_security is set to off
Other than that, you will probably have to join with user_tenant in your policy:
CREATE POLICY tenant_policy ON "user"
USING (
EXISTS(SELECT 1 FROM user_tenant AS ut
WHERE ut.user_id = "user".id
AND ut.tenant_id = current_setting('my_user.current_tenant')::uuid
)
);

Related

Check if value of referenced row matches value in current row

See the following database structure:
v---------------------------------------------------|
v----------------------------|---------------------------| |
+---------------+ +----+---------+------+ +----+---------+---------+-----+
| id | username | | id | user_id | tag | | id | user_id | message | tag |
+----+----------+ +----+---------+------+ +----+---------+---------+-----+
| 1 | User1 | | 1 | 1 | tech | | 1 | 1 | Test1 | 1 |
| 2 | User2 | | 2 | 1 | news | | 2 | 2 | Test2 | 1 |
+----+----------+ +----+---------+------+ +----+---------+---------+-----+
users tags messages
tags.user_id and messages.user_id both reference users.id. messages.tag references tags.id.
Users have tags available (rows in tags where rows.user_id = users.id) and messages (rows in messages where messages.user_id = users.id).
The problem is that any tag can be "attached" to the message, instead of only tags that are owned by the user. So I need an extra restriction that ensures that the tag referenced in messages.tag not only exists (foreign key restriction), but is also owned by the same user as the message itself (messages.user_id = tags.user_id).
I have not found a way yet to achieve this restriction, which is why I'm asking help.
python: 3.8.10
sqlite3.version: 2.6.0
sqlite3.sqlite_version: 3.31.1
From the manual creating a composite FK in Sqlite3 looks like:
CREATE TABLE parent(a PRIMARY KEY, c, d, e, f);
CREATE UNIQUE INDEX i1 ON parent(c, d);
CREATE TABLE child3(j, k, FOREIGN KEY(j, k) REFERENCES parent(c, d));

select values or NULL if it does not exists

I have a table user with columns id (primary key) and name (unique constraint) containing:
+----+------+
| id | name |
+----+------+
| 1 | u1 |
| 2 | u2 |
| 3 | u3 |
+----+------+
I would like a make a query, something like SELECT id, name FROM user WHERE name IN (?,?), with parameters ("u2", "u4"), that I can reuse in CTE, but which returns:
+------+------+
| id | name |
+------+------+
| 2 | u2 |
| NULL | u4 |
+------+------+
(edit: s/NULL/u4).
Is it possible? I'm interested in MariaDB and PostgreSQL.
One use case would be to insert the ids result in another table where NULL is not accepted, so it would insert the names and check for invalid values in only 1 request. (no need for 2 separates requests + application code).
There are different ways.. An easy one in Postgresql could be:
select a, "name"
from unnest(ARRAY['u2', 'u4']::text[]) AS a
LEFT JOIN "user" AS u ON a = u."name";

Snowflake Create View with JSON (VARIANT) field as columns with dynamic keys

I am having a problem creating VIEWS with Snowflake that has VARIANT field which stores JSON data whose keys are dynamic and keys definition is stored in another table. So I want to create a VIEW that has dynamic columns based on the foreign key.
Here are my table looks like:
companies:
| id | name |
| -- | ---- |
| 1 | Company 1 |
| 2 | Company 2 |
invoices:
| id | invoice_number | custom_fields | company_id |
| -- | -------------- | ------------- | ---------- |
| 1 | INV-01 | {"1": "Joe", "3": true, "5": "2020-12-12"} | 1 |
| 2 | INV-01 | {"2":"Hello", "4": 1000} | 2 |
customization_fields:
| id | label | data_type | company_id |
| -- | ----- | --------- | ---------- |
| 1 | manager | text | 1 |
| 2 | reference | text | 2 |
| 3 | emailed | boolean | 1 |
| 4 | account | integer | 2 |
| 5 | due_date | date | 1 |
So I want to create a view for getting each companies invoices something like:
CREATE OR REPLACE VIEW companies_invoices AS SELECT * FROM invoices WHERE company_id = 1
which should get a result like below:
| id | invoice_number | company_id | manager | emailed | due_date |
| -- | -------------- | ---------- | ------- | ------- | -------- |
| 1 | INV-01 | 1 | Joe | true | 2020-12-12 |
So my challenge above here is I cannot make sure the keys when I write the query. If I know that I could write
SELECT
id,
invoice_number,
company_id,
custom_fields:"1" AS manager,
custom_fields:"3" AS emailed,
custom_fields:"5" AS due_date
FROM invoices
WHERE company_id = 1
These keys and labels are written in the customization_fields table, so I tried different ways and I am not able to do that.
So could anyone tell me if we can do or not? If we can please give me an example so it would really help.
You cannot do what you want to do with a view. A view has a fixed set of columns and they have specific types. Retrieving a dynamic set of columns requires some other mechanism.
If you're trying to change the number of columns or the names of the columns based on the rows in the customization_fields table, you can't do it in a view.
If you have a defined schema and just need to grab dynamic JSON properties, you may want to consider looking into Snowflake's GET function. It allows you to get any part of a JSON using a string for the path rather than using a literal path in the SQL statement. For example:
create temp table foo(v variant);
insert into foo select parse_json('{ "name":"John", "age":30, "car":null }');
-- This uses a literal path in the SQL to get to a JSON property
select v:name::string as first_name from foo;
-- This uses the GET function to get the value from a path in a string
select get(v, 'name')::string as first_name from foo;
You can replace the 'name' in the second parameter of the GET function with the value stored in the customization_fields table.
In SF, You will have to use a Stored Proc function to retrieve the dynamic set of columns

Better way to grant access to data within a table based on user?

I'm trying design a system for an API that grants users access to a data table Data based on a permission table Permissions which is related to a group table Group. When a user makes a request for data (from the Data table), my API should only return rows from the Data table based on the values within the columns of the Data table that they have been granted to
view.
By default, a user will have no access to any rows when requesting data through my API. However, I'd like to grant access to Data based on values in columns.
For example If my Data table contains information about news articles and has columns title, news_source, posted_date, and other similar columns
id | title | news_source | posted_date | ...
-----+----------+-----------------------+-------------+------
1 | ... | NYTimes | 2019-12-30 |
2 | ... | BBC | 2019-12-30 |
3 | ... | BBC | 2019-12-30 |
4 | ... | Washington Post | 2019-12-30 |
5 | ... | NYTimes | 2019-12-30 |
6 | ... | NYTimes | 2020-01-01 |
7 | ... | Boston Globe | 2020-01-01 |
In this example, I'd like to grant a group access to get data only from NYTimes, posted after 2020-01-01, etc...
To do this, I've implemented the schema below
+-----+ +--------------+
|Group|<-------|Permission |
+-----+ +--------------+
|name | |group_id |
|... | |column_name |
+-----+ |text_value |
|date_value |
+--------------+
For Group, name is just the name of the group and the ellipse represents some other non-relevant columns. In Permissions, I have the foreign key to Group (group_id), the name of the column in the Data table that I'm accessing (column_name), and the value I'm granting access to (text_value or date_value depending on the column I'm referencing).
Right now, when a user makes a request for data, I run this SQL to apply the permissions (if the user's group has id = 1).
SELECT * FROM Data d
INNER JOIN Permission p1 ON p1.group_id = 1 AND p1.column_name = 'news_source' AND p1.text_value = d.news_source
INNER JOIN Permission p2 ON p2.group_id = 1 AND p2.column_name = 'posted_date' AND p2.date_value >= d.posted_date;
This will work, but I was wondering if there was a better more organized way to go about this. I feel there would be a lot of redundancy in this model across multiple groups with the same permissions.

Reducing a Postgres table to JSON

I have a following table in Postgres
+----+-----------+----------------------+---------+
| id | user_fk| language_fk | details |
+----+-----------+----------------------+---------+
| 1 | 2 | en-us | 123 |
| 2 | 3 | en-us | 456 |
| 3 | 4 | en-us | 789 |
| 4 | 4 | es-la | 012 |
+----+-----------+----------------------+---------+
And I want to reduce this to the following SQL statement:
UPDATE users SET details = '{"en-us": "789", "es-la": "012"}' WHERE id = 4;
UPDATE users SET details = '{"en-us": "123"}' WHERE id = 2;
UPDATE users SET details = '{"en-us": "456"}' WHERE id = 3;
So I want to reduce languages per user and put it in a different table. Is there a way to do this in Postgres?
Use the function jsonb_object_agg() to get the expected output:
select
min(id) as id,
user_fk,
jsonb_object_agg(language_fk, details) as details
from users
group by user_fk
id | user_fk | details
----+---------+----------------------------------
1 | 2 | {"en-us": "123"}
2 | 3 | {"en-us": "456"}
3 | 4 | {"en-us": "789", "es-la": "012"}
(3 rows)
You cannot update the table in this way because of different types of old and new details column. Create a new table with reduced columns using create table from select:
create table new_users as
select
min(id) as id,
user_fk,
jsonb_object_agg(language_fk, details) as details
from users
group by user_fk;