can we insert or update data in table from RLS policy in Postgres? - postgresql

I've created table "clientID_dbUser_mapping" which contains client id (e.g 1,2,3) and DB User name (u1,u2,u3)
Now created another table "test_data" which contains id(PK), data(text), client_id(FK)
created RLS policy for "test_data" to access the appropriate logged in db_user data
here is policy :
CREATE POLICY client_id_testdata_policy ON test_data
FOR ALL
USING (client_id =
(SELECT client_id
FROM client_dbuser_mapping
WHERE db_user_name = current_user)
)
WITH CHECK (client_id =
(SELECT client_id
FROM client_dbuser_mapping
WHERE db_user_name = current_user)
);
This is working fine for SELECT/UPDATE/DELETE
for INSERT I want to add default data of client_id in "test_data" from "clientID_dbUser_mapping" table coz it contains client id as per db_user_name
so how can I achieve this through RLS policy (can i change something in my existing policy) or need to create new policy ?
is it possible or not?

I am not sure if I understood the requirement correctly, but if you want to force the client_id to a certain value upon INSERT, use a trigger:
CREATE FUNCTION set_client_id() RETURNS trigger
LANGUAGE plpgsql AS
$$BEGIN
SELECT cdm.client_id INTO NEW.client_id
FROM client_dbuser_mapping AS cdm
WHERE cdm.db_user_name = current_user;
RETURN NEW;
END;$$;
CREATE TRIGGER set_client_id BEFORE INSERT ON test_data
FOR EACH ROW EXECUTE PROCEDURE set_client_id();

Related

Postgres RLS Policy and functions

I have a RLS policy violation on a Postgres function. I believe it's because the policy relies on rows created in the function. A SELECT command is run in the function. New rows are not available because they are still in a transaction.
Here is the function:
CREATE FUNCTION public.create_message(organization_id int, content text, tags Int[])
RETURNS setof public.message
AS $$
-- insert message, return PK
WITH moved_rows AS (
INSERT INTO public.message (organization_id, content)
VALUES($1, $2)
RETURNING *
),
-- many to many relation
moved_tags AS (
INSERT INTO public.message_tag (message_id, tag_id)
SELECT moved_rows.id, tagInput.tag_id
FROM moved_rows, UNNEST($3) as tagInput(tag_id)
RETURNING *
)
SELECT moved_rows.* FROM moved_rows LEFT JOIN moved_tags ON moved_rows.id = moved_tags.message_id
$$ LANGUAGE sql VOLATILE STRICT;
Here is the policy:
CREATE POLICY select_if_organization
on message_tag
for select
USING ( message_id IN (
SELECT message.id
FROM message
INNER JOIN organization_user ON (organization_user.organization_id = message.organization_id)
INNER JOIN sessions ON (sessions.user_id = organization_user.user_id)
WHERE sessions.session_token = current_user_id()));
Ideas:
Add a field to the joining table to simplify the policy, but it violates normal form.
Return user input instead of running the SELECT, but input may be escaped and I should be able to run a SELECT command
Split into two functions. Create the message row, then add the message_tag. I'm running postgraphile, so two mutations. I have foreign key relations setup between the two. I don't know if graphile will do that automatically.
Error message:
ERROR: new row violates row-level security policy for table "message_tag"
CONTEXT: SQL function "create_message" statement 1
I receive the error when I run the function. I want the function to run successfully, insert one row in the message table, and turning the input array into rows for the message_tag table with message_tag.message_id=message.id, the last inserted id. I need a policy in place so users from that join relation only see their own organization's message_tag rows.
Here is another policy on the INSERT command. It allows INSERT if a user is logged in:
create policy insert_message_tag_if_author
on message_tag
for insert
with check (EXISTS (SELECT * FROM sessions WHERE sessions.session_token = current_user_id()));
According to the error message, this part of your SQL statement causes the error:
INSERT INTO public.message_tag (message_id, tag_id)
SELECT moved_rows.id, tagInput.tag_id
FROM moved_rows, UNNEST($3) as tagInput(tag_id)
RETURNING *
You need to add another policy FOR INSERT with an appropriate WITH CHECK clause.
I ended up adding a field to the joining table, and creating a policy with that. That way, RLS validation does not require a row which would be created in a middle of a function.

