Postgres Row Level Security doesn't allow update on restrictive select policy - sql

Background
I'm working these days on organizing the Postgres database of a project in my work.
EDIT:
This database is connected to a NodeJS server that runs Postgraphile on it, in order to expose the GraphQL interface for the client. Therefore, I have to use RLS in order to forbid the user to query and manipulate rows that he/she doesn't have permission.
One of the tasks that I've got is to add a deleted field for each table, and using RLS to hide the records that deleted = true.
Code Example
To explain my problem, I'll add an SQL code for building a fake database:
Roles
For this example, I'll use these roles:
Superuser role named admin
Role called app_users
2 Users inherit from app_users:
bob
alice
CREATE ROLE admin WITH
LOGIN
SUPERUSER
INHERIT
CREATEDB
CREATEROLE
NOREPLICATION
ENCRYPTED PASSWORD 'md5f6fdffe48c908deb0f4c3bd36c032e72'; -- password: admin
GRANT username TO admin;
CREATE ROLE app_users WITH
NOLOGIN
NOSUPERUSER
NOINHERIT
NOCREATEDB
CREATEROLE
NOREPLICATION;
CREATE ROLE bob WITH
LOGIN
NOSUPERUSER
INHERIT
NOCREATEDB
NOCREATEROLE
NOREPLICATION
ENCRYPTED PASSWORD 'md5e8557d12f6551b2ddd26bbdd0395465c';
GRANT app_users TO bob;
CREATE ROLE alice WITH
LOGIN
NOSUPERUSER
INHERIT
NOCREATEDB
NOCREATEROLE
NOREPLICATION
ENCRYPTED PASSWORD 'md5579e43b423b454623383471aeb85cd87';
GRANT app_users TO alice;
Database
This example will hold a database named league for a mock database for an American football league.
CREATE DATABASE league
WITH
OWNER = admin
ENCODING = 'UTF8'
LC_COLLATE = 'en_US.utf8'
LC_CTYPE = 'en_US.utf8'
TABLESPACE = pg_default
CONNECTION LIMIT = -1;
GRANT CREATE, CONNECT ON DATABASE league TO admin;
GRANT TEMPORARY ON DATABASE league TO admin WITH GRANT OPTION;
GRANT TEMPORARY, CONNECT ON DATABASE league TO PUBLIC;
Scheme: public
I've added some minor changes in the scheme, so in default, role app_users allow any command, type, execute function, etcetera.
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON TABLES TO app_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT ALL ON SEQUENCES TO app_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT EXECUTE ON FUNCTIONS TO app_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE ON TYPES TO app_users;
Creating Tables
Table: TEAMS
CREATE TABLE IF NOT EXISTS public."TEAMS"
(
id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
deleted boolean NOT NULL DEFAULT false,
name text COLLATE pg_catalog."default" NOT NULL,
owner text COLLATE pg_catalog."default" NOT NULL,
CONSTRAINT "TEAMS_pkey" PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE public."TEAMS"
OWNER to admin;
ALTER TABLE public."TEAMS"
ENABLE ROW LEVEL SECURITY;
GRANT ALL ON TABLE public."TEAMS" TO admin;
GRANT ALL ON TABLE public."TEAMS" TO app_users;
CREATE POLICY teams_deleted
ON public."TEAMS"
AS RESTRICTIVE
FOR SELECT
TO app_users
USING (deleted = false);
CREATE POLICY teams_owner
ON public."TEAMS"
AS PERMISSIVE
FOR ALL
TO app_users
USING (owner = CURRENT_USER);
Table: PLAYERS
CREATE TABLE IF NOT EXISTS public."PLAYERS"
(
id text COLLATE pg_catalog."default" NOT NULL,
deleted boolean NOT NULL DEFAULT false,
first_name text COLLATE pg_catalog."default" NOT NULL,
last_name text COLLATE pg_catalog."default" NOT NULL,
team_id integer NOT NULL,
jersey_number integer NOT NULL,
CONSTRAINT "PLAYERS_pkey" PRIMARY KEY (id),
CONSTRAINT fkey_team_id FOREIGN KEY (team_id)
REFERENCES public."TEAMS" (id) MATCH SIMPLE
ON UPDATE CASCADE
ON DELETE CASCADE,
CONSTRAINT check_player_number CHECK (jersey_number > 0 AND jersey_number < 100)
)
TABLESPACE pg_default;
ALTER TABLE public."PLAYERS"
OWNER to admin;
ALTER TABLE public."PLAYERS"
ENABLE ROW LEVEL SECURITY;
GRANT ALL ON TABLE public."PLAYERS" TO admin;
GRANT ALL ON TABLE public."PLAYERS" TO app_users;
CREATE POLICY players_deleted
ON public."PLAYERS"
AS RESTRICTIVE
FOR SELECT
TO app_users
USING (deleted = false);
CREATE POLICY players_owner
ON public."PLAYERS"
AS PERMISSIVE
FOR ALL
TO app_users
USING ((( SELECT "TEAMS".owner
FROM "TEAMS"
WHERE ("TEAMS".id = "PLAYERS".team_id)) = CURRENT_USER));
Test Case (Edited for better understanding)
Run this code using user bob:
INSERT INTO "TEAMS" (name, owner)
VALUES ('Jerusalem Lions', 'bob')
RETURNING id; -- We'll save this id for the next command
INSERT INTO "PLAYERS" (id, first_name, last_name, jersey_number, role, team_id)
VALUES ('99999', 'Eric', 'Cohen', 29, 'linebacker', 888) -- Replace 888 with the returned id from the previous command
RETURNING *;
-- These commands will work
SELECT * FROM "PLAYERS";
UPDATE "PLAYERS"
SET last_name = 'Levi'
WHERE id = '99999'
RETURNING *;
-- This is the command that won't work. I can't change the deleted.
UPDATE "PLAYERS"
SET deleted = true
WHERE id = '99999'
RETURNING *;
EDIT: Now, it's important to understand that The policies as defined above works when I do any query, as long as:
INSERT INTO doesn't include deleted = true (that's ok).
UPDATE that includes SET deleted = true. (This is the main issue).
I want to:
Allow bob to delete a record using deleted = true on an UPDATE command.
Hide in SELECT commands all records that deleted = true.
What should I do? 🤷‍♂️

