Here's what I've been reading:
http://www.postgresql.org/docs/9.2/static/rules-views.html
http://www.postgresql.org/docs/9.2/static/rules-privileges.html
My goal is to allow a login to see only those rows that it "owns", so to speak.
Let's say every table in the database inherits from this table:
create table WHOAMI
(
tenant varchar(25) not null default current_user
);
for example:
create table FOO
(
id int primary key,
invoicedate date
) inherits (WHOAMI);
insert into FOO(id, invoicedate) values(1,now()::date);
select * from FOO;
--abclogin|1|2013-02-01
Is there such a thing in PostgreSQL as a schema-level select rule, affecting all tables and views in the schema, that appends to every select, insert, update, or delete statement a condition that says, in effect, ..AND WHERE TENANT = current_user? If there isn't such a global rule, can it be done on a table-by-table basis? I am not having any success with my attempts, and am probably misunderstanding a few things about how rules are created. Here is what I have tried to do:
I try to create a select-rule:
CREATE RULE "_RETURN" AS ON SELECT TO FOO DO INSTEAD
SELECT * FROM FOO where tenant = current_user;
but get this error: ERROR: could not convert table "foo" to a view because it has indexes
I try to create a view with a security-barrier:
CREATE VIEW TENANTFOO WITH (security_barrier) AS
SELECT * FROM FOO WHERE tenant=current_user;
and then attempt an insert:
insert into TENANTFOO(id,invoicedate)
values(2,(now()::date);
but get this error:
`ERROR: cannot insert into view "tenantfoo"
HINT: You need an unconditional ON INSERT DO INSTEAD rule
or an INSTEAD OF INSERT trigger.`
What steps are required to implement row-level security barriers on tables?
In your last example, you'd need to run the INSERT against the table or create another RULE: ON INSERT TO TENANTFOO DO INSTEAD.
What you're looking for is a Row-Level Security, it is not yet available, although some work had been done on this thing. I hope this patch will make it into the upcoming 9.3 release.
Meanwhile, I've been working with the following design a while ago.
Requirements were similar, views should have been delivering only those rows intended for the CURRENT_USER. In our case access had been done quite simple: a table that specified whether given user had access for the given relation and given key, smth like:
CREATE TABLE user_grants (
user_id integer,
entity_name text, -- should exist in pg_class
entity_id integer
);
Then, say for the tasks, the following view had been created:
CREATE VIEW tasks_v AS
SELECT t.*
FROM tasks t
JOIN user_grants ug ON t.user_id = ug.user_id
AND ug.entity_name='TASKS' AND ug.entity_id = t.task_id;
Of course, the setup is not complete without a number of helper functions, triggers and rules. Also it was necessary to make sure some reasonable default privileges are always granted.
Related
PostGraphile does NOT recommend column-level SELECT grants, instead recommends to
split your concerns into multiple tables and use the
one-to-one relationship feature to link them.
Now I want my users table to have a role field that can be accessed by role_admin but not by role_consumer. Based on the above recommendation, I created two tables. users table (in public schema) contains all fields that both roles can see, and user_accounts (in private schema) contains role field that only role_admin must be able to see. role field is added to the user GraphQL type via computed columns.
CREATE SCHEMA demo_public;
CREATE SCHEMA demo_private;
/* users table*/
CREATE TABLE demo_public.users (
user_id SERIAL PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
);
/* user_accounts */
CREATE TABLE demo_private.user_accounts (
user_id INT PRIMARY KEY REFERENCES demo_public.users (user_id) ON DELETE CASCADE,
role text not null default 'role_consumer',
);
/* role as computed column */
CREATE FUNCTION demo_public.users_role
(
u demo_public.users
)
RETURNS TEXT as $$
<code>
$$ LANGUAGE SQL STRICT STABLE;
Now basically I have two potions to set permissions.
1) The first option is to use table level security. IOW to grant select access on table user_accounts to ONLY role_admin.
GRANT SELECT ON TABLE demo_private.user_accounts TO role_admin;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_admin;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_any_user_accounts ON demo_private.user_accounts FOR SELECT TO role_admin using (true);
The problem with this approach is that when role_consumer runs a query that contains role field
{
me {
firstname
role
}
}
The above query returns an error. This is not good since the error affect the whole result hiding the result of other sibling fields.
2) The other option is to use row level security besides table level; IOW on table level, to grant select access on table user_accounts to both role_admin and role_consumer but in row level only allow admins to access rows of user_accounts.
GRANT USAGE ON SCHEMA demo_private TO role_consumer;
GRANT SELECT ON TABLE demo_private.user_accounts TO role_consumer;
GRANT EXECUTE ON FUNCTION demo_public.users_role(demo_public.users) TO role_consumer;
ALTER TABLE demo_private.user_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY select_user_accounts ON demo_private.user_accounts FOR SELECT
USING ('role_admin' = nullif(current_setting('role', true), ''));
Now if the user with consumer_role runs the aforementioned query, the role field will be null, not affecting its sibling fields. But two questions:
Should we always avoid errors to prevent them affecting their siblings?
If yes, should we always handle things in Row Level and never only in Table Level?
For option 1, throwing an error from PostgreSQL during a query is not a good idea in PostGraphile because we compile the entire GraphQL tree into a single SQL query, so an error aborts the entire query. Instead, I would factor the permissions into the function and simply return null (rather than an error) if the user is not allowed to view it. One way to do this is with an additional WHERE clause:
CREATE FUNCTION demo_public.users_role (
u demo_public.users
) RETURNS TEXT AS $$
select role
from demo_private.user_accounts
where user_id = u.id
and current_setting('jwt.claims.role') = 'role_admin';
$$ LANGUAGE SQL STABLE;
For option 2: this is a perfectly valid solution.
Should we always avoid errors to prevent them affecting their siblings?
It's rare to throw errors when querying things in GraphQL - normally you return null instead. Think of it like visiting a private repository on GitHub when logged out - they don't return the "forbidden" error which reveals that the resource exists, instead they return the 404 error suggesting that it doesn't - unless you know better!
If yes, should we always handle things in Row Level and never only in Table Level?
I personally only use one role with PostGraphile, app_visitor, and that has been sufficient for all applications I've built with PostGraphile so far.
I have multiple tables that I would like users to be able to update through the rest api, and many (if not all) have columns with sensible defaults.
The web app itself can be designed to hide these columns, but I want to allow direct access to the api as well so that others can make use of the data however they see fit.
Unfortunately, this means they can set the defaulted columns explicitly (set timestamp columns to 1972, or set id columns to arbitrary values).
What mechanisms are available to restrict this on the backend (Postgres 9.4)?
You should do this at API level.
If anybody issues a malformed request (e.g. they want to overwrite an ID or a timestamp), answer with a proper status code (perhaps 400), amended with a meaningful message, for instance "Hey you tried to update , which is read only."
If you would really insist to handle it at db level, here they suggest that:
The easiest way is to create BEFORE UPDATE trigger that will compare OLD and NEW row and RAISE EXCEPTION if the change to the row is forbidden.
I've had some luck experimenting with Postgres' column-level grants. It's important in a development environment to make sure that your database users isn't a superuser (if it is, create a second superuser, then revoke it from the dev account with alter role).
Then, commands similar to these can be run on a table:
revoke all on schema.table from dev_user;
grant select, delete, references on schema.table to dev_user;
grant update (col1, col2) on schema.table to dev_user;
grant insert (col1, col2) on schema.table to dev_user;
Some caveats:
Remember to grant "references" as well if another table will fkey to it.
Remember to give col1 and col2 (and any other) sane defaults, because the API will be unable to change those in any way.
DO NOT FORGET TO CREATE A SECOND SUPERUSER ACCOUNT BEFORE REVOKING SUPERUSER STATUS FROM THE DEV ACCOUNT. It is possible to recover this, but a big pain in the ass.
Also, if you're keeping these grant/revocations in the same file as the create table statement, the following form might be of use:
do $$begin execute 'grant select, delete, references on schema.table to ' || current_user; end$$;
This way the statements will translate correctly to production, which may not use the same username as in development.
PostgreSQL since version 9.3 supports updatable views, so instead of exposing actual table you can expose a view with a limited subset of columns:
CREATE TABLE foo (id SERIAL, name VARCHAR, protected NUMERIC DEFAULT 0);
CREATE VIEW foo_v AS SELECT name FROM foo;
Now you can do things like:
INSERT INTO foo_v VALUES ('foobar');
UPDATE foo_v SET name = 'foo' WHERE name = 'foobar';
If you need more you can use INSTEAD INSERT/UPDATE RULE or INSTEAD OF INSERT TRIGGER.
I got table a
CREATE TABLE test (
id SERIAL,
name character varying NOT NULL,
PRIMARY KEY (id)
);
a view
CREATE VIEW TEST_VIEW AS
SELECT id,name
FROM test;
and just want to forward a given update queue to the actual table behind the view
CREATE RULE TEST_VIEW_UPDATE
AS ON UPDATE TO TEST_VIEW
DO INSTEAD UPDATE TEST;
But this approach results in an error as the SET statement is probably missing. How can I do this correctly in the most generic (therefore no limitation on what is actually updated) way?
On PostgreSQL 9.3 this will work automatically and without changes. PostgreSQL will create simple views as updateable by default.
In prior versions, specify all columns in the UPDATE. There's no wildcard.
If you're on 9.1 or above (which you should always mention in every question - select version()) you should use an INSTEAD OF view trigger rather than a rule.
As far as I know, it's not possible to do it like this, you have to write actual command:
CREATE RULE TEST_VIEW_UPDATE
AS ON UPDATE TO TEST_VIEW
DO INSTEAD UPDATE TEST set name = NEW.name, col1 = NEW.col1 where id = NEW.id;
It's also possible to do what you want with triggers - check this and this links.
My site was developed using Drupal 6 running on a Postgresql 8.3 server on Ubuntu 11.10. Also webmin version 1.590.
Now I want to update records in a table, but when I run:
UPDATE uac_institution_view SET status = '2' WHERE nid = '9950'
it gives me an error like:
Failed to execute SQL : SQL UPDATE uac_institution_view SET status =
'2' WHERE nid = '9950' failed : ERROR: cannot update a view HINT: You
need an unconditional ON UPDATE DO INSTEAD rule.
The problem is that only SELECT queries work. UPDATE, INSERT and DELETE commands are not working; they fail with the above error.
Is this a permisssion problem? A syntax error? Something else?
PostgreSQL views are not updateable by default. You must tell PostgreSQL how you want the view to be updated.
Do this using "an unconditional ON UPDATE DO INSTEAD rule" (as the error message you pasted said) or preferably on PostgreSQL 9.1 and above using a view trigger. I provided links to all that in my answer to your previous post, but here's some more info:
updateable views in PostgreSQL 9.1 using INSTEAD OF trigger
updateable views (for Pg 9.0 and below using rules)
CREATE TRIGGER
CREATE VIEW
rules vs triggers
rules
triggers in PL/pgSQL
In many cases it's better to leave the view read-only and just update the underlying table. Since you have not provided a definition of the view it's hard to say what that would actually involve. Update your question with the output of running \d uac_institution_view in psql and comment to say you've done so; maybe I can point out a way to run the update directly on the underlying table(s).
You are using a very obsolete version of PostgreSQL (8.3) so you cannot use the preferred INSTEAD OF trigger approach, you must either use rules or update the underlying table directly.
FYI, after the answer involving rules/triggers was posted, PostgreSQL 9.3 came out with auto-updatable views. Version 9.3 is in beta 2 as of June 27, 2013, so it's not yet GA.
Here is an example: https://web.archive.org/web/20160322164044/http://michael.otacoo.com/postgresql-2/postgres-9-3-feature-highlight-auto-updatable-views/
I am on postgres 9.5 and views are updatable by default.
Example :
CREATE TABLE UP_DATE (id number, name varchar2(29));
insert into up_date values(1, 'Foo');
select * from up_date;
CREATE OR REPLACE VIEW UPDATE
AS
Select
name from up_date;
select * from update;
insert into update values('Bar');
select * from update;
Will out put Foo and Bar
Everything just works from PG 9.3 onwards as noted by Jeff French... With some exceptions (more info below).
Simple example
You can test this code on your PostgreSQL. Use cascade drop when you are done (to drop view with the table).
-- create table
--DROP TABLE user_table CASCADE;
CREATE TABLE user_table (
id serial,
lastname varchar(100),
user_type varchar(2) DEFAULT 'nn',
PRIMARY KEY (id)
);
-- initial data
INSERT INTO user_table(lastname) VALUES('Foo');
SELECT * FROM user_table;
-- simple view (note, no id here)
CREATE OR REPLACE VIEW user_view
AS
SELECT lastname, user_type
FROM user_table
;
-- check view (will have initial data)
SELECT * FROM user_view;
-- insert into user_table via view
INSERT INTO user_view VALUES('Bar');
-- check (both will have both records)
SELECT * FROM user_view;
SELECT * FROM user_table;
-- you can run above many times
-- (id will auto-increment even though it is not in the view)
-- update user_table via view
UPDATE user_view SET user_type='v' WHERE lastname = 'Bar';
SELECT * FROM user_table;
Limitations
There are some limitations though and that will depend on you PG version.
In PG 9.3 views cannot have any expressions etc. Also only one table is allowed... So more or less a simple select limiting, reordering, or renaming columns.
In PG 9.4 views can be partially updatable. So you can have expression but you will not be able to update them (not out of the box at least).
If you have WHERE in your view then you might get a bit weird results. So this will still work with insert:
CREATE OR REPLACE VIEW user_view
AS
SELECT lastname as last_name, user_type
FROM user_table
WHERE user_type = 'v'
;
INSERT INTO user_view VALUES('Bar');
But update might not work. At least this will not work in that it will update 0 rows:
UPDATE user_view SET user_type='v';
Because effectively that would be equivalent to below query (so makes sense if you think about it):
UPDATE user_view SET user_type='v' WHERE user_type = 'v';
I wonder if at some point they might support joins... But at the time of writing PG 14 is out and it doesn't support joined tabled in views (for updates I mean).
Alternatives
You can still use INSTEAD OF triggers, especially for more complicated views. And you can use rules... But (as noted in the CREATE RULE docs) automatically updatable views will be faster then manually created rules.
In a SSIS package at work there are some SQL tasks that create staging tables for holding import data. All the statements take this form:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'dbo.tbNewTable') AND type in (N'U'))
BEGIN
TRUNCATE TABLE dbo.tbNewTable
END
ELSE
BEGIN
CREATE TABLE dbo.tbNewTable (
ColumnA VARCHAR(10) NULL,
ColumnB VARCHAR(10) NULL,
ColumnC INT NULL
) ON PRIMARY
END
In Itzik Ben-Gan's T-SQL Fundamentals I see a different form of statement for creating a table:
IF OBJECT_ID('dbo.tbNewTable', 'U') IS NOT NULL
BEGIN
DROP TABLE dbo.tbNewTable
END
CREATE TABLE dbo.tbNewTable (
ColumnA VARCHAR(10) NULL,
ColumnB VARCHAR(10) NULL,
ColumnC INT NULL
) ON PRIMARY
Each of these appears to do the same thing. After execution, there will be a empty table called tbNewTable in the dbo schema.
Are there any practical or theoretical differences between the two? What implications might they have?
The first one assumes that if the table exists, it has the same columns as those it would create. The second one does not make that assumption. So if a table with that name happened to exist and had a different set of columns, the two would have very different results.
The first will not actually DROP the table -- it merely TRUNCATES all the data in said table. Hence why the CREATE is guarded.
Thus the form with the DROP will allow the subsequent CREATE to change the schema (when the new table is created) even if tbNewTable previously existed.
Because the DROP/CREATE alters the database schema it may not also be allowed in all cases. For instance, a view created with a SCHEMABINDING will prevent the table from being dropped. (This also hold true for more general FK relationships, should any exist.)
...when SCHEMABINDING is specified, the base table or tables cannot be modified in a way that would affect the view definition.
The TRUNCATE should be marginally faster in one of those constant "don't care" ways: there should be no performance consideration given to one over the other.
There are also permission differences. TRUNCATE only requires the ALTER permission.
The minimum permission required is ALTER on table_name. TRUNCATE TABLE permissions default to the table owner...
Happy coding.
These are very different..
The first does an equality check on the sys.objects system table and looks to see if there is a matching table name. If so, it truncates the table. Basically removing all rows but maintaining the table structure itself - i.e. the actual table is never dropped.
In the second, the check to make sure that the table exists is implicitly done using the OBJECT_ID() method. If so, the table is dropped completely - rows and structure.
If you have a primary and foreign key constraint on the table, you'll certainly have issues dropping it completely... and if you have other tables that are linked to the table you are trying to 'truncate' you'll have issues there too, unless you have cascade deletion turned on.
I tend to dislike either construction in an SSIS package. I create the tables in a deployment script and I want the package to fail if one of the tables I use is missing later on because then something drastically wrong has happened and I want to investigate what before I try putting data anywhere.