How to make INSERT ... RETURNING statement work when using Row Level Security (RLS)?

Below you can find code for a minimal test case for an issue I am having in a system where RLS is used to manage access to a hierarchical datastructure. I am using Postgres v11.
In the code I have units, which is a top level object. units have subunits in a 1-n relation.
There are also users, where a user can have access to a number of units through the unit_owner table.
The RLS policies are designed to let a user insert new subunits into the units he owns.
All this works fine as can be seen up until the 2nd last line in the code.
BUT here is my issue: This database is exposed through a GraphQL middleware (Postgraphile) which needs the result of an insert back by means of the INSERT ... RETURNING feature.
And as can be seen in the last insert statement, this does not work, it gets "ERROR: new row violates row-level security policy".
The problem seems to arise from the fact that RETURNING needs the select rights, and the select policy function is evaluated using the set of subunit ids available before the insert, not after.
Any tips on how I can let my users insert subunits into their units would be appreciated!
CREATE SCHEMA insert_returning;
CREATE ROLE users;
GRANT USAGE ON SCHEMA insert_returning TO users;
DROP TABLE IF EXISTS insert_returning.unit;
DROP TABLE IF EXISTS insert_returning.subunit;
DROP TABLE IF EXISTS insert_returning.unit_owner;
CREATE TABLE insert_returning.unit (
id integer NOT NULL,
description varchar NULL,
CONSTRAINT unit_pk PRIMARY KEY (id)
);
CREATE TABLE insert_returning.subunit (
id integer NOT NULL,
unit_id integer NOT NULL,
description varchar NULL,
CONSTRAINT subunit_pk PRIMARY KEY (id)
);
CREATE TABLE insert_returning.unit_owner (
user_id integer NOT NULL,
unit_id integer NOT NULL
);
GRANT SELECT,INSERT,UPDATE ON TABLE insert_returning.unit TO users;
GRANT SELECT,INSERT,UPDATE ON TABLE insert_returning.subunit TO users;
GRANT SELECT ON TABLE insert_returning.unit_owner TO users;
CREATE OR REPLACE FUNCTION insert_returning.get_users_units()
RETURNS SETOF integer
LANGUAGE sql VOLATILE SECURITY DEFINER AS
$$
SELECT uo.unit_id FROM insert_returning.unit_owner uo
WHERE uo.user_id = 17;
$$;
CREATE OR REPLACE FUNCTION insert_returning.get_users_subunits()
RETURNS SETOF integer
LANGUAGE sql VOLATILE SECURITY DEFINER AS
$$
SELECT s.id FROM insert_returning.subunit s
JOIN insert_returning.unit_owner uo ON uo.unit_id = s.unit_id
WHERE uo.user_id = 17;
$$;
ALTER TABLE insert_returning.unit ENABLE ROW LEVEL SECURITY;
ALTER TABLE insert_returning.subunit ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS select_unit ON insert_returning.unit;
DROP POLICY IF EXISTS select_subunit ON insert_returning.subunit;
DROP POLICY IF EXISTS insert_subunit ON insert_returning.subunit;
CREATE POLICY select_unit ON insert_returning.unit FOR SELECT TO PUBLIC USING ((
SELECT (id IN ( SELECT unit_id FROM insert_returning.unit_owner WHERE user_id = 17))
));
CREATE POLICY select_subunit ON insert_returning.subunit FOR SELECT TO PUBLIC USING ((
SELECT (id IN (SELECT insert_returning.get_users_subunits()) )
));
CREATE POLICY insert_subunit ON insert_returning.subunit FOR INSERT TO PUBLIC WITH CHECK ((
SELECT (unit_id IN (SELECT insert_returning.get_users_units()) )
));
INSERT INTO insert_returning.unit (id, description) VALUES (1, 'I am visible');
INSERT INTO insert_returning.unit (id, description) VALUES (2, 'I am hidden');
INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (1, 1, 'I belong to a visible unit');
INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (2, 2, 'I belong to a hidden unit');
INSERT INTO insert_returning.subunit (id, unit_id, description) VALUES (3, 1, 'I too belong to a visible unit');
INSERT INTO insert_returning.unit_owner (user_id,unit_id) VALUES (17,1);
SET ROLE users;
SELECT * FROM insert_returning.subunit; -- works
INSERT INTO insert_returning.subunit VALUES (4, 1, 'I am a new subunit'); -- works
INSERT INTO insert_returning.subunit VALUES (5, 1, 'I am another new subunit') RETURNING *; -- FAILS
--
You analyzed the problem right: the inserted row is not available to the subquery in the FOR SELECT policy on subunit.
There is no way to "make it work" like this. You will have to find a different test for the policy, one that does not expect to find the new row in the table. The way your case is written, you could directly use the unit_id of the new row for a simpler test, but you assure us that this wouldn't work in your real use case...
You cannot select the new row, but you can use all attributes of the new row. So try to write the condition using an SQL expression that does not involve a subselect on the table itself.
In order to make it work (and you do not see a way to change the underlying RLS), you can create a custom mutation function which you can mark as SECURITY DEFINER.
In this mutation function you will have to make the checking yourself.
This does not answer your question regarding the RLS - which I think has already been properly accessed in another answer in this question. It is more likely a tip from a fellow Postgraphile user.
Also:
In my experience using functions in RLS is almost always a performance penalty. Especially when they are not inlined. In your case VOLATILE and SECURITY DEFINER should already prevent inlining.
It is almost always faster to use EXISTS instead of IN in RLS definitions. Your experience may differ.

Can we create a trigger (or any object) to catch each login name

Actually I need to find all the users logged into a SQL Server in last month, so that I can delete the users which are not using SQL Server from AD group.
I was not able to find that so now I am trying to create trigger so that after one month I can delete users.
Under the server properties select the Security tab and enable Login auditing for "Both failed and successful logins". The information is then recorded in the SQL Server Logs.
It is also possible to do it via trigger. Something like
CREATE TRIGGER Logon_Audit
ON ALL SERVER
FOR LOGON
AS
BEGIN
declare #acct as nvarchar(max)
set #acct = ORIGINAL_LOGIN()
-- insert #acct into some table
END
I used fully qualified name for table and deleted multiple records
Create Table LoginDetails (iD int identity(1,1),LoginName nvarchar(max) , LoginTime datetime)
Create TRIGGER Logon_Audit
ON ALL SERVER
FOR LOGON
AS
BEGIN
declare #acct as nvarchar(max)
declare #LoginTime as nvarchar(max)
set #acct = ORIGINAL_LOGIN()
set #LoginTime = getdate()
insert into master..LoginDetails
Select top 1 #acct,#LoginTime
delete
from master..LoginDetails
where id not in (
Select max(id)
from LoginDetails
group by LoginName,LoginTime
)
END

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.

How to find table creation time?