From the documentation:
When a mix of permissive and restrictive policies are present, a record is only accessible if at least one of the permissive policies passes, in addition to all the restrictive policies.
It means that you cannot permit in one policy something restricted in another policy. For this reason, restrictive policies should be used with extreme caution. When there is no policy defined, everything is restricted, so you should focus on permitting what should be allowed.
Simplified example:
create table my_table(
id int primary key,
user_name text,
deleted bool);
alter table my_table enable row level security;
create policy rule_for_select
on my_table
as permissive
for select
to app_users
using (not deleted);
create policy rule_for_all
on my_table
as permissive
for all
to app_users
using (user_name = current_user and not deleted);
insert into my_table(id, user_name, deleted) values
(1, 'alice', false),
(2, 'bob', true),
(3, 'celine', true)
The user bob will see row 1. He would be able to update row 2 if deleted is false.

What I can gather is, that
UPDATE "PLAYERS" SET deleted=true
will not fail for "Bob". But:
UPDATE "PLAYERS" SET deleted=true RETURNING *
will. Postgraphile will usually "return" something when doing mutations. After updating the row - you basically forbid the select statement.
The usual hack (feature) in Postgraphile land is usually creating a custom mutation and define it as security definer.
Maybe there are other ways around this - but this is the hammer->nail approach in Postgraphile, in my experience.

Similar problem can be seen here: Unable to update row of table with Row Level Security where I proposed a workaround by implementing a grace period.

Related

Setting up a role and linking it to an user is not working as expected in SQL

I created a role R_CLIENTE:
CREATE ROLE R_CLIENTE IDENTIFIED BY RolCliente;
Then I granted some privileges on it:
GRANT SELECT ON alquiler.CLIENTE TO R_CLIENTE;
(Schema alquiler and table CLIENTE already exist). Then I created an user U_Cliente1:
CREATE USER U_Cliente1 IDENTIFIED BY Cliente1 DEFAULT TABLESPACE table_def TEMPORARY TABLESPACE table_temp QUOTA 2M ON table_def PASSWORD EXPIRE;
(Both tablespaces already exist). I granted U_Cliente1 to R_CLIENTE privileges:
GRANT R_CLIENTE TO U_Cliente1;
When I login as U_Cliente1 I am not able to select any data from the table alquiler.CLIENTE:
SQL> desc alquiler.CLIENTE;
ERROR:
ORA-04043: object alquiler.CLIENTE does not exist
However, if I grant directly the privilege to the user U_Cliente1:
GRANT SELECT ON alquiler.CLIENTE TO U_Cliente1;
Now I am able to select the table alquiler.CLIENTE:
SQL> desc alquiler.CLIENTE;
Name Null? Type
----------------------------------------- -------- ----------------------------
K_CODCLIENTE NOT NULL NUMBER(5)
N_NOMBRE1 NOT NULL VARCHAR2(15)
N_NOMBRE2 VARCHAR2(15)
N_APELLIDO1 NOT NULL VARCHAR2(15)
N_APELLIDO2 VARCHAR2(15)
N_DIRECCION NOT NULL VARCHAR2(50)
Q_TELEFONO NOT NULL NUMBER(10)
K_CODREF NUMBER(5)
I_TIPOID NOT NULL VARCHAR2(2)
Q_IDENTIFICACION NOT NULL VARCHAR2(10)
How can I properly link a role and user(s) so they can all share the same privileges?
The reason that the direct grant worked the way it did is because direct grants are always active. Roles can be activated and deactivated within a session. In your example you didn't configure the role as a default role, so it must be explicitly activated after you login, like this:
set role r_cliente;
alternatively, after granting the role set it as a default for the user:
alter user U_Cliente1 default role r_cliente;
or
alter user U_Cliente1 default role all;
Then the role will be active automatically when you login.

RLS Policy doesn't applied for table in PostgreSQL while the RLS has been enabled

I've trying to apply RLS on my PostgreSQL 14. But it seems didn't work. Here it's my SQL (i run it on PgAdmin4):
CREATE TABLE IF NOT EXISTS public.employee
(
tenant_id character varying(255) NOT NULL,
username character varying(255) NOT NULL,
CONSTRAINT employee_pkey PRIMARY KEY (username)
)
ALTER TABLE IF EXISTS employee
OWNER to postgres;
ALTER TABLE IF EXISTS employee
ENABLE ROW LEVEL SECURITY;
CREATE POLICY employee_tenant_isolation_policy
ON employee
AS PERMISSIVE
FOR ALL
TO public
USING (((tenant_id)::text = ((current_setting('app.tenant_id'::text))::character varying)::text));
i'm also already have to insert the data:
INSERT INTO public.employee(
tenant_id, username)
VALUES ('tenant1', 'tenant1');
INSERT INTO public.employee(
tenant_id, username)
VALUES ('tenant2', 'tenant2');
As you can see, i have enabled the RLS and created the policy. But, when i try this query:
SET app.tenant_id to 'tenant1';
SELECT * FROM employee;
I got two values (tenant1, and tenant2). I expect it to only return row with 'tenant1' values.
Any ideas?
Thanks.
As per the documentation, superusers bypass RLS:
Superusers and roles with the BYPASSRLS attribute always bypass the row security system when accessing a table. Table owners normally bypass row security as well, though a table owner can choose to be subject to row security with ALTER TABLE ... FORCE ROW LEVEL SECURITY.
If you create a non-superuser, you should be able to see the filtering:
edb=# create user foobar with login;
CREATE ROLE
edb=# grant select on employee to foobar;
GRANT
edb=# \c edb foobar;
You are now connected to database "edb" as user "foobar".
edb=> SET app.tenant_id to 'tenant1';
SET
edb=> select * from employee ;
tenant_id | username
-----------+----------
tenant1 | tenant1
(1 row)

How to revoke permissions of a specific field in PostgreSQL

I have a table like:
CREATE TABLE public.pagos
(
condominio character varying(12) NOT NULL,
id_unidad character varying(10) NOT NULL,
fechapago date NOT NULL,
montopago integer NOT NULL
)
A user:
CREATE USER user1 WITH PASSWORD 'user1';
I give permissions to it user:
GRANT SELECT ON pagos TO user1;
How I can Revoke all permissions (in this case only select) of user1 in a specific field of the table (like the montopago field) ?
Thanks !
Permissions in SQL are additive, so you can never make exceptions of the kind "grant select on the whole table, but revoke it from one specific column". What you need to do is to revoke the permission you granted on the whole table and grant column permissions instead:
REVOKE SELECT ON pagos FROM user1;
GRANT SELECT (condominio, id_unidad, fechapago) ON pagos TO user1;

