Revoke SELECT permission on table - postgresql

I have the following issue (which seems desperate for me).
I want to have two roles:
Role main_admin who owns MYTABLE.
Role child_admin who only can add columns to MYTABLE but can't select from this table (for privacy reason).
My plan was:
Add role child_admin as subrole to main_admin (so, main_admin is a group role for child_admin).
Revoke SELECT privilege from child_admin
But rovokation doesn't work because child_admin inherits SELECT privilege from main_admin. But I must include child_admin into main_admin because it is the only way to give child_admin the ability to alter MYTABLE (is it right?) So, it looks like vicious circle. Is there some way to solve my issue?

You can have main_admin define a function with SECURITY DEFINER which does the requested ALTER TABLE after validating its input. Then grant EXECUTE on this function to child_admin (who shouldn't be granted the main_admin role).

Related

PostgreSQL Error [42501]: ERROR: must be owner of relation table

I am maintaining a database 'db' in which there are around 100 tables.I have one super user 'A' and 'A' is the owner of all tables. How can I give Alter permission to new user 'B' specific to single table without inheriting all permissions from 'A'.
I tried by providing Grant A to B;. This Grant option given all permissions from 'A' to 'B'. I want above scenario to restrict to one particular table.
Is this possible?
The documentation recently acquired this explanation:
The right to modify or destroy an object is inherent in being the object's owner, and cannot be granted or revoked in itself. (However, like all privileges, that right can be inherited by members of the owning role; see Section 21.3.)
So the only people who can run ALTER TABLE are:
Superusers
the table owner
members of the table owner role
So GRANT a TO b is the only way to give somebody the privilege.
You might be able to use a SECURITY DEFINER function that belongs to a, but be careful with that.

Forbid the owner of a user from GRANTing on that table

I'm trying to allow a database user to be able to alter/drop (certain) tables, but not GRANT privileges on them. Is this possible?
It looks like they need to be the owner of the tables, but from https://www.postgresql.org/docs/current/sql-grant.html
The right to drop an object, or to alter its definition in any way, is not treated as a grantable privilege; it is inherent in the owner, and cannot be granted or revoked. (However, a similar effect can be obtained by granting or revoking membership in the role that owns the object; see below.) The owner implicitly has all grant options for the object, too.
This sounds like it's not possible. However, is this definitely the case? Is there some way with triggers for example to make certain GRANTs fail?
Yes, only the owner of a table or a superuser can ALTER or DROP it, and these users can always GRANT privileges on the table.
Your only option is to create an event trigger that fires on GRANT and throws an error for the tables where it should be forbidden.

Postgres permission to select from information_schema tables

I'm trying to lock down the user permissions used by an application to connect to its Postgres database. The idea is that the application just needs to access data but not create or drop tables. I created a role called readwrite and assigned the role to the user. I configured the role like this:
CREATE ROLE readwrite;
GRANT CONNECT ON DATABASE corre TO readwrite;
GRANT USAGE, CREATE ON SCHEMA public TO readwrite;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO readwrite;
I’ve discovered that the role breaks a specific select that's done in a trigger function. The select is:
SELECT c.column_name::text
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema
AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
WHERE constraint_type = 'PRIMARY KEY' and tc.table_name = TG_TABLE_NAME;
This is to find out the name of the PK column of the table. The select works fine for the user postgres because it’s an admin. It returns a single row (I don't have any composite PKs). If I run the select as a user with the readwrite role, it runs but returns no rows.
I think I need to grant the role some additional permission for the select to work but I have no idea which one.
Any ideas how I can get this to work as intended?
UPDATE: I originally noticed the issue on Postgres 10.6 but I've also confirmed the same behavior on 11.5
UPDATE 2: Breaking down the select above, the role can't see any rows in information_schema.constraint_column_usage. It also misses a handful of rows in the other two tables (compared to selecting as the admin user postgres) but they don't seem relevant. I tried granting REFERENCES permission but that didn't make any difference:
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON TABLES TO readwrite;
Just a side note about reworking default privileges. I might be wrong (someone please correct me), but I remember getting erratic results if I didn't REVOKE defaults before resetting them. GRANTS can be applied in so many places, and they interact in some (at least to me) confusing ways. So, I strip everything down to the metal, and then build it up again. It's been months since I looked at this, but I ended up having to write a script to build up all of the GRANTS statements, here's a sample:
------------------------------------------------------------------------------
-- REVOKE ALL on each schema.
------------------------------------------------------------------------------
REVOKE ALL PRIVILEGES ON SCHEMA api FROM PUBLIC; -- -- Clear out the magic PUBLIC pseudo-user.
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_developers;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM PUBLIC;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_developers;
------------------------------------------------------------------------------
-- GRANT USAGE on each schema and CREATE selectively.
-- Note: The api group only gets access to the api schema.
------------------------------------------------------------------------------
GRANT USAGE, CREATE ON SCHEMA api TO group_admins;
GRANT USAGE ON SCHEMA api TO group_api_users;
GRANT USAGE, CREATE ON SCHEMA api TO group_developers;
------------------------------------------------------------------------------
-- REGRANT tables/views.
------------------------------------------------------------------------------
-- REVOKE ALL on tables/views.
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM PUBLIC;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_developers;
-- GRANT rights that can be applied to all tables/views in a schema.
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER, TRUNCATE ON ALL TABLES IN SCHEMA api TO group_admins;
GRANT SELECT ON ALL TABLES IN SCHEMA api TO group_api_users;
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER ON ALL TABLES IN SCHEMA api TO group_developers;
-- GRANT full CRUD rights selectively by table.
-- Note: group_admins and group_developers are granted full CRUD rights on all tables above.
-- Snip
------------------------------------------------------------------------------
-- REGRANT DEFAULT privileges
------------------------------------------------------------------------------
-- Clear any existing table defaults from each schema.
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM PUBLIC;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_admins;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_api_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_developers;
-- ALTER DEFAULT PRIVILEGES that can be applied to all tables/views in a schema
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER, TRUNCATE ON TABLES TO group_admins;
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT ON TABLES TO group_api_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER ON TABLES TO group_developers;
After that, I tend to double-check table and view rights. Here's a function I adapted from code I found to summarize table grants:
CREATE OR REPLACE FUNCTION data.show_table_rights(t_name text)
RETURNS TABLE("Table_Name" name, "User_Name" name, "SELECT" text, "INSERT" text, "UPDATE" text, "DELETE" text, "TRUNCATE" text, "REFERENCES" text, "TRIGGER" text)
LANGUAGE sql
STABLE
AS $function$
SELECT
t.tablename,
u.usename,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'select') = TRUE then 'X' ELSE ' ' END AS select,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'insert')= TRUE then 'X' ELSE ' ' END AS insert,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'update') = TRUE then 'X' ELSE ' ' END AS update,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'delete') = TRUE then 'X' ELSE ' ' END AS delete,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'truncate') = TRUE then 'X' ELSE ' ' END AS truncate,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'references') = TRUE then 'X' ELSE ' ' END AS references,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'trigger') = TRUE then 'X' ELSE ' ' END AS trigger
FROM pg_tables t,
pg_user u
WHERE t.tablename = t_name
ORDER BY u.usename;
$function$
I don't love that function...but I don't hate it enough that I ever get around to rewriting it. (The bits I hate are my fault, not whoever I adapted it from.) If I were to rewrite it, I'd get rid of the uppercase column titles and make the input a regclass. Live and learn. Anyway, to call it:
select * from show_table_rights('item');
That spits out a cross-tab with rolls down the left and rights as columns. I've got one for views too. The difference there is that you're joining against and use pg_views instead of pg_tables. I see that I've got versions for schema and database rights, but rarely ever use those.
Another side note since GRANTs and DEFAULT grants are coming up. I got rid of the public schema pretty early in the piece. I found the default behaviors there...confusing. Plus, thinking about a space where multiple users can share in that way feels really easy to screw up. Later, a privilege escalation CVE that hinged on PUBLIC popped up...so I was glad to have gotten rid of the public schema.
I love Postgres, but don't really grok the permissions system fully. Grants can be set at the database, schema, table, and more...and then on the user (roll) who may inherit from other rolls. With the object hierarchy, the grants are cumulatively restrictive. So, you can be granted SELECT on a table, but that's meaningless unless you have USAGE on the database and access to the schema. On the other hand, inherited roll privileges are additive. So, rights are a restrictive funnel on the database-schema-table side, and an expansive (more permissive) system on the rolls side. If I've got that right, it's a fundamentally confusing design. Hence my code to spit out ~1,000 GRANTs to rebuild everything. And I've got rolls for users and groups (rolls with no log on) and try to put all of the rights into the groups.
Chances are, I'm missing something obvious. My setup has the distinct smell of something where you keep pouring code on top until it stops moving because you don't understand the system well enough to do the simple thing. Granted ;-) I'll circle back to it eventually. Postgres is huge, I'm constantly studying as tasks come to hand, but there are only so many days in the week.
The best piece I remember running into on GRANTs in Postgres is this one:
https://illuminatedcomputing.com/posts/2017/03/postgres-permissions/
Too long an answer for comments...
I've never used row- or column-level security features.There are some incredibly knowledgeable people here who seem to monitor the questions 24/7. I'd be curious if anyone else could comment.
I did look into row-level security for a multi-tenant setup. I remember coming to the conclusion that it is complicated. I figured that I'd use views as they're simple to review, edit, and understand. And you can find them in any SQL database. The row level security features I found...more complicated to understand, more hidden under the hood, and a bit Postgres-specific. With that said it's a super cool idea. As I understand it, you're bolting a policy (filter) onto a base table, and then that rule flows through to any view, etc. automatically. You can't subvert it if you try:
Here are some articles I found useful when looking into this:
https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/
https://www.citusdata.com/blog/2018/04/04/raw-sql-access-with-row-level-security/
https://medium.com/#cazzer/practical-application-of-row-level-security-b33be18fd198
https://info.crunchydata.com/blog/a-postgresql-row-level-security-primer-creating-large-policies
Here's a simple policy to limit users to viewing scans by their department:
create policy filter_scan
on data.scan
using (department_id = user_get_department_id(current_user));
The content of the policy above is the USING clause. It' a WHERE clause, so far as I can see. You can also add an optional CHECK clause on some operations.
Nearly every example I found assumes you're filtering by current_user() and that there is a column to match. The RLS system is based on roles, so this makes sense. And while it makes sense to use the role name, it's not realistic to assume you'll have such a matching column in every table. Or, for that matter, that such a column would even make sense. Examples commonly use chat systems, etc. where the current user is a care part of the data. In my organization's case, that rarely makes sense. (We're tracking physical objects, users are just slow, error-prone peripherals.) Hence the fake stored function call to user_get_department_id. The idea here is that you've got some utility tables to map roles to specific IDs or other attributes for specific tables. Then a function such as user_get_department_id or perhaps user_get_visible_department_ids queries the utility table for the user's allowed ID or IDs and returns them as a list. Slick!
But, like I said, I only ever tested this out on scratch tables and threw it all away. Views seem good enough for the small number of tables, etc. we're dealing with. For folks with multi-tenant setups with 10,000 clients, life would be different. (The Citus folks suggest splitting the tables physically into different databases, as they would.)

Revoke SELECT from inherited role

I am having a slight problem getting permissions to work the way I want them.
I have a role that should generally be allowed to SELECT everywhere, there are a bunch of members to this role. One of them should NOT be allowed to select from a certain table.
I thought this would be possible by granting role membership to the general reader role and revoking SELECT from the restricted table.
It seems the the permissions of the parent role apply and not the specific permissions. Is there a way around this without having to maintain the permissions of the more restricted role or am I applying the role concept in PostgreSQL in a wrong way?
Here's a sample script:
-- as superuser
CREATE DATABASE permission_test;
\c permission_test
CREATE ROLE r_general_select;
CREATE ROLE r_restricted_select IN ROLE r_general_select;
-- set the default permissions
ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT ON TABLES TO "r_general_select";
CREATE TABLE "open"(
id SERIAL,
payload TEXT
);
insert into "open"(payload) values ('test');
-- covered by default privileges
GRANT SELECT ON "open" TO PUBLIC;
-- Tests
-- this is good
SET ROLE r_general_select;
SELECT * FROM "open";
RESET ROLE;
-- this is good
SET ROLE r_restricted_select;
SELECT * FROM "open";
RESET ROLE;
CREATE TABLE "restricted" (
id SERIAL,
payload TEXT
);
insert into "restricted"(payload) values ('test');
-- the role and it's members should be able to read
GRANT SELECT ON "restricted" TO r_general_select;
-- except for this one!
REVOKE SELECT ON "restricted" FROM r_restricted_select;
-- Tests
-- this is good
SET ROLE r_general_select;
SELECT * FROM restricted;
RESET ROLE;
-- this should barf with a permission violation
SET ROLE r_restricted_select;
SELECT * FROM restricted;
RESET ROLE;
--- CLEANUP
DROP OWNED BY "r_restricted_select" CASCADE;
DROP ROLE r_restricted_select ;
DROP OWNED BY "r_general_select" CASCADE;
DROP ROLE r_general_select ;
In PostgreSQL, role permissions are purely additive. There is no way in such a model to revoke from a descendant, inheriting role a permission granted on the inherited one.
To fix this you need to change your permissions approach and base it on permissions that always occur together. I usually do this by looking at functional dependencies and operational dependencies together.

What is a PostgreSQL table owner?

I am unsure about what does a PostgreSQL table owner means. I notice that it changes an attribute of the table itself and not about the owner because it is specified through an
ALTER TABLE table_name OWNER TO role_name;
You can see who is owner in certain table:
select * from pg_tables where tablename = 'my_tbl';
or you can see all tables by certain owner:
select * from pg_tables where tableowner = 'username';
The owner is (if nothing else happened) the user (role) that created the table. So if user arthur runs CREATE TABLE foo (id INTEGER), arthur owns the table.
The owner of a table has all privileges on it - including the privilege to drop it. Or the privilege to grant other users (roles) access to the table.
The SQL script generated by pg_dump typically includes the ALTER TABLE ... OWNER TO ... statement as those scripts are intended to be run by the DBA and in that case all tables would be owned by the DBA - which means the "real" owner could not change or access the tables.
Some excerpts from the official docs:
When an object is created, it is assigned an owner. The owner is normally the role that executed the creation statement. For most kinds of objects, the initial state is that only the owner (or a superuser) can do anything with the object. To allow other roles to use it, privileges must be granted.
The right to modify or destroy an object is inherent in being the object's owner, and cannot be granted or revoked in itself. (However, like all privileges, that right can be inherited by members of the owning role; see Section 21.3.)
Ordinarily, only the object's owner (or a superuser) can grant or revoke privileges on an object.
An object's owner can choose to revoke their own ordinary privileges, for example to make a table read-only for themselves as well as others. But owners are always treated as holding all grant options, so they can always re-grant their own privileges.