简体   繁体   中英

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.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM