postgresqlpermissionsrow-level-securitypostgrestpostgresql-14

column-level security with policies in PostgreSQL 14 / PostgREST?


I'm using PostgREST as an API for a project and now implement the security functions.

One thing I'd like to have but that is covered neither by row- nor by column-level security as I've read them so far is this use case:

From everything I've read so far, neither row-level nor column-level security cover this, but I'm sure Postgres can handle it. How?

(Note: I know how to understand who the current user is and which row belongs to him, that is not my question)

Clarification: My users are not database users, but users in a user table. I have DB user roles for users, editors, admin, etc. but I don't create a DB role for every user signing up to the service. I can distinguish them for row-level security via JWT and

current_setting('request.jwt.claims', true)::json->>'user_id'

That's why I'm not trying to solve this with the obvious solution of using views.


Solution

  • -- Normal user
    CREATE TABLE users (
        user_name text not null,
        user_id text primary key,
        phone text,
        address text,
        users_field1 text,
        users_field2 text,
        users_field3 text,
        users_field4 text,
        created_by text DEFAULT CURRENT_USER,
        created_at timestamptz DEFAULT now()
    );
    
    
    INSERT INTO users (user_name, phone, address, user_id)
    VALUES ('bob', 'misc', 'misc_add','unique1'),
    ('alice', 'misc1', 'misc_add1','unique2'),
    ('alice1', 'misc1', 'misc_add1','unique3'),
    ('alice2', 'misc1', 'misc_add1','unique4');
    
    CREATE ROLE common_users;
    CREATE OR REPLACE FUNCTION regress_rls_schema.current_userid()
     RETURNS text
     LANGUAGE plpgsql
     STABLE
    AS $function$
    BEGIN
      RETURN current_setting('regress_rls_schema.current_userid');
    EXCEPTION
      WHEN undefined_object THEN
        RETURN NULL;
    END;
    $function$;
    
    
    --two interface. one common interface, everyone can see it.
    CREATE VIEW common_view WITH ( security_barrier = TRUE
    ) AS
    SELECT
        user_name,
        users_field1,
        users_field2,
        users_field3
    FROM
        users;
    
    CREATE OR REPLACE VIEW special_view WITH ( security_barrier = TRUE
    ) AS
    SELECT
        user_name,
        user_id,
        phone,
        users_field3,
        users_field1,
        users_field2
    FROM
        users
    WHERE
        user_id = current_userid();
    
    GRANT SELECT ON common_view TO public;
    GRANT SELECT, INSERT, UPDATE, DELETE ON users TO admin_user;
    GRANT SELECT, DELETE, UPDATE ON special_view TO common_users;
    
    set role  common_users;
    set session  regress_rls_schema.current_userid = 'unique1';
    select current_userid();
    
    TABLE common_view; --ok
    TABLE special_view; --ok
    table users; --permission denied. 
    
    
     -- permission denied for table users
    UPDATE
        users
    SET
        created_by = 'dummy'
    RETURNING
        *;
    
    UPDATE
        special_view
    SET
        users_field3 = 'hi there'
    RETURNING
        *; --ok
    
    UPDATE
        special_view
    SET
        phone = '1234'
    RETURNING
        *;--ok.
    
    set session  regress_rls_schema.current_userid = 'unique2';
    TABLE special_view;--ok
    
    UPDATE
        special_view
    SET
        phone = '911246',
        users_field2 = 'test view',
        users_field1 = ' this is a test'
    RETURNING
        *;
    
    -- permission denied for table users
    UPDATE
        users
    SET
        phone = '9012890'
    RETURNING
        *;