How can I find the table creation time in PostgreSQL?
Example: If I created a file I can find the file creation time like that I want to know the table creation time.
I had a look through the pg_* tables, and I couldn't find any creation times in there. It's possible to locate the table files, but then on Linux you can't get file creation time. So I think the answer is that you can only find this information on Windows, using the following steps:
get the database id with select datname, datdba from pg_database;
get the table filenode id with select relname, relfilenode from pg_class;
find the table file and look up its creation time; I think the location should be something like <PostgreSQL folder>/main/base/<database id>/<table filenode id> (not sure what it is on Windows).
You can't - the information isn't recorded anywhere. Looking at the table files won't necessarily give you the right information - there are table operations that will create a new file for you, in which case the date would reset.
I don't think it's possible from within PostgreSQL, but you'll probably find it in the underlying table file's creation time.
Suggested here :
SELECT oid FROM pg_database WHERE datname = 'mydb';
Then (assuming the oid is 12345) :
ls -l $PGDATA/base/12345/PG_VERSION
This workaround assumes that PG_VERSION is the least likely to be modified after the creation.
NB : If PGDATA is not defined, check Where does PostgreSQL store the database?
Check data dir location
SHOW data_directory;
Check For Postgres relation file path :
SELECT pg_relation_filepath('table_name');
you will get the file path of your relation
check for creation time of this file <data-dir>/<relation-file-path>
I tried a different approach to get table creation date which could help for keeping track of dynamically created tables. Suppose you have a table inventory in your database where you manage to save the creation date of the tables.
CREATE TABLE inventory (id SERIAL, tablename CHARACTER VARYING (128), created_at DATE);
Then, when a table you want to keep track of is created it's added in your inventory.
CREATE TABLE temp_table_1 (id SERIAL); -- A dynamic table is created
INSERT INTO inventory VALUES (1, 'temp_table_1', '2020-10-07 10:00:00'); -- We add it into the inventory
Then you could get advantage of pg_tables to run something like this to get existing table creation dates:
SELECT pg_tables.tablename, inventory.created_at
FROM pg_tables
INNER JOIN inventory
ON pg_tables.tablename = inventory.tablename
/*
tablename | created_at
--------------+------------
temp_table_1 | 2020-10-07
*/
For my use-case it is ok because I work with a set of dynamic tables that I need to keep track of.
P.S: Replace inventory in the database with your table name.
I'm trying to follow a different way for obtain this.
Starting from this discussion my solution was:
DROP TABLE IF EXISTS t_create_history CASCADE;
CREATE TABLE t_create_history (
gid serial primary key,
object_type varchar(20),
schema_name varchar(50),
object_identity varchar(200),
creation_date timestamp without time zone
);
--delete event trigger before dropping function
DROP EVENT TRIGGER IF EXISTS t_create_history_trigger;
--create history function
DROP FUNCTION IF EXISTS public.t_create_history_func();
CREATE OR REPLACE FUNCTION t_create_history_func()
RETURNS event_trigger
LANGUAGE plpgsql
AS $$
DECLARE
obj record;
BEGIN
FOR obj IN SELECT * FROM pg_event_trigger_ddl_commands () WHERE command_tag in ('SELECT INTO','CREATE TABLE','CREATE TABLE AS')
LOOP
INSERT INTO public.t_create_history (object_type, schema_name, object_identity, creation_date) SELECT obj.object_type, obj.schema_name, obj.object_identity, now();
END LOOP;
END;
$$;
--ALTER EVENT TRIGGER t_create_history_trigger DISABLE;
--DROP EVENT TRIGGER t_create_history_trigger;
CREATE EVENT TRIGGER t_create_history_trigger ON ddl_command_end
WHEN TAG IN ('SELECT INTO','CREATE TABLE','CREATE TABLE AS')
EXECUTE PROCEDURE t_create_history_func();
In this way you obtain a table that records all the creation tables.
--query
select pslo.stasubtype, pc.relname, pslo.statime
from pg_stat_last_operation pslo
join pg_class pc on(pc.relfilenode = pslo.objid)
and pslo.staactionname = 'CREATE'
Order By pslo.statime desc
will help to accomplish desired results
(tried it on greenplum)
You can get this from pg_stat_last_operation. Here is how to do it:
select * from pg_stat_last_operation where objid = 'table_name'::regclass order by statime;
This table stores following operations:
select distinct staactionname from pg_stat_last_operation;
staactionname
---------------
ALTER
ANALYZE
CREATE
PARTITION
PRIVILEGE
VACUUM
(6 rows)