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