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

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!

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.

truncate all tables in Postgres except for the ones provided in a list

I want to truncate the whole database while maintaining the sequence identity. I came up with something like this:
WITH tables_to_be_truncated AS (
SELECT table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND table_schema='public'
AND table_name NOT IN ('admins', 'admin_roles')
)
TRUNCATE TABLE (SELECT table_name FROM tables_to_be_truncated) CONTINUE IDENTITY RESTRICT;
I get this error:
ERROR: syntax error at or near "TRUNCATE"
LINE 9: TRUNCATE TABLE (SELECT table_name FROM tables_to_be_truncated...
I do have the permissions to truncate the tables and when I run for a single table like TRUNCATE TABLE access_tokens it works fine.
I also tried with this
TRUNCATE TABLE (SELECT string_agg(table_name, ', ') FROM tables_to_be_truncated) CONTINUE IDENTITY RESTRICT
which didn't work as well.
From what I see in other posts, people are doing it with functions. I didn't want to go down this path honestly but if this is the only way...
You don't need a function for that. An anonymous code block will do:
DO $$
DECLARE row RECORD;
BEGIN
FOR row IN SELECT table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND table_schema='public'
AND table_name NOT IN ('admins', 'admin_roles')
LOOP
EXECUTE format('TRUNCATE TABLE %I CONTINUE IDENTITY RESTRICT;',row.table_name);
END LOOP;
END;
$$;
Other than that I don't think you'll be able to run dynamic queries with pure SQL.
Demo: db<>fiddle

Postgres: GRANT to user based on sub-query

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.

Run Query on All Schemas Postgres

We have a around 100+ schema maintained in PostgreSQL. Now we want to query on all schema, is there any way to do that?
other than views, procedures and union all?
Any postgres functions which let you query on multiple schemas
The following catalog query will produce valid queries for every table on all schemas of your database. You can copy this to a valid SQL file.
SELECT 'SELECT * FROM ' || table_schema || '.' || table_name || ';' AS query
FROM information_schema.tables
WHERE table_schema IN
(
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name NOT LIKE 'pg_%' AND schema_name != 'information_schema'
);
Does this help?

Phantom Postgres table exists but can't be dropped?

I seem to have some sort of phantom table in Postgres.
Suppose I do the following:
select * from information_schema.tables where table_schema = 'public';
I get:
table_name | table_type | ...
phantom_table BASE TABLE
...
So, I run:
drop table phantom_table cascade;
And I get:
ERROR: table "phantom_table" does not exist
Things I've tried:
Checking for spelling errors and making sure the schema is correct (I've even copied/pasted table name out of information schema query results).
vacuum
Reconnecting.
Killing other running processes from my user (nobody else is using the DB).
Checking for active locks on the table (there aren't any).
Anybody have any other ideas for things I should try?
You probably have some white space at the end of the name.
The easiest way is to let the format() function generate you the correct table name and statement:
select format('drop table %I.%I;', table_schema, table_name) as drop_statement
from information_schema.tables
where table_schema = 'public'
and table_name like '%phantom%';
Edit: it seems that psql on Windows isn't able to handle an identifier with a new line in a drop statement (it does when creating the table however).
To workaround that, you can use a DO block:
do
$$
declare
l_stmt text;
begin
select format('drop table %I.%I;', table_schema, table_name) as drop_statement
into l_stmt
from information_schema.tables
where table_schema = 'public'
and table_name like '%phantom%';
execute l_stmt;
end;
$$
;
Note this code assumes that only a single table with that name exists.