Postgres: GRANT to user based on sub-query - postgresql

This is a symptom of database and user names being different between my dev/staging/live environments, but is there a way to GRANT permissions to a user, determined by some kind of sub-query?
Something like this (not valid syntax):
GRANT UPDATE (my_column) ON my_table TO (SELECT CASE current_database()
WHEN 'account-dev' THEN 'c-app'
WHEN 'account-staging' THEN 'x-app'
WHEN 'account-live' THEN 'a-app'
END);

Use psql and its wonderful \gexec:
SELECT format(
'GRANT UPDATE (my_column) ON my_table TO %I;',
CASE current_database()
WHEN 'account-dev' THEN 'c-app'
WHEN 'account-staging' THEN 'x-app'
WHEN 'account-live' THEN 'a-app'
END
) \gexec
Alternatively, you can write a DO statement that uses EXECUTE to execute a dynamic statement constructed as above.

Related

GRANT statements with bound parameters

I'm using a client library that only accepts SQL strings that are compile-time constant, in order to prevent SQL injection attacks. And I wanted to execute some GRANT statements for a set of tables and a user.
I tried
GRANT SELECT ON $1 TO $2
and passing the table and user names as bound parameters. But that fails with
syntax error at or near "$1"
Not being able to pass in a tablename as a bound parameter is understandable (you can't use SELECT columns FROM $1 for instance), and with a bit of work, I can make the tablenames compile-time constants. But changing the command to
GRANT SELECT ON MyTable to $1
and passing just the username as a bound parameter also fails. Which is more of an issue: whereas the tablenames can be hard-coded with a bit of work, the username is only known at runtime.
Is there a way to pass the username as a bound parameter, or do I need to bypass my client library in order to GRANT permissions to a run-time-defined username?
CREATE OR REPLACE FUNCTION test_grant (_role text)
RETURNS void
AS $$
DECLARE
_sql text := '';
BEGIN
_sql := 'GRANT SELECT ON a to ' || quote_ident(_role) || ' GRANTED BY current_user ';
RAISE NOTICE '%', _sql;
EXECUTE _sql;
RAISE NOTICE '% granted table a to %', CURRENT_USER, _role;
END
$$
LANGUAGE plpgsql
STRICT.
You can also make the table as function input argument. quote_ident is used for identifiers quoting. In GRANT SELECT ON MyTable to $1 you hope to make sure $1 is a identifiers rather than some string. Because if $1 string then the whole command can be:
GRANT SELECT ON MyTable to public;
GRANT SELECT ON MyTable to role_a WITH GRANT OPTION;
So the above function can solve these problem.
The only statements that can use parameters are INSERT, UPDATE, DELETE and SELECT. GRANT cannot use parameters; you will have to build a statement dynamically.

Postgres run statement against multiple schemas

I have a mult-tenant application where tenants are set up on different schemas within the same database. The reason being that they have some shared data they use on one of the schemas.
Up until now I've been using a bash script with a list of the schemas in it that needs to be updated whenever a new schema is added and I need to do things like table schema changes across the accounts. For instance adding a new column to a table.
Is there a way in Postgres, psql, etc... to run for instance
ALTER TABLE some_table ADD COLUMN some_column TEXT NOT NULL DEFAULT '';
without having to do string replacement in another script like bash for instance.
So in other words is there an easy enough way to get the schemas, and write in psql a for loop that will iterate through the schemas and run the statement each by setting search_path for instance.
The reason being that the number of tenants is growing, and new tenants can be added by admin users that aren't devs, so I'm constantly updating my shell scripts. This will only grow exponentially. Is there a standard way of handling this kind of problem?
You can do that with a little PL/pgSQL block:
do
$$
declare
s_rec record;
begin
for s_rec in select schema_name
from information_schema.schemata
where schema_name not in ('pg_catalog', 'information_schema')
loop
execute format ('ALTER TABLE if exists %I.some_table ADD COLUMN some_column TEXT NOT NULL DEFAULT ''''), s_rec.schema_name);
end loop;
end;
$$
The if exists will make sure the statement doesn't fail if that table doesn't exist in the schema.
If you over-simplified your question and want in fact run complete scripts once for each schema, generating a script for each schema that includes the actual script is probably easier:
select concat(format('set search_path = %I;', schema_name),
chr(10),
'\i complete_migration_script.sql')
from information_schema.schemata
where schema_name not in ('pg_catalog', 'information_schema')
You can spool the output of that statement into a file and then run that file using psql (of course you need to replace complete_migration_script.sql with the actual name of your script)

How can a set PostgreSQL schema on the fly using Doctrine and Symfony?

I'm trying to create a multi tenent app using Symfony 2.6 and PostgreSQL schemas (namespaces). I would like to know how can I change some entity schema on the pre persist event?
I know that it's possible to set the schema as annotation #Table(schema="schema") but this is static solution I need something more dynamic!
The purpose using PostgreSQL is take advantage of schemas feature like:
CREATE TABLE tenant_1.users (
# table schema
);
CREATE TABLE tenant_2.users (
# table schema
);
So, if I want only users from tenant_2 my query will be something like SELECT * FROM tenant_2.users;
This way my data will be separated and I will have only one database to connect and maintain.
$schema = sprintf('tenant_%d', $id);
$em->getConnection()->exec('SET search_path TO ' . $schema);
You might also want to involve PostgreSQL's row level security instead - that way you can actually prevent the tenant from accessing the data, not just hiding it by prefixing a schema path.
Check this one out: https://www.tangramvision.com/blog/hands-on-with-postgresql-authorization-part-2-row-level-security. I just set a working tenant separation with the information on that page and I'm quite excited about it.
In my case, my tenants are called organisations, and some (not all) tables have an organisation_id that permanently binds a row to it.
Here is a version of my script I run during a schema update, which finds all tables with column organisation_id and enables the row level security with a policy that only shows rows that an org owns, if the org role is set:
CREATE ROLE "org";
-- Find all tables with organisation_id and enable the row level security
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (
SELECT
t.table_name, t.table_schema, c.column_name
FROM
information_schema.tables t
INNER JOIN
information_schema.columns c ON
c.table_name = t.table_name
AND c.table_schema = t.table_schema
AND c.column_name = 'organisation_id'
WHERE
t.table_type = 'BASE TABLE'
AND t.table_schema != 'information_schema'
AND t.table_schema NOT LIKE 'pg_%'
) LOOP
EXECUTE 'ALTER TABLE ' || quote_ident(r.table_schema) || '.' || quote_ident(r.table_name) || ' ENABLE ROW LEVEL SECURITY';
EXECUTE 'DROP POLICY IF EXISTS org_separation ON ' || quote_ident(r.table_schema) || '.' || quote_ident(r.table_name);
EXECUTE 'CREATE POLICY org_separation ON ' || quote_ident(r.table_schema) || '.' || quote_ident(r.table_name) || 'FOR ALL to org USING (organisation_id = substr(current_user, 5)::int)';
END LOOP;
END $$;
-- Grant usage on all tables in all schemas to the org role
DO $do$
DECLARE
sch text;
BEGIN
FOR sch IN (
SELECT
schema_name
FROM
information_schema.schemata
WHERE
schema_name != 'information_schema'
AND schema_name NOT LIKE 'pg_%'
) LOOP
EXECUTE format($$ GRANT USAGE ON SCHEMA %I TO org $$, sch);
EXECUTE format($$ GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA %I TO org $$, sch);
EXECUTE format($$ GRANT SELECT, UPDATE, INSERT, DELETE ON ALL TABLES IN SCHEMA %I TO org $$, sch);
EXECUTE format($$ ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT SELECT, UPDATE ON SEQUENCES TO org $$, sch);
EXECUTE format($$ ALTER DEFAULT PRIVILEGES IN SCHEMA %I GRANT INSERT, SELECT, UPDATE, DELETE ON TABLES TO org $$, sch);
END LOOP;
END;
$do$;
Step two, when I create a new organisation, I also create a role for it:
CREATE ROLE "org:86" LOGIN;
GRANT org TO "org:86";
Step three, at the beginning of every request that should be scoped to a particular organisation, I call SET ROLE "org:86"; to enable the restrictions.
There is much more happening around what we do with all of this, but the code above should be complete enough to help people get started.
Good luck!

How to revoke all group roles from login role

How to revoke all group roles from login role? Is there a way how to do this automatically?
Since you can GRANT / REVOKE several roles at once, a single DO command with dynamic SQL would be simpler / faster (set-based operations are regularly faster in RDBMS than looping):
DO
$do$
DECLARE
_role regrole := 'my_role'; -- provide valid role name here
_memberships text := (
SELECT string_agg(m.roleid::regrole::text, ', ')
FROM pg_auth_members m
WHERE m.member = _role
);
BEGIN
IF _memberships IS NULL THEN
RAISE NOTICE 'No group memberships found for role %.', _role;
ELSE
RAISE NOTICE '%',
-- EXECUTE
format('REVOKE %s FROM %s', _memberships, _role);
END IF;
END
$do$;
The code is in debug mode. Comment RAISE NOTICE '%', and un-comment EXECUTE to prime the bomb.
DO and string_agg() require Postgres 9.0 or later.
The object identifier type regrole was added with Postgres 9.5
Casting to regrole verifies role names on input and double-quotes where necessary when outputting text - so no SQL-injection possible.
Effectively executes a command like:
REVOKE role_a, role_b FROM my_user;
Doesn't break with maliciously formed role names:
REVOKE role_a, role_b, "FROM postgres; DELETE * FROM usr; --" FROM my_user;
Note the double quotes around the trick-name.
Raises a notice if no role memberships are found.
This revokes all memberships in other roles. It's all just roles to Postgres, some have the LOGIN privilege ("user roles"), others don't ("group roles").
Think need to query all the roles
select usename, rolname
from pg_user
join pg_auth_members on (pg_user.usesysid=pg_auth_members.member)
join pg_roles on (pg_roles.oid=pg_auth_members.roleid)
and LOOP through the result to REVOKE rolname FROM usename;

mysqli cannot call stored procedure

Procedure
delimiter $$
drop procedure if exists db1.test;
create procedure db1.test()
deterministic
begin
select * from table1;
end$$
delimiter ;
php code:
$conn = new mysqli('localhost','username','passwd','db1');
$query1 = 'select * from table1';
$query2 = 'call test()';
Then $conn->query($query1) works while $conn->query($query2) returns bool(false).
But in mysql, both query1 and query2 work.
What did I miss here? Thanks!
Okay, if it's not the syntax, it could be permissions. Did you grant execute privileges to the user for this database?
Edit:
Here's the SQL to do this:
GRANT EXECUTE ON `db1` . * TO 'user'#'localhost';
(Even if a user has all the permissions required to do the SQL inside the stored procedure as individual queries, you'd still need the EXECUTE privilege to actually call the procedure.)
Are you sure the procedure is being created? Both MySQL Workbench and phpMyAdmin are telling me there's a syntax error in select * from table; --- probably because table is a reserved word? This worked in MySQL Workbench:
USE `db1`;
DROP procedure IF EXISTS `test`;
DELIMITER $$
USE `db1`$$
CREATE PROCEDURE `db1`.`test` ()
BEGIN
select * from `table`;
END
$$
DELIMITER ;
Note the addition of backticks to table. With that change on my system, $query2 succeeds but $query1 fails (with or without the change to the procedure of course)