Postgres RLS inconsistency on one table - postgresql

I'm using RLS in a multi-tenancy model. No problems for the first several tables I enabled. Then I add RLS to a new table and suddenly I cannot insert a record to that table.
Here is a table that works:
CREATE TABLE wtr.adjustment (
id uuid UNIQUE NOT NULL DEFAULT uuid_generate_v1(),
created_at timestamp NOT NULL DEFAULT current_timestamp,
updated_at timestamp NOT NULL DEFAULT current_timestamp,
vendor_id uuid NOT NULL,
reporting_period_id uuid NOT NULL,
inventory_lot_id uuid NOT NULL,
adjustment_date date NOT NULL,
quantity_delta NUMERIC(50,2) NOT NULL,
adjustment_type wtr.adjustment_type NOT NULL,
comments text,
CONSTRAINT pk_adjustment PRIMARY KEY (id)
);
--||--
GRANT select, insert, update, delete ON TABLE wtr.adjustment TO wtr_user;
--||--
ALTER TABLE wtr.adjustment ADD CONSTRAINT fk_adjustment_vendor FOREIGN KEY ( vendor_id ) REFERENCES wtr.vendor( id );
--||--
ALTER TABLE wtr.adjustment ADD CONSTRAINT fk_adjustment_reporting_period FOREIGN KEY ( reporting_period_id ) REFERENCES wtr.reporting_period( id );
--||--
ALTER TABLE wtr.adjustment ADD CONSTRAINT fk_adjustment_inventory_lot FOREIGN KEY ( inventory_lot_id ) REFERENCES wtr.inventory_lot( id );
--||--
ALTER TABLE wtr.adjustment ENABLE ROW LEVEL SECURITY;
--||--
CREATE POLICY select_adjustment ON wtr.adjustment FOR SELECT
USING (vendor_id = wtr.current_vendor_id());
--||--
CREATE FUNCTION wtr.fn_timestamp_update_adjustment() RETURNS trigger AS $$
BEGIN
NEW.updated_at = current_timestamp;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
--||--
CREATE TRIGGER tg_timestamp_update_adjustment
BEFORE UPDATE ON wtr.adjustment
FOR EACH ROW
EXECUTE PROCEDURE wtr.fn_timestamp_update_adjustment();
--||--
and the associated function that works:
CREATE OR REPLACE FUNCTION wtr.build_adjustment(
_reporting_period_id uuid,
_inventory_lot_gov_id text,
_strain_name text,
_room_name text,
_inventory_type_name text,
_adjustment_date text,
_quantity_delta NUMERIC(50,2),
_adjustment_type wtr.adjustment_type,
_comments text
)
RETURNS wtr.adjustment as $$
DECLARE
_vendor_id uuid;
_inventory_lot wtr.inventory_lot;
_adjustment wtr.adjustment;
_inventory_type_id uuid;
BEGIN
_vendor_id := wtr.current_vendor_id();
_inventory_lot := wtr.find_or_build_existing_inventory_lot(
_strain_name,
_room_name,
_inventory_lot_gov_id,
_inventory_type_name
);
INSERT INTO wtr.adjustment(
vendor_id,
reporting_period_id,
inventory_lot_id,
adjustment_date,
quantity_delta,
adjustment_type,
comments
)
SELECT
_vendor_id,
_reporting_period_id,
_inventory_lot.id,
_adjustment_date::DATE,
_quantity_delta,
_adjustment_type,
_comments
RETURNING *
INTO _adjustment;
RETURN _adjustment;
END;
$$ language plpgsql strict security definer;
--||--
GRANT execute ON FUNCTION wtr.build_adjustment(
uuid,
text,
text,
text,
text,
text,
numeric,
wtr.adjustment_type,
text
) TO wtr_user;
here is the table that fails:
CREATE TABLE wtr.received_inventory_transfer (
id uuid UNIQUE NOT NULL DEFAULT uuid_generate_v1(),
created_at timestamp NOT NULL DEFAULT current_timestamp,
updated_at timestamp NOT NULL DEFAULT current_timestamp,
vendor_id uuid NOT NULL,
reporting_period_id uuid NOT NULL,
inventory_lot_id uuid NOT NULL,
transfer_date DATE NOT NULL,
quantity_received NUMERIC(50,2) NOT NULL,
CONSTRAINT pk_received_inventory_transfer PRIMARY KEY (id)
);
--||--
GRANT select, insert, update, delete ON TABLE wtr.received_inventory_transfer TO wtr_user;
--||--
ALTER TABLE wtr.received_inventory_transfer ADD CONSTRAINT fk_received_inventory_transfer_vendor FOREIGN KEY ( vendor_id ) REFERENCES wtr.vendor( id );
--||--
ALTER TABLE wtr.received_inventory_transfer ADD CONSTRAINT fk_received_inventory_transfer_reporting_period FOREIGN KEY ( reporting_period_id ) REFERENCES wtr.reporting_period( id );
--||--
ALTER TABLE wtr.received_inventory_transfer ADD CONSTRAINT fk_received_inventory_transfer_inventory_lot FOREIGN KEY ( inventory_lot_id ) REFERENCES wtr.inventory_lot( id );
--||--
ALTER TABLE wtr.received_inventory_transfer ENABLE ROW LEVEL SECURITY;
--||--
CREATE POLICY select_received_inventory_transfer ON wtr.received_inventory_transfer FOR SELECT USING (vendor_id = wtr.current_vendor_id());
--||--
CREATE FUNCTION wtr.fn_timestamp_update_received_inventory_transfer() RETURNS trigger AS $$
BEGIN
NEW.updated_at = current_timestamp;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
--||--
CREATE TRIGGER tg_timestamp_update_received_inventory_transfer
BEFORE UPDATE ON wtr.received_inventory_transfer
FOR EACH ROW
EXECUTE PROCEDURE wtr.fn_timestamp_update_received_inventory_transfer();
--||--
and the associated failing function:
CREATE OR REPLACE FUNCTION wtr.build_received_inventory_transfers(
_reporting_period_id uuid,
_transfer_date text
-- _received_inventory_transfers jsonb
)
RETURNS wtr.received_inventory_transfer as $$
DECLARE
_vendor_id uuid;
_inventory_lot wtr.inventory_lot;
_received_inventory_transfer_info jsonb;
_received_inventory_transfer wtr.received_inventory_transfer;
_quantity numeric(50,2);
BEGIN
_vendor_id := wtr.current_vendor_id();
-- this call is currently hard coded for debug purposes
_inventory_lot := wtr.find_or_build_existing_inventory_lot(
'tacos',
'N/A',
'1234123412341234',
'Hash'
);
-- again, this is hard-coded for debug purposes
RAISE EXCEPTION 'v: %, rp: %, il: %, td: %, q: %',
_vendor_id,
_reporting_period_id,
_inventory_lot.id,
_transfer_date::DATE,
20
;
-- this is the call that fails
INSERT INTO wtr.received_inventory_transfer(
vendor_id,
reporting_period_id,
inventory_lot_id,
transfer_date,
quantity_received
)
SELECT
_vendor_id,
_reporting_period_id,
_inventory_lot.id,
_transfer_date::DATE,
20
RETURNING *
INTO _received_inventory_transfer;
RETURN _received_inventory_transfer;
END;
$$ language plpgsql;
--||--
GRANT execute ON FUNCTION wtr.build_received_inventory_transfers(
uuid,
text
-- jsonb
) TO wtr_user;
the current_vendor_id function uses a jwt token claim that is passed in by the server, which is postgraphile.
At the insert statement, the call fails with:
new row violates row-level security policy for table
\"received_inventory_transfer\"",
This info is all that the logs will show me.
I think my real question is - how can I further debug RLS policy? If I only enable RLS, but create no select policy, I get the same failure.

