Protect column/key in SELECT call - postgresql

Assuming I have a (over simplified, non secure) table that looks like:
CREATE TABLE users (id SERIAL PRIMARY KEY, user VARCHAR(25), _password VARCHAR(25), email VARCHAR(80));
I want to add a additional failsafe on the column _password that prevents it from being returned on a SELECT * FROM users call, is this possible in PostgreSQL and if so, how?
I tried some versions of https://stackoverflow.com/a/7250991/929999, but this probably isn't what I was looking for. But that got me thinking that there might be a constraint that could be created. I can't find anyone who's tried this or asked it before, so I'm kind of lost seeing as I'm not a database expert by any means.
So for now I dump all results from the database into a custom dictionary placeholder in Python with a function called .safe_dump() that removes any keys starting with _<key>.
And I guess I could create a separate table containing a list of sensitive keys and match those on every SELECT statement via a JOIN or similar, but that would just move the risk of accidentally retrieving a sensitive key from the SELECT call to keeping that "JOIN table" updated.
Is there a flag in PostgreSQL that can filter out of block calls trying to access a key while still allowing it to be used on WHERE x=y clauses?

You can deny permission for that column:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
"user" VARCHAR(25),
_password VARCHAR(25),
email VARCHAR(80)
);
REVOKE ALL ON users FROM laurenz;
GRANT SELECT (id, "user", email) ON users TO public;
test=> SELECT * FROM users;
ERROR: permission denied for relation users
test=> SELECT id, "user", email FROM users;
id | user | email
----+------+-------
(0 rows)
If you'd rather want exclude the column from the output, use a view:
CREATE VIEW users_v AS SELECT id, "user", email FROM users;
GRANT SELECT ON users_v TO PUBLIC;

Related

Trigger and function to insert user id into another table

I am using Prisma as my schema and migrating it to supabase with prisma migrate dev
One of my tables Profiles, should reference the auth.users table in supabase, in sql something like this id uuid references auth.users not null,
Now since that table is automatically created in supabase do I still add it to my prisma schema? It's not in public either it is in auth.
model Profiles {
id String #id #db.Uuid
role String
subId String
stripeCustomerId String
refundId String[]
createdAt DateTime #default(now())
updatedAt DateTime #updatedAt
}
The reason I want the relation is because I want a trigger to automatically run a function that inserts an id and role into the profiles table when a new users is invited.
This is that trigger and function
-- inserts a row into public.profiles
create function public.handle_new_user()
returns trigger
language plpgsql
security definer
as $$
begin
insert into public.Profiles (id, role)
values (new.id, 'BASE_USER');
return new;
end;
$$;
-- trigger the function every time a user is created
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
I had this working when I created the profiles table manually in supabase I included the reference to the auth.users, that's the only reason I can think of why the user Id and role won't insert into the profiles db when I invite a user, the trigger and function are failing
create table public.Profiles (
id uuid references auth.users not null,
role text,
primary key (id)
);
Update from comment:
One error I found is
relation "public.profiles" does not exist
I change it to "public.Profiles" with a capital in supabase, but the function seem to still be looking for lowercase.
What you show should just work:
db<>fiddle here
Looks like you messed up capitalization with Postgres identifiers.
If you (or your ORM) created the table as "Profiles" (with double-quotes), non-standard capitalization is preserved and you need to double-quote the name for the rest of its life.
So the trigger function body must read:
...
insert into public."Profiles" (id, role) -- with double-quotes
...
Note that schema and table (and column) have to be quoted separately.
See:
Are PostgreSQL column names case-sensitive?

Best practice to handle column level select grants in PostGraphile

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.

Postgresql Row Level Security Checking new data in WITH CHECK in Policy for INSERT

I've a Database with several tables.
A user has several objects and an object has several parts.
I want to write a policy that only the creator of the object is allowed to add parts to the object. Therefore I need to get the object a to be inserted part belongs to, but I've no idea how to check the data.
Is there a way to get the data to be inserted in the policy?
Thanks for your effort.
Here is an example how to implement something like that with row level security. Adapt it to your need!
CREATE TABLE object(
id integer PRIMARY KEY,
name text NOT NULL,
owner name NOT NULL DEFAULT current_user
);
CREATE TABLE part(
id integer PRIMARY KEY,
parent_id integer NOT NULL REFERENCES object(id),
name text NOT NULL
);
We have to give people some permissions:
GRANT SELECT, INSERT ON object TO PUBLIC;
GRANT SELECT, INSERT ON part TO PUBLIC;
Now we enable row level security and allow only INSERTs in part when the owner in object matches:
ALTER TABLE part ENABLE ROW LEVEL SECURITY;
CREATE POLICY insert_only_owned ON part
FOR INSERT TO PUBLIC
WITH CHECK (EXISTS(
SELECT 1
FROM object o
WHERE o.id = parent_id
AND owner = current_user
));

Prevent updates to generated primary key column

In PostgreSQL I have created a table and with an id column defined as serial. I have inserted two rows, but I can still update the value of the id column.
But I need prevent updates to the generated value of the id column.
create table aas.apa_testtable
(
id serial primary key,
name text
)
insert into aas.apa_testtable(name) select ('test')
insert into aas.apa_testtable(name) select ('test2')
-- I want this to be impossible / result in an error:
update aas.apa_testtable set id=3 where id=2
You can revoke update on table and grant it on column(s):
REVOKE UPDATE ON TABLE aas.apa_testtable FROM some_role;
GRANT UPDATE (name) ON TABLE aas.apa_testtable TO some_role;
Remember about role public, superusers and other inheritance issues you might have in your setup.
--Do not try this, it will not work without revoking table level privileges:
REVOKE UPDATE (id) ON TABLE aas.apa_testtable FROM some_role;
Alternative is to create trigger that will check if old != new, but with details provided I don't see need for it.

removing data which are not refenceing to other table POSTGRES

I have one table USER and some other tables like USER_DETAILS ,USER_QUALIFICATION etc USER_ID references to all such table i want to remove those USER_ID which are not present in any other tables.
Deleting all of the users that are not present in a connected table:
DELETE FROM table WHERE user_id NOT IN (SELECT user_id FROM other_table)
If you want to delete only users that are not found in any table than you can add
AND NOT IN (SELECT user_id FROM another_table)
Alternatively you can create a tmp table and merge in all the user_ids that you want to keep and use that table in the sub-select for the NOT IN.
Use a DELETE with a not exists condition for all related tables:
delete from "USER" u
where not exists (select *
from user_details ud
where ud.user_id = u.user_id)
and not exists (select *
from user_qualification uq
where uq.user_id = u.user_id);
Note that user is a reserved word, and thus needs to be quoted to be usable as a table name. But quoting makes it case-sensitive. So "USER" and "user" are two different table names. As you have not included the DDL for your tables I cannot tell if your table is named "USER" or "user".
In general I would strongly recommend to avoid using double quotes for identifies completely.