Postgres Parent-Child row level security that can inherit from parents? - postgresql

Child table that looks like this
CREATE TABLE folder_item (
id uuid PRIMARY KEY DEFAULT gen_random_uuid()
,parent_id uuid REFERENCES folder_item (id) ON DELETE CASCADE
,role text NOT NULL DEFAULT 'inherit'
);
With a permissions model
CREATE POLICY folder_item_rolecheck ON folder_item FOR SELECT USING ( role = assigned_role );
However if it finds a row with 'inherit' I want it to look at the parent role instead (recursively)
Is that possible?

-- Set NO FORCE ROW LEVEL SECURITY on table "folder_item" to off RLS for OWNER
ALTER TABLE folder_item NO FORCE ROW LEVEL SECURITY
-- Create function with RECURSIVE qwery and SECURITY DEFINER with OWNER as for table "folder_item"
CREATE OR REPLACE FUNCTION folder_item_check_child(
in_parent_id uuid
, in_role text)
RETURNS boolean
LANGUAGE 'plpgsql'
COST 100
STABLE SECURITY DEFINER
AS $BODY$BEGIN
RETURN EXISTS(
WITH RECURSIVE
childs AS (
SELECT tt.id, tt.role FROM folder_item AS tt
WHERE tt.parent_id=in_parent_id
UNION
SELECT child.id, child.role
FROM childs AS parent
INNER JOIN folder_item AS child ON child.parent_id=parent.id
)
SELECT * FROM childs AS tt WHERE tt.role=in_role
);
END$BODY$;
-- CREATE POLICY
CREATE POLICY folder_item_rolecheck ON folder_item FOR SELECT USING ( role = assigned_role
OR folder_item_check_child(id, assigned_role)
);

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
$$;

How to have parent record data available when a child one is deleted through cascade