of course, the next thing i figured out:
CREATE POLICY insert_received_inventory_transfer
ON wtr.received_inventory_transfer
FOR INSERT
WITH CHECK (vendor_id = wtr.current_vendor_id());

Related

Postgres row level security insert returning

Given the following snippet from my schema:
create table users (
id serial primary key,
name text not null
);
create table user_groups (
id serial primary key,
name text not null
);
create table user_user_group (
user_id integer not null references users(id),
user_group_id integer not null references user_groups(id)
);
grant all on users to staff;
grant all on user_groups to staff;
grant all on user_user_group to staff;
create function can_access_user_group(id integer) returns boolean as $$
select exists(
select 1
from user_user_group
where user_group_id = id
and user_id = current_user_id()
);
$$ language sql stable security invoker;
create function can_access_user(id integer) returns boolean as $$
select exists(
select 1
from user_user_group
where user_id = id
and can_access_user_group(user_group_id)
);
$$ language sql stable security invoker;
alter table users enable row level security;
create policy staff_users_policy
on users
to staff
using (
can_access_user(id)
);
Please assume the staff role, and current_user_id() function are tested and working correctly. I'm hoping to allow the "staff" role to create users in user groups they can access via the user_user_group table. The following statement fails the staff_users_policy:
begin;
set local role staff;
with new_user as (
insert into users (
name
) values (
'Some name'
)
returning id
)
insert into user_user_group (
user_id,
user_group_id
)
select
new_user.id,
1 as user_group_id
from new_user;
commit;
I can add a staff_insert_users_policy like this:
create policy staff_insert_users_policy
on users
for insert
to staff
with check (
true
);
Which allows me to insert the user but fails on returning id, and I need the new user id in order to add the row to the user_user_group table.
I understand why it fails, but conceptually how can I avoid this problem? I could create a "definer" function, or a new role with it's own policy just for this but I'm hoping there's a more straightforward approach.
I just came around this problem too and solved it by generating the uuid before inserting it:
create or replace function insert_review_with_reviewer(
v_review_input public.review,
v_reviewer_input public.reviewer
)
returns void
language plpgsql
security invoker
as
$$
declare
v_review_id uuid := gen_random_uuid();
begin
insert into public.review
(id,
organisation_id,
review_channel_id,
external_id,
star_rating,
comment)
values (v_review_id,
v_review_input.organisation_id,
v_review_input.review_channel_id,
v_review_input.external_id,
v_review_input.star_rating,
v_review_input.comment);
insert into public.reviewer
(organisation_id, profile_photo_url, display_name, is_anonymous, review_id)
values (v_reviewer_input.organisation_id, v_reviewer_input.profile_photo_url, v_reviewer_input.display_name,
v_reviewer_input.is_anonymous, v_review_id);
end if;
end
$$;

