Supabase: PostgreSQL policy using a column value gives error - postgresql

I am trying to set a Row Level Security policy in PostgreSQL via Supabase.
The table has two relevant fields: access (either "P" for public or "R" for restricted) and include (an array of email ids). I wish to set a policy which allows all authenticated users to access the records which are either "P" or if "R" their email must be present.
CREATE POLICY "policy_name"
ON public.table
FOR SELECT
TO authenticated
USING (
access = "P" OR
auth.email() = ANY(include)
);
This gives an error saying Column "P" does not exist
I am new to SQL expressions and will appreciate any help. Thanks

The error was because I used double quotes!!!!
The policy that works in supabase is:
(((jwt() ->> 'email'::text) = ANY (include)) OR (access = 'P'::text) OR (uid() = user_id))
Changed the auth.email() to jwt() ->> 'email' since the former is being deprecated.
I added the last uid() = user_id (a column in the table) without which it would not permit the user to add a record

Related

Trying to Right an AND-ed SQL Expression

I'm trying to create a SQL expression for an INSERT policy for a resource_authors table (a JOIN table linking resources and users):
CREATE POLICY insert_resources_authors ON public.resource_authors
FOR INSERT TO public_user
WITH CHECK (*some expression*)
What I'd like to say is "the user must be logged in and there must not be an existing author record for that resource yet". I can express the first part using a custom current_user_id function I have:
current_user_id() IS NOT NULL
And I can get the second with:
SELECT count(user_id) > 0
FROM resource_authors
WHERE resource_id = resource_id
... but I can't figure out how to combine them. Can anyone clue me in to how I can select both, as a single boolean?
Literally place an AND between them (and wrap them in parenthesis to make clear they're subqueries):
(current_user_id() IS NOT NULL)
AND
NOT (SELECT count(user_id) > 0
FROM resource_authors
WHERE resource_id = resource_id)
I'd write
CREATE POLICY insert_resources_authors ON public.resource_authors
FOR INSERT TO public_user
WITH CHECK (
current_user_id() IS NOT NULL
AND
NOT EXISTS (SELECT *
FROM resource_authors ra
WHERE ra.resource_id = resource_authors.resource_id
)
);
However I find it questionable to allow anyone to make themselves author of any authorless resource. I'd rather record the author via a trigger when they create the resource.

PostgreSQL 9.5 - Row level security / ROLE best practices

I'm tying to grasp the best way to use the new row level security feature in a multi-tenant database that supports a web application.
Currently, the application has a few different ROLEs available, depending on the action it is attempting to take.
Once the application makes a connection using its own ROLE, the application passes authentication parameters (provided by the user) into different functions that filter out rows based on the user supplied authentication parameters. The system is designed to work with thousands of users and it seems to work; however, it's defiantly clunky (and slow).
It seems that if I wanted to use the new row level security feature I would need to create a new ROLE for each real world user (not just for the web application) to access the database.
Is this correct? and if so, is it a good idea to create thousands of ROLEs in the database?
Update from a_horse_with_no_name's link in the comments (thanks, that thread is spot on):
CREATE USER application;
CREATE TABLE t1 (id int primary key, f1 text, app_user text);
INSERT INTO t1 VALUES(1,'a','bob');
INSERT INTO t1 VALUES(2,'b','alice');
ALTER TABLE t1 ENABLE ROW LEVEL SECURITY;
CREATE POLICY P ON t1 USING (app_user = current_setting('app_name.app_user'));
GRANT SELECT ON t1 TO application;
SET SESSION AUTHORIZATION application;
SET app_name.app_user = 'bob';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
1 | a | bob
(1 row)
SET app_name.app_user = 'alice';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
2 | b | alice
(1 row)
SET app_name.app_user = 'none';
SELECT * FROM t1;
id | f1 | app_user
----+----+----------
(0 rows)
Now, I'm confused by current_setting('app_name.app_user') as I was under the impression this was only for configuration parameters... where is app_name defined?
Setting security policies based on a session setting is a BAD BAD BAD idea (I hate both CAPS and bold so trust me that I mean it). Any user can SET SESSION 'app_name.app_user' = 'bob', so as soon as someone figures out that "app_name.app_user" is the door in (trust me, they will) then your whole security is out the door.
The only way that I see is to use a table accessible to your webadmin only which stores session tokens (uuid type comes to mind, cast to text for ease of use). The login() function is SECURITY DEFINER (assuming owner webadmin), setting the token as well as a session SETting, and then the table being owned by (or having appropriate privileges for) webadmin refers to that table and the session setting in its policy.
Unfortunately, you cannot use temporary (session) tables here because you cannot build policies on a temporary table so you have to use a "real" table. That is something of a performance penalty, but weigh that against the damage of a hack...
In practice:
CREATE FUNCTION login (uname text, pwd text) RETURNS boolean AS $$
DECLARE
t uuid;
BEGIN
PERFORM * FROM users WHERE user = uname AND password = pwd;
IF FOUND THEN
INSERT INTO sessions SET token = uuid_generate_v4()::text, user ....
RETURNING token INTO t;
SET SESSION "app_name.token" = t;
RETURN true;
ELSE
SET SESSION "app_name.token" = '';
RETURN false;
END IF;
END; $$ LANGUAGE plpgsql STRICT;
And now your policy would link to sessions:
CREATE POLICY p ON t1 FOR SELECT
USING (SELECT true FROM sessions WHERE token = current_setting('app_name.token'));
(Since uuids may be assumed to be unique, no need for LIMIT 1. ordering or other magic, if the uuid is in the table the policy will pass, otherwise fail.) The uuid is impossible to guess (within your lifetime anyway) and impossible to retrieve by anyone but webadmin.

Case-Sensitive Linked-Server merge script - Set IDENTITY_INSERT <table> ON not working

I am doing a experimental script to do a SQL Comparison (COLLATED as case-sensitive) and I am having issues with the SET IDENTITY_INSERT <Table> ON
I have switched on this option and disabled foreign key checks, but it still seems to be complaining about the latter.
Here are the steps I followed:
1 - I created a linked server
EXEC sp_addlinkedserver #Server=N'xxx.xxx.xxx.xxx', #srvproduct=N'SQL Server'
2 - I added the login credentials
EXEC master.dbo.sp_addlinkedsrvlogin
#rmtsrvname = N'xxx.xxx.xxxx.xxx',
#locallogin = NULL ,
#useself = N'False',
#rmtuser = N'xxxxxxxxxxx',
#rmtpassword = N'xxxxxxxxxxx'
3 - In the same batch, I set the identity_insert, disabled foreign key checks and ran the following merge script. Note, the deferred query returns an XML field which is disallowed over distributed servers, so I casted to NVARCHAR(MAX)
SET IDENTITY_INSERT [DATABASE1].[dbo].[TABLE1] ON
ALTER TABLE [DATABASE1].[dbo].[TABLE1] NOCHECK CONSTRAINT ALL
MERGE [DATABASE1].[dbo].[TABLE1]
USING OPENQUERY([xxx.xxx.xxx.xxx], 'SELECT S.ID, S.EventId, S.SnapshotTypeID, CAST(S.Content AS NVARCHAR(MAX)) AS Content FROM [DATABASE1].[dbo].[TABLE1] AS S') AS S
ON (CAST([DATABASE1].[dbo].[TABLE1].Content AS NVARCHAR(MAX)) = S.Content)
WHEN NOT MATCHED BY TARGET
THEN INSERT VALUES (S.ID, S.EventId, S.SnapshotTypeID, CAST(S.Content AS XML))
WHEN MATCHED
THEN UPDATE SET [DATABASE1].[dbo].[TABLE1].EventId = S.EventId,
[DATABASE1].[dbo].[TABLE1].SnapshotTypeID = S.SnapshotTypeID,
[DATABASE1].[dbo].[TABLE1].Content = S.Content
COLLATE Latin1_General_CS_AS;
GO
The error message I am getting reads as follows:
Msg 8101, Level 16, State 1, Line 4
An explicit value for the identity column in table 'Database1.dbo.Table' can only be specified when a column list is used and IDENTITY_INSERT is ON.
How can I fix this? As I mentioned, this script is only an experiment for one of the systems I am writing. I am probably reinventing the wheel somewhere, but its all about learning in this exercise.
An explicit value for the identity column in table 'Database1.dbo.Table' can only be specified when a column list is used and IDENTITY_INSERT is ON.
You have no column list

Update table with multiple fields from another table

I am trying to update a field in a table in Postgres called certificate_name by concatenating 3 fields from another table (first_name, middle_name, and last_name). I have tried several statements, but they all throw errors; my most recent attempt was the following:
update candidate_attributes ca
inner join "user" u on u.id=ca.candidate_user_id
set ca.certificate_name = concat(u.first_name, u.middle_name, u.last_name);
I'm getting an error that says:
syntax error at or near "inner"....
What am I doing wrong?
Your syntax isn't valid in Postgres. Resembles SQL Server syntax.
Read the manual on UPDATE and use instead:
UPDATE candidate_attributes ca
SET certificate_name = concat_ws(' ', u.first_name,u.middle_name,u.last_name)
FROM "user" u
WHERE u.id = ca.candidate_user_id;
I also threw in concat_ws() instead of concat(), assuming you want a space between each part of the name.

Postgres recursive query for SSH authorized key paths

How do I structure a recursive query to help me find all server-to-server jumps that are accessible via SSH public key authorization?
For a given SSH pubkey fingerprint, first find the list of servers/accounts that the key is authorized for. For each server found, iterate over the private keys on that server, and find the list of servers/accounts that each of those are authorized for.
Repeat this process until there's a dead end, i.e. the authorized key path ends. Return a full authorized key path (as an array) per row.
Simplified example:
CREATE TABLE sshkeys (
server text, -- server UUID
username text, -- authorized username
privkey_owner text, -- owner of private key
fingerprint text, -- SSH key fingerprint
keytype text, -- "public" or "private"
)
Server Username Owner Fingerprint Keytype
banana root key_id_james public
banana james key_id_james public
banana root key_id_root#banana private
apple root key_id_root#banana public
apple fred fred key_id_fred private
mango fred key_id_fred public
James, using his key with ID key_id_james, has access to root#banana. From there, the root#banana private key is authorized for root#apple. On apple, root can access key_id_fred, and is thus authorized for fred#mango.
So ultimately, James has access to Fred's account on mango, via root access to banana and apple. He also has access to james#banana. The two output rows would consist of arrays of {source, keyid, target} elements, looking something like:
{{NULL, key_id_james, root#banana},
{banana, key_id_root#banana, root#apple},
{apple, key_id_fred, fred#mango}},
{{NULL, key_id_james, james#banana}}
I plan to add restrictions and heuristics that should be taken into account, including whether private keys are encrypted and whether a given user has group or chmod access to another user's private key (by joining to other tables in the database). That should be easy to tack onto a working base query, however.
As for the query, I'm unable to figure out how to do the secondary step, that is: For each server initially found to be accessible via the given key, iterate over all private keys on each server and recurse. I'm not getting further than this non-working query:
WITH RECURSIVE initial_authkeys AS (
SELECT
server, username, privkey_owner, fingerprint, keytype
FROM
sshkeys
WHERE
fingerprint = 'key_id_james' AND
keytype = 'public'
UNION ALL
SELECT
ak.server, ak.username, ak.privkey_owner, ak.fingerprint, ak.keytype
FROM
sshkeys,
initial_authkeys ak
WHERE
sshkeys.fingerprint = ak.fingerprint AND
sshkeys.keytype = 'private'
)
SELECT * FROM initial_authkeys;
Any suggestions?
What's needed to make this work is a subquery expression to find the SSH fingerprints of any available private key on the server where the given pubkey is authorized. Below is a working query that includes a rudimentary heuristic to only find private keys if the user has root access to the server. It could be expanded to verify private key file permissions, group ownership, etc.
WITH RECURSIVE authkeys(server, username, fingerprint, level, auth_path) AS (
SELECT
server, username, fingerprint, 1 AS level,
ARRAY['Authorized to '||username||
'#'||server||' using keyid='||fingerprint]
FROM
sshkeys
WHERE
fingerprint = 'key_id_james' AND keytype = 'public'
UNION
SELECT
k.server, k.username, k.fingerprint, ak.level+1 AS level,
auth_path || ARRAY['Authorized to '||k.username||
'#'||k.server||' using keyid='||k.fingerprint]
FROM
sshkeys k, authkeys ak
WHERE
k.keytype = 'public' AND k.fingerprint IN (
SELECT
fingerprint
FROM
sshkeys k2
WHERE
k2.server = ak.server AND k2.keytype = 'private' AND
ak.username = 'root'
)
) SELECT * FROM authkeys;