Why can I still not access any DB function in PSQL?

This is my init.sql file :
DROP DATABASE IF EXISTS my_data;
CREATE DATABASE my_data;
DROP USER IF EXISTS u;
CREATE USER u WITH PASSWORD 'secret';
GRANT ALL ON ALL TABLES IN SCHEMA public TO u;
GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO u;
GRANT ALL ON ALL FUNCTIONS IN SCHEMA public TO u;
\c my_data;
CREATE TABLE users (
ID SERIAL PRIMARY KEY NOT NULL,
email VARCHAR NOT NULL DEFAULT '',
password VARCHAR NOT NULL DEFAULT '',
active SMALLINT NOT NULL DEFAULT '1',
created TIMESTAMP NOT NULL,
modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
salt VARCHAR NOT NULL DEFAULT ''
);
Then if I :
psql -f init.sql
And..
psql -d my_data -U u
my_data=> select * from users;
ERROR: permission denied for relation users
Why would this permission be denied if I just granted it?
More info
my_data=> \z users
Access privileges
Schema | Name | Type | Access privileges | Column access privileges
--------+-------+-------+-------------------+--------------------------
public | users | table | |
(1 row)
You only gave permission on the schema, which is separate from the tables. If you want to give permissions on the tables also you can use
GRANT ALL ON ALL TABLES IN SCHEMA public TO u;
Note that if you have sequences or other objects they also need separate permissions.
This has to be set after the tables have been created since permissions are set for existing objects.
If you want to set default permissions for new objects you can use
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO u;

Error connecting to SQL database I have created with a user I have created

I have some problems with connecting to a databases using the users i have created.Firstly i created the users like this:
create user util1 identified by user1;
create user util2 identified by user2;
create user util3 identified by user3;
create user util4 identified by user4;
create user util5 identified by user5;
create user util6 identified by user6;
create user util7 identified by user7;
create user util8 identified by user8;
grant dba to util1,util2,util3,util4,util5,util6,util7,util8;
select lpad(' ', 2*level) || granted_role "User, his roles and privileges"
from (select null grantee, username granted_role from dba_users where username like upper('%UTIL%')
union
select grantee, granted_role from dba_role_privs
union
select grantee, privilege from dba_sys_privs)
start with grantee is null
connect by grantee = prior granted_role;
This is working fine.But when i try to create a table using "util1"'s rights it doesn't work:
connect util1/user1 AS SYSDBA;
create table pr1(
cod_subansamblu number(4),
denumire varchar2(20) not null,
cantitate number(7,3),
UM varchar2(3),
pret_unitar number(9,2),
cod_ansamblu number(4),
cod_sectie number(4) not null,
constraint pr1_cod_subansamblu_pk primary key(cod_subansamblu),
constraint pr1_UM_chk check(UM in ('BUC','ML','MP','MC','SET'))
);
I really don't know what is going wrong. Help will be greatly appreciated.
The error message I receive is:
Last Execution Details
Results
Statistics Plan
ORA-00900: invalid SQL statement
There is nothing wrong with the CREATE TABLE statement:
SQL> create table pr1(
cod_subansamblu number(4),
denumire varchar2(20) not null,
cantitate number(7,3),
UM varchar2(3),
pret_unitar number(9,2),
cod_ansamblu number(4),
cod_sectie number(4) not null,
constraint pr1_cod_subansamblu_pk primary key(cod_subansamblu),
constraint pr1_UM_chk check(UM in ('BUC','ML','MP','MC','SET'))
); 2 3 4 5 6 7 8 9 10 11
Table created.
SQL>
So it's hard to understand why it throws ORA-00900 when you run it. What client are you using to run it? SQL*Plus? An IDE like TOAD? Can you provide a cut'n'paste of the complete session so we can see exactly what's going on?
The other thing to bear in mind is, if you connect using as sysdba you login to the database as SYS, regardless of whether you specify a user in the CONNECT string:
SQL> conn apc/password as sysdba
Connected.
SQL> sho user
USER is "SYS"
SQL>
(This only works if you are logged into the OS as the oracle account or other appropriately privileged user).
So you may think you are running your create table statement as UTIL1 but you would in fact be running it as SYS, which is not a good idea. In fact it is bad practice.