I'm having an issue with this code when I try to input values into the transactions table

So I'm setting up a schema in which I can input transactions of a journal entry independent of each other but also that rely on each other (mainly to ensure that debits = credits). I set up the tables, function, and trigger. Then, when I try to input values into the transactions table, I get the error below. I'm doing all of this in pgAdmin4.
CREATE TABLE transactions (
transactions_id UUID PRIMARY KEY DEFAULT uuid_generate_v1(),
entry_id INTEGER NOT NULL,
post_date DATE NOT NULL,
account_id INTEGER NOT NULL,
contact_id INTEGER NULL,
description TEXT NOT NULL,
reference_id UUID NULL,
document_id UUID NULL,
amount NUMERIC(12,2) NOT NULL
);
CREATE TABLE entries (
id UUID PRIMARY KEY,
test_date DATE NOT NULL,
balance NUMERIC(12,2)
CHECK (balance = 0.00)
);
CREATE OR REPLACE FUNCTION transactions_biut()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
EXECUTE 'INSERT INTO entries (id,test_date,balance)
SELECT
entry_id,
post_date,
SUM(amount) AS ''balance''
FROM
transactions
GROUP BY
entry_id;';
END;
$$;
CREATE TRIGGER transactions_biut
BEFORE INSERT OR UPDATE ON transactions
FOR EACH ROW EXECUTE PROCEDURE transactions_biut();
INSERT INTO transactions (
entry_id,
post_date,
account_id,
description,
amount
)
VALUES
(
'1',
'2019-10-01',
'101',
'MISC DEBIT: PAID FOR FACEBOOK ADS',
-200.00
),
(
'1',
'2019-10-01',
'505',
'MISC DEBIT: PAID FOR FACEBOOK ADS',
200.00
);
After I execute this input, I get the following error:
ERROR: column "id" of relation "entries" does not exist
LINE 1: INSERT INTO entries (id,test_date,balance)
^
QUERY: INSERT INTO entries (id,test_date,balance)
SELECT
entry_id,
post_date,
SUM(amount) AS "balance"
FROM
transactions
GROUP BY
entry_id;
CONTEXT: PL/pgSQL function transactions_biut() line 2 at EXECUTE
SQL state: 42703
There are a few problems here:
You're not returning anything from the trigger function => should probably be return NEW or return OLD since you're not modifying anything
Since you're executing the trigger before each row, it's bound to fail for any transaction that isn't 0 => maybe you want a deferred constraint trigger?
You're not grouping by post_date, so your select should fail
You've defined entry_id as INTEGER, but entries.id is of type UUID
Also note that this isn't really going to scale (you're summing up all transactions of all days, so this will get slower and slower...)
#chirs I was able to figure out how to create a functioning solution using statement-level triggers:
CREATE TABLE transactions (
transactions_id UUID PRIMARY KEY DEFAULT uuid_generate_v1(),
entry_id INTEGER NOT NULL,
post_date DATE NOT NULL,
account_id INTEGER NOT NULL,
contact_id INTEGER NULL,
description TEXT NOT NULL,
reference_id UUID NULL,
document_id UUID NULL,
amount NUMERIC(12,2) NOT NULL
);
CREATE TABLE entries (
entry_id INTEGER PRIMARY KEY,
post_date DATE NOT NULL,
balance NUMERIC(12,2),
CHECK (balance = 0.00)
);
CREATE OR REPLACE FUNCTION transactions_entries() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'DELETE') THEN
INSERT INTO entries
SELECT o.entry_id, o.post_date, SUM(o.amount) FROM old_table o GROUP BY o.entry_id, o.post_date;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO entries
SELECT o.entry_id, n.post_date, SUM(n.amount) FROM new_table n, old_table o GROUP BY o.entry_id, n.post_date;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO entries
SELECT n.entry_id,n.post_date, SUM(n.amount) FROM new_table n GROUP BY n.entry_id, n.post_date;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER transactions_ins
AFTER INSERT ON transactions
REFERENCING NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
CREATE TRIGGER transactions_upd
AFTER UPDATE ON transactions
REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
CREATE TRIGGER transactions_del
AFTER DELETE ON transactions
REFERENCING OLD TABLE AS old_table
FOR EACH STATEMENT EXECUTE PROCEDURE transactions_entries();
Any thoughts on optimization?

