I have an updatable postgres view which exposes a person table. The view hides some private data, such as email. When inserting I want users to be able to insert values for that private data. Would it be better to use a rule or a trigger to gain this functionality?
After some testing it seems like I can't use a trigger to insert to columns not defined in the view.
One possible way is to use privileges instead of view to expose subset of columns.
For example, given the table
create table person(
first_name text,
last_name text,
email text
);
you can grant the following privileges to a user:
grant select(first_name, last_name) on person to someuser;
grant insert on person to someuser;
And this is how it works:
postgres=> insert into person values('Foo','Bar','foo#bar.com');
INSERT 0 1
postgres=> select * from person;
ERROR: permission denied for relation person
postgres=> select first_name, last_name from person;
first_name | last_name
------------+-----------
Foo | Bar
(1 row)
Of course, it is only possible on "per user" basis.
Another way is to use function defined as "security definer". This specifies that the function is to be executed with the privileges of the user that created it.
So you can define a function to insert data directly into the table; definer must have the insert privilege:
create function person_insert(first_name text, last_name text, email text)
returns void
security definer
as $$
insert into person(first_name, last_name, email) values ($1, $2, $3);
$$ language sql;
Then the other user can call it without having himself the insert privilege:
postgres=> insert into person(first_name, last_name, email) values ('foo', 'bar', 'foo#bar.com');
ERROR: permission denied for relation person
postgres=> select person_insert('foo','bar','foo#bar.com');
people_insert
---------------
(1 row)
postgres=> select * from person_view;
first_name | last_name
------------+-----------
Foo | Bar
(1 row)
Related
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
$$;
Perhaps this error can be caused by problems with the SCHEMA, I tried to fix it, but I was completely confused.
Here is the detailed description.
There is a separate file that runs the following commands:
CREATE DATABASE weather;
CREATE SCHEMA public;
CREATE SCHEMA schema1;
SET search_path = schema1, public;
CREATE TABLE "Sities" (
Id SERIAL PRIMARY KEY,
name TEXT,
country TEXT,
weather_id_api int);
CREATE TABLE "Forecasts" (
Id SERIAL PRIMARY KEY,
city_id int,
time DATE,
temp INT,
humidity INT,
pressure INT);
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres;
GRANT usage ON SCHEMA public TO postgres;
The execution of each command is checked for errors. These commands are executed without problems.
Further in another file such commands are executed:
SET search_path = schema1, public;
INSERT INTO "Sities" (name, country, weather_id_api)
SELECT 'Orenburg', "RU", 234234
WHERE NOT EXISTS (SELECT name FROM "Sities" WHERE name="Orenburg");
The last command causes an error:
panic: pq: Relation "Sities" does not exist
goroutine 1 [running]: main.PostToDatabase(0x11731ee0)
D:/Go/src/WeatherSoket/main.go:135 +0x40f main.Update()
D:/Go/src/WeatherSoket/main.go:150 +0x52 main.main()
D:/Go/src/WeatherSoket/main.go:165 +0xbe exit status 2
This works - try to check quotes " and apostrophes ':
SET search_path = schema1, public;
INSERT INTO "Sities" (name, country, weather_id_api)
SELECT 'Orenburg', 'RU', 234234
WHERE NOT EXISTS (SELECT name FROM "Sities" WHERE name='Orenburg');
http://sqlfiddle.com/#!17/5abd9/4
This question already has answers here:
Get the name of a row's source table when querying the parent it inherits from
(2 answers)
Closed 2 years ago.
Take the following query:
CREATE TEMP TABLE users
(
user_id SERIAL,
name varchar(50)
);
CREATE TEMP TABLE admins
(
section integer
) INHERITS(users);
INSERT INTO users (name) VALUES ('Kevin');
INSERT INTO admins (name, section) VALUES ('John', 1);
CREATE FUNCTION pg_temp.is_admin(INTEGER) RETURNS BOOLEAN AS
$$
DECLARE
result boolean;
BEGIN
SELECT INTO result COUNT(*) > 0
FROM admins
WHERE user_id = $1;
RETURN result;
END;
$$
LANGUAGE PLPGSQL;
SELECT name, pg_temp.is_admin(user_id) FROM users;
Is there any postgres feature that would allow me to get rid of the is_admin function? Basically to check the row class type (in terms of inheritance)?
I understand the table design isn't ideal, this is just to provide a simple example so I can find out if what I am after is possible.
You can use the tableoid hidden column for this.
SELECT tableoid, * FROM users;
or in this case:
SELECT tableoid = 'admins'::regclass AS is_admin, * FROM users;
Note, however, that this will fall apart horribly if you want to find a non-leaf membership, i.e. if there was superusers that inherited from admins, a superuser would be reported here with is_admin false.
AFAIK there's no test for "is member of a relation or any child relation(s)", though if you really had t you could get the oids of all the child relations with a subquery, doing a tableoid IN (SELECT ...).
When using table inheritance, I would like to enforce that insert, update and delete statements should be done against descendant tables. I thought a simple way to do this would be using a trigger function like this:
CREATE FUNCTION test.prevent_action() RETURNS trigger AS $prevent_action$
BEGIN
RAISE EXCEPTION
'% on % is not allowed. Perform % on descendant tables only.',
TG_OP, TG_TABLE_NAME, TG_OP;
END;
$prevent_action$ LANGUAGE plpgsql;
...which I would reference from a trigger defined specified using BEFORE INSERT OR UPDATE OR DELETE.
This seems to work fine for inserts, but not for updates and deletes.
The following test sequence demonstrates what I've observed:
DROP SCHEMA IF EXISTS test CASCADE;
psql:simple.sql:1: NOTICE: schema "test" does not exist, skipping
DROP SCHEMA
CREATE SCHEMA test;
CREATE SCHEMA
-- A function to prevent anything
-- Used for tables that are meant to be inherited
CREATE FUNCTION test.prevent_action() RETURNS trigger AS $prevent_action$
BEGIN
RAISE EXCEPTION
'% on % is not allowed. Perform % on descendant tables only.',
TG_OP, TG_TABLE_NAME, TG_OP;
END;
$prevent_action$ LANGUAGE plpgsql;
CREATE FUNCTION
CREATE TABLE test.people (
person_id SERIAL PRIMARY KEY,
last_name text,
first_name text
);
psql:simple.sql:17: NOTICE: CREATE TABLE will create implicit sequence "people_person_id_seq" for serial column "people.person_id"
psql:simple.sql:17: NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "people_pkey" for table "people"
CREATE TABLE
CREATE TRIGGER prevent_action BEFORE INSERT OR UPDATE OR DELETE ON test.people
FOR EACH ROW EXECUTE PROCEDURE test.prevent_action();
CREATE TRIGGER
CREATE TABLE test.students (
student_id SERIAL PRIMARY KEY
) INHERITS (test.people);
psql:simple.sql:24: NOTICE: CREATE TABLE will create implicit sequence "students_student_id_seq" for serial column "students.student_id"
psql:simple.sql:24: NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "students_pkey" for table "students"
CREATE TABLE
--The trigger successfully prevents this INSERT from happening
--INSERT INTO test.people (last_name, first_name) values ('Smith', 'Helen');
INSERT INTO test.students (last_name, first_name) values ('Smith', 'Helen');
INSERT 0 1
INSERT INTO test.students (last_name, first_name) values ('Anderson', 'Niles');
INSERT 0 1
UPDATE test.people set first_name = 'Oh', last_name = 'Noes!';
UPDATE 2
SELECT student_id, person_id, first_name, last_name from test.students;
student_id | person_id | first_name | last_name
------------+-----------+------------+-----------
1 | 1 | Oh | Noes!
2 | 2 | Oh | Noes!
(2 rows)
DELETE FROM test.people;
DELETE 2
SELECT student_id, person_id, first_name, last_name from test.students;
student_id | person_id | first_name | last_name
------------+-----------+------------+-----------
(0 rows)
So I'm wondering what I've done wrong that allows updates and deletes directly against the test.people table in this example.
The trigger is set to execute FOR EACH ROW, but there is no row in test.people, that's why it's not run.
As a sidenote, you may issue select * from ONLY test.people to list the rows in test.people that don't belong to child tables.
The solution seems esasy: set a trigger FOR EACH STATEMENT instead of FOR EACH ROW, since you want to forbid the whole statement anyway.
CREATE TRIGGER prevent_action BEFORE INSERT OR UPDATE OR DELETE ON test.people
FOR EACH STATEMENT EXECUTE PROCEDURE test.prevent_action();
To automatically add a column in a second table to tie it to the first table via a unique index, I have a rule such as follows:
CREATE OR REPLACE RULE auto_insert AS ON INSERT TO user DO ALSO
INSERT INTO lastlogin (id) VALUES (NEW.userid);
This works fine if user.userid is an integer. However, if it is a sequence (e.g., type serial or bigserial), what is inserted into table lastlogin is the next sequence id. So this command:
INSERT INTO user (username) VALUES ('john');
would insert column [1, 'john', ...] into user but column [2, ...] into lastlogin. The following 2 workarounds do work except that the second one consumes twice as many serials since the sequence is still auto-incrementing:
CREATE OR REPLACE RULE auto_insert AS ON INSERT TO user DO ALSO
INSERT INTO lastlogin (id) VALUES (lastval());
CREATE OR REPLACE RULE auto_insert AS ON INSERT TO user DO ALSO
INSERT INTO lastlogin (id) VALUES (NEW.userid-1);
Unfortunately, the workarounds do not work if I'm inserting multiple rows:
INSERT INTO user (username) VALUES ('john'), ('mary');
The first workaround would use the same id, and the second workaround is all kind of screw-up.
Is it possible to do this via postgresql rules or should I simply do the 2nd insertion into lastlogin myself or use a row trigger? Actually, I think the row trigger would also auto-increment the sequence when I access NEW.userid.
Forget rules altogether. They're bad.
Triggers are way better for you. And in 99% of cases when someone thinks he needs a rule. Try this:
create table users (
userid serial primary key,
username text
);
create table lastlogin (
userid int primary key references users(userid),
lastlogin_time timestamp with time zone
);
create or replace function lastlogin_create_id() returns trigger as $$
begin
insert into lastlogin (userid) values (NEW.userid);
return NEW;
end;
$$
language plpgsql volatile;
create trigger lastlogin_create_id
after insert on users for each row execute procedure lastlogin_create_id();
Then:
insert into users (username) values ('foo'),('bar');
select * from users;
userid | username
--------+----------
1 | foo
2 | bar
(2 rows)
select * from lastlogin;
userid | lastlogin_time
--------+----------------
1 |
2 |
(2 rows)