Consider the following two tables:
CREATE TABLE public.parent
(
id bigint NOT NULL DEFAULT nextval('parent_id_seq'::regclass),
CONSTRAINT pk_parent PRIMARY KEY (id)
);
CREATE TABLE public.child
(
child_id bigint NOT NULL DEFAULT nextval('child_child_id_seq'::regclass),
parent_id bigint NOT NULL,
CONSTRAINT pk_child PRIMARY KEY (child_id),
CONSTRAINT inx_parent FOREIGN KEY (parent_id)
REFERENCES public.parent (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX fki_child
ON public.child
USING btree
(parent_id);
CREATE TRIGGER child_trg
BEFORE DELETE
ON public.child
FOR EACH ROW
EXECUTE PROCEDURE public.trg();
And the trg is defined as:
CREATE OR REPLACE FUNCTION public.trg()
RETURNS trigger AS
$BODY$BEGIN
INSERT INTO temp
SELECT p.id
FROM parent p
WHERE
p.id = OLD.parent_id;
return OLD;
END;$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
To sum up what is happening, there're two tables with a simple parent-child relationship and a cascade on it. There's also a trigger defined on child listening to deletion. I need to access parent's data, in the trigger, when the child's records are deleted due to cascade on parent-child relation. But I can not since they are already deleted! Does anyone have any idea how?
One solution would be to create a BEFORE DELETE trigger on parent instead, which can see all data.
CREATE OR REPLACE FUNCTION public.trg_parent()
RETURNS trigger AS
$func$
BEGIN
INSERT INTO some_tbl (id) -- use target list !!
VALUES (OLD.parent_id);
RETURN OLD;
END
$func$ LANGUAGE plpgsql;
CREATE TRIGGER parent_trg
BEFORE DELETE ON public.parent
FOR EACH ROW EXECUTE PROCEDURE public.trg_parent();

Create unique constraint initially disabled

This is my table :
CREATE TABLE [dbo].[TestTable]
(
[Name1] varchar(50) COLLATE French_CI_AS NOT NULL,
[Name2] varchar(255) COLLATE French_CI_AS NULL,
CONSTRAINT [TestTable_uniqueName1] UNIQUE ([Name1]),
CONSTRAINT [TestTable_uniqueName1Name2] UNIQUE ([Name1], [Name2])
)
ALTER TABLE [dbo].[TestTable]
ADD CONSTRAINT [TestTable_uniqueName1]
UNIQUE NONCLUSTERED ([Name1])
ALTER TABLE [dbo].[TestTable]
ADD CONSTRAINT [TestTable_uniqueName1Name2]
UNIQUE NONCLUSTERED ([Name1], [Name2])
GO
ALTER INDEX [TestTable_uniqueName1]
ON [dbo].[TestTable]
DISABLE
GO
My idea is to enable/disable one or other unique contraint depending on the customer application. With this way, I can catch the thrown exception in my c# code, and display a specific error message to the GUI.
Now, my problem is to alter the collation of columns Name1 & Name2, I need to make them case sensitive (French_CS_AS). To alter these fields, I have to drop the two constraints and recreate it. According to the explained schema, I cannot create an enabled constraint and then disable it, because by some customers, I have duplicate keys for one or other constraint.
For my update script, my idea number 1 was
Save the name of enabled constraints in a temp table
Drop the constraints
Alter columns
Create DISABLED unique constraints
Enable specific constraints according to the saved values in points 1.
My problem is in point 4., I don't find how to create a disabled unique constraint with an ALTER TABLE statement. Is it possible to create it directly in the sys.indexes table ?
My idea number 2 was
Rename TestTable to TestTableCopy
Recreate TestTable with the new fields collation, and otherwise the same schema (indexes, FK, triggers, ...)
Disable specifical unique contraints in TestTable
Migrate data from TestTableCopy to TestTable
Drop TestTableCopy
In this way, my fear is to loose some links with other tables/dependencies, beceause it is a central table in my database.
Is there any other way to achieve my goal?
If necessary, I can use unique indexes instead of unique constraints.
It looks like it is impossible to create a unique index on a column that already has duplicate values.
So, rather than having a disabled unique index either:
not have an index at all (which is the same as having a disabled index from the query processor point of view),
or create a non-unique index.
For those instanses where your client has unique data create unique index. For those instanses where your client has non-unique data create non-unique index.
CREATE PROCEDURE [dbo].[spUsers_AddUsers]
#Name1 varchar(50) ,
#Name2 varchar(50) ,
#Unique bit
AS
declare #err int
begin tran
if #Unique = 1 begin
if not exists (SELECT * FROM Users WHERE Name1 = #Name1 and Name2 = #Name2)
begin
INSERT INTO Users (Name1,Name2)
VALUES (#Name1,#Name2)
set #err = ##ERROR
end else
begin
UPDATE Users
set Name1 = #Name1,
Name2 = #Name2
where Name1 = #Name1 and Name2 = #Name2
set #err = ##ERROR
end
end else begin
if not exists ( SELECT * FROM Users WHERE Name1 = #Name1 )
begin
INSERT INTO Users (Name1,Name2)
VALUES (#Name1,#Name2)
set #err = ##ERROR
end else
begin
UPDATE Users
set Name1 = #Name1,
Name2 = #Name2
where Name1 = #Name1
set #err = ##ERROR
end
if #err = 0 commit tran
else rollback tran
So first you check if you need an unique Name1 and Name2 or just Name1. Then if you do you an insert/update based on what constrain you have.

Create view on table which exist in multiple schemas with same name

I am trying to create one view (or view in each schema to be used without modifications) on table which exists in multiple schemas with the same name
create schema company_1;
create schema company_2;
...
CREATE TABLE company_1.orders
(
id serial NOT NULL,
amount real,
paid real,
CONSTRAINT orders_pkey PRIMARY KEY (id )
)
WITH (
OIDS=FALSE
);
CREATE TABLE company_2.orders
(
id serial NOT NULL,
amount real,
paid real,
CONSTRAINT orders_pkey PRIMARY KEY (id )
)
WITH (
OIDS=FALSE
);
....
What is correct way of creating view on table orders without specifying schema for every view or specifying current schema?
What I need and failed to get is either
CREATE OR REPLACE VIEW
public.full_orders AS
SELECT id, amount FROM orders;
or
CREATE OR REPLACE VIEW
company_1.full_orders AS
-- company_2.full_orders AS
-- company_n.full_orders AS
SELECT id, amount FROM current_schema.orders;
Using postgresql 9.2.2
EDIT: The way I went:
CREATE VIEW company_1.full_orders AS
SELECT id, amount FROM company_1.orders;
On schema copy discussed here I butaly do this
FOR src_table IN
SELECT table_name
FROM information_schema.TABLES
WHERE table_schema = source_schema AND table_type = 'VIEW'
LOOP
SELECT view_definition
FROM information_schema.views
WHERE table_name = src_table AND table_schema = source_schema INTO q;
trg_table := target_schema||'.'||src_table;
EXECUTE 'CREATE VIEW ' || trg_table || ' AS '||replace(q, source_schema, target_schema);
END LOOP;
Still looking for better solution...
It's not possible to do with with a straightforward view. The view records the underlying table's identity at creation time, so it is not affected by schema settings done later on.
You could do it using a set-returning function using dynamic SQL, and then wrap that into a view. But I don't think that's a good solution.
I would just create quasi-duplicates for the view, as you have been doing, and enhance my deployment script to keep them all up to date.

Get row to swap tables on a certain condition

I currently have a parent table:
CREATE TABLE members (
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
first_name varchar(20)
last_name varchar(20)
address address (composite type)
contact_numbers varchar(11)[3]
date_joined date
type varchar(5)
);
and two related tables:
CREATE TABLE basic_member (
activities varchar[3])
INHERITS (members)
);
CREATE TABLE full_member (
activities varchar[])
INHERITS (members)
);
If the type is full the details are entered to the full_member table or if type is basic into the basic_member table. What I want is that if I run an update and change the type to basic or full the tuple goes into the corresponding table.
I was wondering if I could do this with a rule like:
CREATE RULE tuple_swap_full
AS ON UPDATE TO full_member
WHERE new.type = 'basic'
INSERT INTO basic_member VALUES (old.member_id, old.first_name, old.last_name,
old.address, old.contact_numbers, old.date_joined, new.type, old.activities);
... then delete the record from the full_member
Just wondering if my rule is anywhere near or if there is a better way.
You don't need
member_id SERIAL NOT NULL, UNIQUE, PRIMARY KEY
A PRIMARY KEY implies UNIQUE NOT NULL automatically:
member_id SERIAL PRIMARY KEY
I wouldn't use hard coded max length of varchar(20). Just use text and add a check constraint if you really must enforce a maximum length. Easier to change around.
Syntax for INHERITS is mangled. The key word goes outside the parens around columns.
CREATE TABLE full_member (
activities text[]
) INHERITS (members);
Table names are inconsistent (members <-> member). I use the singular form everywhere in my test case.
Finally, I would not use a RULE for the task. A trigger AFTER UPDATE seems preferable.
Consider the following
Test case:
Tables:
CREATE SCHEMA x; -- I put everything in a test schema named "x".
-- DROP TABLE x.members CASCADE;
CREATE TABLE x.member (
member_id SERIAL PRIMARY KEY
,first_name text
-- more columns ...
,type text);
CREATE TABLE x.basic_member (
activities text[3]
) INHERITS (x.member);
CREATE TABLE x.full_member (
activities text[]
) INHERITS (x.member);
Trigger function:
Data-modifying CTEs (WITH x AS ( DELETE ..) are the best tool for the purpose. Requires PostgreSQL 9.1 or later.
For older versions, first INSERT then DELETE.
CREATE OR REPLACE FUNCTION x.trg_move_member()
RETURNS trigger AS
$BODY$
BEGIN
CASE NEW.type
WHEN 'basic' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.basic_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
WHEN 'full' THEN
WITH x AS (
DELETE FROM x.member
WHERE member_id = NEW.member_id
RETURNING *
)
INSERT INTO x.full_member (member_id, first_name, type) -- more columns
SELECT member_id, first_name, type -- more columns
FROM x;
END CASE;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
Trigger:
Note that it is an AFTER trigger and has a WHEN condition.
WHEN condition requires PostgreSQL 9.0 or later. For earlier versions, you can just leave it away, the CASE statement in the trigger itself takes care of it.
CREATE TRIGGER up_aft
AFTER UPDATE
ON x.member
FOR EACH ROW
WHEN (NEW.type IN ('basic ','full')) -- OLD.type cannot be IN ('basic ','full')
EXECUTE PROCEDURE x.trg_move_member();
Test:
INSERT INTO x.member (first_name, type) VALUES ('peter', NULL);
UPDATE x.member SET type = 'full' WHERE first_name = 'peter';
SELECT * FROM ONLY x.member;
SELECT * FROM x.basic_member;
SELECT * FROM x.full_member;