PostgreSQL script references hangs

I"new to PostgreSQL and am struggling with a DDL issue. Below script "hangs" unless I remove column "keel" from table and unique constraint definitions. What may be the error ? Your input highly appreciated.
CREATE TABLE k_keel
(
id integer NOT NULL,
nim character varying(30) NOT NULL,
kood character(2) NOT NULL, -- Kahetäheline ISO 639 keelekood
CONSTRAINT k_keel_pkey PRIMARY KEY (id),
CONSTRAINT k_keel_nim_uniq UNIQUE (nim)
)
WITH (
OIDS=FALSE
);
ALTER TABLE k_keel
OWNER TO postgres;
GRANT ALL ON TABLE k_keel TO postgres;
GRANT SELECT ON TABLE k_keel TO overload;
COMMENT ON COLUMN k_keel.kood IS 'Kahetäheline ISO 639 keelekood';
-- Table: ac.e_kiri
-- DROP TABLE ac.e_kiri CASCADE;
CREATE TABLE ac.e_kiri
(
id INTEGER PRIMARY KEY DEFAULT nextval('ac.email_id_seq'::regclass),
nimi character varying(100) NOT NULL UNIQUE,
memo TEXT
)
WITH (
OIDS=FALSE
);
ALTER TABLE ONLY ac.e_kiri
ADD CONSTRAINT "Nimi peal olema unikaalne" UNIQUE (nimi);
ALTER TABLE ac.e_kiri
OWNER TO postgres;
GRANT ALL ON TABLE ac.e_kiri TO postgres;
GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE ac.e_kiri TO overload;
COMMENT ON TABLE ac.e_kiri IS 'Tüüpkirjade tabel';
-- DROP TABLE ac.e_kiri_tekst CASCADE;
CREATE TABLE ac.e_kiri_tekst
(
id INTEGER PRIMARY KEY DEFAULT nextval('ac.email_id_seq'::regclass),
e_kiri INTEGER REFERENCES ac.e_kiri (id) NOT NULL,
keel INTEGER REFERENCES public.k_keel (id) NOT NULL,
subjekt text,
sisu text
)
WITH (
OIDS=FALSE
);
ALTER TABLE ONLY ac.e_kiri_tekst
ADD CONSTRAINT "Nimi ja keel peavad olema unikaalsed" UNIQUE (e_kiri,keel);
ALTER TABLE ac.e_kiri_tekst
OWNER TO postgres;
GRANT ALL ON TABLE ac.e_kiri_tekst TO postgres;
GRANT SELECT, UPDATE, INSERT, DELETE ON TABLE ac.e_kiri_tekst TO overload;
COMMENT ON TABLE ac.e_kiri_tekst IS 'Tüüpkirja subjekt ja tekst antud keeles';

I want to insert rows into history tables from actual tables (parent and child) on insertion of row into actual tables using postgresql trigger

For Example:
Consider my original tables are:
CREATE TABLE employees(
id serial primary key,
first_name varchar(40) NOT NULL,
last_name varchar(40) NOT NULL
);
CREATE TABLE employees_detail
(
eid integer NOT NULL,
id integer,
first_name character varying(40),
last_name character varying(40),
CONSTRAINT employees_detail_pkey PRIMARY KEY (eid),
CONSTRAINT employees_detail_id_fkey FOREIGN KEY (id)
REFERENCES employees (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
The above tables have foreign key relation. So when I insert a row into both the tables, at the same time I want to insert them into history tables.
consider history tables are:
CREATE TABLE employee_audits (
id serial primary key,
employee_id int4 NOT NULL,
last_name varchar(40) NOT NULL,
changed_on timestamp(6) NOT NULL
)
CREATE TABLE employee_audits_detail
(
eid integer NOT NULL,
id integer,
last_name character varying(40),
changed_on timestamp(6) NOT NULL,
CONSTRAINT employee_audits_detail_pkey PRIMARY KEY (eid),
CONSTRAINT employee_audits_detail_fkey FOREIGN KEY (id)
REFERENCES employee_audits (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION
)
I have created below trigger to insert into original parent table(employees) to history parent table(employee_audits). My trigger is as follows:
CREATE OR REPLACE FUNCTION log_last_name_changes()
RETURNS trigger AS
$BODY$
BEGIN
IF NEW.last_name <> OLD.last_name THEN
INSERT INTO employee_audits(employee_id,last_name,changed_on)
VALUES(OLD.id,OLD.last_name,now());
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql
CREATE TRIGGER last_name_changes
BEFORE UPDATE
ON employees
FOR EACH ROW
EXECUTE PROCEDURE log_last_name_changes();
Same way I want to insert original child table rows into history child table.

Need foreign key as array

CREATE TABLE test ( id int PRIMARY KEY , name );
CREATE TABLE test1 ( id integer[] REFERENCES test , rollid int );
ERROR: foreign key constraint "test3_id_fkey" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
after that I try to another way also
CREATE TABLE test1 ( id integer[] , rollid int);
ALTER TABLE test1 ADD CONSTRAINT foreignkeyarray FOREIGN KEY (id) REFERENCES test;
ERROR: foreign key constraint "fkarray" cannot be implemented
DETAIL: Key columns "id" and "id" are of incompatible types: integer[] and integer.
so I try create a foreign key array means it say error. please tell me anyone?
postgresql version is 9.1.
What you're trying to do simply can't be done. At all. No ifs, no buts.
Create a new table, test1_test, containing two fields, test1_id, test_id. Put the foreign keys as needed on that one, and make test1's id an integer.
Using arrays with foreign element keys is usually a sign of incorrect design. You need to do separate table with one to many relationship.
But technically it is possible. Example of checking array values without triggers. One reusable function with paramethers and dynamic sql. Tested on PostgreSQL 10.5
create schema if not exists test;
CREATE OR REPLACE FUNCTION test.check_foreign_key_array(data anyarray, ref_schema text, ref_table text, ref_column text)
RETURNS BOOL
RETURNS NULL ON NULL INPUT
LANGUAGE plpgsql
AS
$body$
DECLARE
fake_id text;
sql text default format($$
select id::text
from unnest($1) as x(id)
where id is not null
and id not in (select %3$I
from %1$I.%2$I
where %3$I = any($1))
limit 1;
$$, ref_schema, ref_table, ref_column);
BEGIN
EXECUTE sql USING data INTO fake_id;
IF (fake_id IS NOT NULL) THEN
RAISE NOTICE 'Array element value % does not exist in column %.%.%', fake_id, ref_schema, ref_table, ref_column;
RETURN false;
END IF;
RETURN true;
END
$body$;
drop table if exists test.t1, test.t2;
create table test.t1 (
id integer generated by default as identity primary key
);
create table test.t2 (
id integer generated by default as identity primary key,
t1_ids integer[] not null check (test.check_foreign_key_array(t1_ids, 'test', 't1', 'id'))
);
insert into test.t1 (id) values (default), (default), (default); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3]); --ok
insert into test.t2 (id, t1_ids) values (default, array[1,2,3,555]); --error
If you are able to put there just values from test.id, then you can try this:
CREATE OR REPLACE FUNCTION test_trigger() RETURNS trigger
LANGUAGE plpgsql AS $BODY$
DECLARE
val integer;
BEGIN
SELECT id INTO val
FROM (
SELECT UNNEST(id) AS id
FROM test1
) AS q
WHERE id = OLD.id;
IF val IS NULL THEN RETURN OLD;
ELSE
RAISE 'Integrity Constraint Violation: ID "%" in Test1', val USING ERRCODE = '23000';
RETURN NULL;
END IF;
END; $BODY$;
-- DROP TRIGGER test_delete_trigger ON test;
CREATE TRIGGER test_delete_trigger BEFORE DELETE OR UPDATE OF id ON test
FOR EACH ROW EXECUTE PROCEDURE test_trigger();