pg_rls 0.2.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +55 -17
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -2
- data/CODE_OF_CONDUCT.md +77 -29
- data/Guardfile +44 -0
- data/README.md +247 -83
- data/Rakefile +5 -12
- data/Steepfile +29 -0
- data/UPGRADE.md +106 -0
- data/app/models/pg_rls/admin.rb +24 -0
- data/app/models/pg_rls/current.rb +48 -0
- data/app/models/pg_rls/record.rb +13 -0
- data/app/models/pg_rls/tenant/searchable.rb +60 -0
- data/app/models/pg_rls/tenant/securable.rb +67 -0
- data/app/models/pg_rls/tenant/switchable.rb +40 -0
- data/app/models/pg_rls/tenant.rb +9 -0
- data/assets/logo.svg +8 -0
- data/docker-compose.yml +14 -0
- data/lib/generators/pg_rls/active_record/active_record_generator.rb +62 -65
- data/lib/generators/pg_rls/install/install_generator.rb +38 -0
- data/lib/generators/pg_rls/pg_rls_generator.rb +2 -1
- data/lib/generators/pg_rls/templates/USAGE +28 -0
- data/lib/generators/pg_rls/{active_record/templates → templates/app/models}/abstract_base_class.rb.tt +1 -3
- data/lib/generators/pg_rls/{active_record/templates → templates/app/models}/model.rb.tt +0 -2
- data/lib/generators/pg_rls/templates/config/initializers/pg_rls.rb.tt +58 -0
- data/lib/generators/pg_rls/templates/db/migrate/backport_pg_rls_table.rb.tt +14 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_tenant_table.rb.tt +5 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_table.rb.tt +29 -0
- data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_tenant_table.rb.tt +29 -0
- data/lib/pg_rls/active_record/connection_adapters/connection_pool.rb +31 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rb +207 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/errors.rb +17 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rb +167 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rb +91 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rb +56 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rb +95 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rb +127 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rb +71 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb +120 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rb +30 -0
- data/lib/pg_rls/active_record/connection_adapters/postgre_sql.rb +36 -0
- data/lib/pg_rls/active_record/connection_adapters.rb +12 -0
- data/lib/pg_rls/active_record/database_shards.rb +74 -0
- data/lib/pg_rls/active_record/migration/command_recorder.rb +28 -0
- data/lib/pg_rls/active_record/migration.rb +11 -0
- data/lib/pg_rls/active_record/test_databases.rb +19 -0
- data/lib/pg_rls/active_record.rb +11 -0
- data/lib/pg_rls/active_support/string_ext.rb +17 -0
- data/lib/pg_rls/active_support.rb +9 -0
- data/lib/pg_rls/connection_config.rb +61 -0
- data/lib/pg_rls/deprecation.rb +14 -0
- data/lib/pg_rls/engine.rb +8 -0
- data/lib/pg_rls/error.rb +10 -0
- data/lib/pg_rls/generators/.keep +0 -0
- data/lib/pg_rls/railtie.rb +1 -11
- data/lib/pg_rls/tasks/.keep +0 -0
- data/lib/pg_rls/version.rb +3 -1
- data/lib/pg_rls.rb +67 -151
- data/rbs_collection.lock.yaml +132 -0
- data/rbs_collection.yaml +127 -0
- data/review_code.sh +33 -0
- data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
- data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
- data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
- data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
- data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
- data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
- data/sig/pg_rls/active_record/database_shards.rbs +34 -0
- data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
- data/sig/pg_rls/active_record/migration.rbs +8 -0
- data/sig/pg_rls/active_record.rbs +7 -0
- data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
- data/sig/pg_rls/active_support/string_ext.rbs +27 -0
- data/sig/pg_rls/active_support.rbs +7 -0
- data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
- data/sig/pg_rls/connection_config.rbs +16 -0
- data/sig/pg_rls/deprecation.rbs +9 -0
- data/sig/pg_rls/engine.rbs +7 -0
- data/sig/pg_rls/errors.rbs +14 -0
- data/sig/pg_rls/railtie.rbs +6 -0
- data/sig/pg_rls/tenant_test_helper.rbs +14 -0
- data/sig/pg_rls.rbs +60 -0
- data/sig/support/active_record.rbs +86 -0
- data/sig/support/active_support.rbs +7 -0
- data/sig/support/fowardable.rbs +2 -0
- data/sig/support/pg.rbs +12 -0
- data/sig/support/rails.rbs +38 -0
- data/start.sh +30 -0
- metadata +167 -48
- data/.rspec +0 -3
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -300
- data/LICENSE.txt +0 -21
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/lib/generators/pg_rls/active_record/templates/convert_migration.rb.tt +0 -11
- data/lib/generators/pg_rls/active_record/templates/convert_migration_backport.rb.tt +0 -12
- data/lib/generators/pg_rls/active_record/templates/init_convert_migration.rb.tt +0 -11
- data/lib/generators/pg_rls/active_record/templates/init_migration.rb.tt +0 -25
- data/lib/generators/pg_rls/active_record/templates/init_model.rb.tt +0 -24
- data/lib/generators/pg_rls/active_record/templates/migration.rb.tt +0 -17
- data/lib/generators/pg_rls/base.rb +0 -36
- data/lib/generators/pg_rls/install_generator.rb +0 -90
- data/lib/generators/pg_rls.rb +0 -19
- data/lib/generators/templates/README +0 -22
- data/lib/generators/templates/pg_rls.rb.tt +0 -48
- data/lib/pg_rls/Rakefile +0 -7
- data/lib/pg_rls/current/context.rb +0 -10
- data/lib/pg_rls/database/admin_statements.rb +0 -28
- data/lib/pg_rls/database/configurations.rb +0 -46
- data/lib/pg_rls/database/prepared.rb +0 -40
- data/lib/pg_rls/database/tasks/admin_database.rake +0 -40
- data/lib/pg_rls/errors/index.rb +0 -4
- data/lib/pg_rls/errors/rake_only_error.rb +0 -12
- data/lib/pg_rls/errors/tenant_not_found.rb +0 -13
- data/lib/pg_rls/logger.rb +0 -31
- data/lib/pg_rls/middleware/set_reset_connection.rb +0 -93
- data/lib/pg_rls/middleware/sidekiq/client.rb +0 -22
- data/lib/pg_rls/middleware/sidekiq/server.rb +0 -19
- data/lib/pg_rls/middleware/sidekiq.rb +0 -11
- data/lib/pg_rls/middleware.rb +0 -8
- data/lib/pg_rls/multi_tenancy.rb +0 -32
- data/lib/pg_rls/schema/down_statements.rb +0 -54
- data/lib/pg_rls/schema/dumper.rb +0 -36
- data/lib/pg_rls/schema/statements.rb +0 -72
- data/lib/pg_rls/schema/up_statements.rb +0 -104
- data/lib/pg_rls/tenant.rb +0 -153
@@ -0,0 +1,207 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
# This module contains the logic to validate user privileges
|
8
|
+
module CheckRlsUserPrivileges
|
9
|
+
include SqlHelperMethod
|
10
|
+
|
11
|
+
def check_rls_user_privileges!(role_name, schema = PgRls.schema, rls_role_group = PgRls.rls_role_group)
|
12
|
+
check_user_exists!(role_name) && check_user_in_rls_group!(role_name) &&
|
13
|
+
check_schema_usage_privilege!(rls_role_group, schema) &&
|
14
|
+
check_default_table_privileges!(rls_role_group,
|
15
|
+
schema) && check_default_sequence_privileges!(rls_role_group, schema)
|
16
|
+
end
|
17
|
+
|
18
|
+
def check_table_privileges!(role_name, schema, table_name)
|
19
|
+
execute_sql!(check_table_privileges_sql(role_name, schema, table_name))
|
20
|
+
true
|
21
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
22
|
+
raise UserMissingTablePrivilegesError, e.message
|
23
|
+
end
|
24
|
+
|
25
|
+
def check_sequence_privileges!(role_name, schema, sequence_name)
|
26
|
+
execute_sql!(check_sequence_privileges_sql(role_name, schema, sequence_name))
|
27
|
+
true
|
28
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
29
|
+
raise UserMissingSequencePrivilegesError, e.message
|
30
|
+
end
|
31
|
+
|
32
|
+
def check_table_rls_enabled!(table_name, schema = PgRls.schema)
|
33
|
+
execute_sql!(check_table_rls_enabled_sql(schema, table_name))
|
34
|
+
true
|
35
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
36
|
+
raise TableRlsNotEnabledError, e.message
|
37
|
+
end
|
38
|
+
|
39
|
+
def check_table_user_policy_exists!(table_name, user, schema = PgRls.schema)
|
40
|
+
execute_sql!(check_table_user_policy_exists_sql(schema, table_name, user))
|
41
|
+
true
|
42
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
43
|
+
raise TableUserPolicyDoesNotExistError, e.message
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def check_user_exists!(role_name)
|
49
|
+
execute_sql!(check_user_exists_sql(role_name))
|
50
|
+
true
|
51
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
52
|
+
raise UserDoesNotExistError, e.message
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_user_in_rls_group!(role_name)
|
56
|
+
execute_sql!(check_user_in_rls_group_sql(role_name))
|
57
|
+
true
|
58
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
59
|
+
raise UserNotInPgRlsGroupError, e.message
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_schema_usage_privilege!(role_name, schema)
|
63
|
+
execute_sql!(check_schema_usage_privilege_sql(role_name, schema))
|
64
|
+
true
|
65
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
66
|
+
raise UserMissingSchemaUsagePrivilegeError, e.message
|
67
|
+
end
|
68
|
+
|
69
|
+
def check_default_table_privileges!(role_name, schema)
|
70
|
+
execute_sql!(check_default_table_privileges_sql(role_name, schema))
|
71
|
+
true
|
72
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
73
|
+
raise UserMissingTablePrivilegesError, e.message
|
74
|
+
end
|
75
|
+
|
76
|
+
def check_default_sequence_privileges!(role_name, schema)
|
77
|
+
execute_sql!(check_default_sequence_privileges_sql(role_name, schema))
|
78
|
+
true
|
79
|
+
rescue ::ActiveRecord::StatementInvalid => e
|
80
|
+
raise UserMissingSequencePrivilegesError, e.message
|
81
|
+
end
|
82
|
+
|
83
|
+
def check_user_exists_sql(role_name)
|
84
|
+
<<~SQL
|
85
|
+
DO $$ BEGIN
|
86
|
+
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{role_name}') THEN
|
87
|
+
RAISE EXCEPTION 'User % does not exist', '#{role_name}';
|
88
|
+
END IF;
|
89
|
+
END $$;
|
90
|
+
SQL
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_user_in_rls_group_sql(role_name)
|
94
|
+
<<~SQL
|
95
|
+
DO $$ BEGIN
|
96
|
+
IF NOT EXISTS (
|
97
|
+
SELECT FROM pg_catalog.pg_auth_members
|
98
|
+
JOIN pg_roles AS rls_group ON rls_group.oid = pg_auth_members.roleid
|
99
|
+
JOIN pg_roles AS rls_member ON rls_member.oid = pg_auth_members.member
|
100
|
+
WHERE rls_group.rolname = 'rls_group' AND rls_member.rolname = '#{role_name}'
|
101
|
+
) THEN
|
102
|
+
RAISE EXCEPTION 'User % is not a member of pg_rls', '#{role_name}';
|
103
|
+
END IF;
|
104
|
+
END $$;
|
105
|
+
SQL
|
106
|
+
end
|
107
|
+
|
108
|
+
# defacl.defaclobjtype = 'r' because r stands for relation (table, view), S stands for sequence
|
109
|
+
# if needed more info visit https://www.postgresql.org/docs/9.6/catalog-pg-default-acl.html
|
110
|
+
def check_schema_usage_privilege_sql(role_name, schema)
|
111
|
+
<<~SQL
|
112
|
+
DO $$
|
113
|
+
BEGIN
|
114
|
+
IF NOT EXISTS (
|
115
|
+
SELECT FROM pg_namespace n
|
116
|
+
LEFT JOIN LATERAL aclexplode(n.nspacl) acl ON true LEFT JOIN pg_roles grantee_roles ON acl.grantee = grantee_roles.oid
|
117
|
+
WHERE acl.privilege_type = 'USAGE' AND n.nspname = '#{schema}' AND grantee_roles.rolname = '#{role_name}'
|
118
|
+
) THEN
|
119
|
+
RAISE EXCEPTION 'User % is missing USAGE privilege on schema %', '#{role_name}', '#{schema}';
|
120
|
+
END IF;
|
121
|
+
END $$;
|
122
|
+
SQL
|
123
|
+
end
|
124
|
+
|
125
|
+
def check_default_table_privileges_sql(role_name, schema)
|
126
|
+
<<~SQL
|
127
|
+
DO $$ BEGIN
|
128
|
+
IF NOT EXISTS (
|
129
|
+
SELECT FROM pg_default_acl defacl JOIN pg_namespace n ON defacl.defaclnamespace = n.oid LEFT JOIN LATERAL aclexplode(defacl.defaclacl) acl ON true
|
130
|
+
LEFT JOIN pg_roles r_grantee ON r_grantee.oid = acl.grantee WHERE r_grantee.rolname = '#{role_name}' AND n.nspname = '#{schema}' AND defacl.defaclobjtype = 'r'
|
131
|
+
AND acl.privilege_type IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE') GROUP BY n.nspname, defacl.defaclobjtype, r_grantee.rolname HAVING COUNT(DISTINCT acl.privilege_type) = 4
|
132
|
+
) THEN
|
133
|
+
RAISE EXCEPTION 'User % is missing one or more of SELECT, INSERT, UPDATE, DELETE privileges on tables in schema %', '#{role_name}', '#{schema}';
|
134
|
+
END IF;
|
135
|
+
END $$;
|
136
|
+
SQL
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_table_privileges_sql(role_name, schema, table_name)
|
140
|
+
<<~SQL
|
141
|
+
DO $$ BEGIN
|
142
|
+
IF NOT EXISTS (
|
143
|
+
SELECT FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid LEFT JOIN LATERAL aclexplode(c.relacl) acl ON true LEFT JOIN pg_roles r_grantee ON r_grantee.oid = acl.grantee
|
144
|
+
WHERE r_grantee.rolname = '#{role_name}' AND n.nspname = '#{schema}' AND c.relname = '#{table_name}' AND acl.privilege_type IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE')
|
145
|
+
GROUP BY n.nspname, c.relname, r_grantee.rolname HAVING COUNT(DISTINCT acl.privilege_type) = 4
|
146
|
+
) THEN
|
147
|
+
RAISE EXCEPTION 'User % is missing one or more of SELECT, INSERT, UPDATE, DELETE privileges on table % in schema %', '#{role_name}', '#{table_name}', '#{schema}';
|
148
|
+
END IF;
|
149
|
+
END $$;
|
150
|
+
SQL
|
151
|
+
end
|
152
|
+
|
153
|
+
def check_default_sequence_privileges_sql(role_name, schema)
|
154
|
+
<<~SQL
|
155
|
+
DO $$ BEGIN
|
156
|
+
IF NOT EXISTS (
|
157
|
+
SELECT FROM pg_default_acl defacl JOIN pg_namespace n ON defacl.defaclnamespace = n.oid LEFT JOIN LATERAL aclexplode(defacl.defaclacl) acl ON true
|
158
|
+
LEFT JOIN pg_roles r_grantee ON r_grantee.oid = acl.grantee WHERE r_grantee.rolname = '#{role_name}' AND n.nspname = '#{schema}' AND defacl.defaclobjtype = 'S'
|
159
|
+
AND acl.privilege_type IN ('SELECT', 'USAGE') GROUP BY n.nspname, defacl.defaclobjtype, r_grantee.rolname HAVING COUNT(DISTINCT acl.privilege_type) = 2
|
160
|
+
) THEN
|
161
|
+
RAISE EXCEPTION 'User % is missing USAGE and/or SELECT privileges on sequences in schema %', '#{role_name}', '#{schema}';
|
162
|
+
END IF; END $$;
|
163
|
+
SQL
|
164
|
+
end
|
165
|
+
|
166
|
+
def check_sequence_privileges_sql(role_name, schema, sequence_name)
|
167
|
+
<<~SQL
|
168
|
+
DO $$ BEGIN
|
169
|
+
IF NOT EXISTS (
|
170
|
+
SELECT FROM pg_class c JOIN pg_namespace n ON c.relnamespace = n.oid LEFT JOIN LATERAL aclexplode(c.relacl) acl ON true LEFT JOIN pg_roles r_grantee ON r_grantee.oid = acl.grantee
|
171
|
+
WHERE r_grantee.rolname = '#{role_name}' AND n.nspname = '#{schema}' AND c.relname = '#{sequence_name}' AND acl.privilege_type IN ('SELECT', 'USAGE')
|
172
|
+
GROUP BY n.nspname, c.relname, r_grantee.rolname HAVING COUNT(DISTINCT acl.privilege_type) = 2
|
173
|
+
) THEN
|
174
|
+
RAISE EXCEPTION 'User % is missing USAGE and/or SELECT privileges on sequence % in schema %', '#{role_name}', '#{sequence_name}', '#{schema}';
|
175
|
+
END IF;
|
176
|
+
END $$;
|
177
|
+
SQL
|
178
|
+
end
|
179
|
+
|
180
|
+
def check_table_rls_enabled_sql(schema, table_name)
|
181
|
+
<<~SQL
|
182
|
+
DO $$ BEGIN
|
183
|
+
IF NOT EXISTS (
|
184
|
+
SELECT FROM pg_policies WHERE schemaname = '#{schema}' AND tablename = '#{table_name}'
|
185
|
+
) THEN
|
186
|
+
RAISE EXCEPTION 'RLS is not enabled on table %', '#{table_name}';
|
187
|
+
END IF;
|
188
|
+
END $$;
|
189
|
+
SQL
|
190
|
+
end
|
191
|
+
|
192
|
+
def check_table_user_policy_exists_sql(schema, table_name, user)
|
193
|
+
<<~SQL
|
194
|
+
DO $$ BEGIN
|
195
|
+
IF NOT EXISTS (
|
196
|
+
SELECT FROM pg_policies WHERE schemaname = '#{schema}' AND tablename = '#{table_name}' AND policyname = '#{schema}_#{table_name}_#{user}'
|
197
|
+
) THEN
|
198
|
+
RAISE EXCEPTION 'Policy %_% does not exist', '#{table_name}', '#{user}';
|
199
|
+
END IF;
|
200
|
+
END $$;
|
201
|
+
SQL
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
class UserDoesNotExistError < PgRls::Error; end
|
8
|
+
class UserNotInPgRlsGroupError < PgRls::Error; end
|
9
|
+
class UserMissingSchemaUsagePrivilegeError < PgRls::Error; end
|
10
|
+
class UserMissingTablePrivilegesError < PgRls::Error; end
|
11
|
+
class UserMissingSequencePrivilegesError < PgRls::Error; end
|
12
|
+
class TableRlsNotEnabledError < PgRls::Error; end
|
13
|
+
class TableUserPolicyDoesNotExistError < PgRls::Error; end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
# This module contains the logic to grant user privileges
|
8
|
+
module GrantRlsUserPrivileges
|
9
|
+
include SqlHelperMethod
|
10
|
+
|
11
|
+
def grant_rls_user_privileges(schema = PgRls.schema, role_name = PgRls.rls_role_group)
|
12
|
+
grant_schema_usage(schema, role_name)
|
13
|
+
grant_schema_migration_table_privileges(schema, role_name)
|
14
|
+
grant_default_sequence_privileges(schema, role_name)
|
15
|
+
grant_default_table_privileges(schema, role_name)
|
16
|
+
grant_existing_table_privileges(schema, role_name)
|
17
|
+
grant_existing_sequence_privileges(schema, role_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def revoke_rls_user_privileges(schema = PgRls.schema, role_name = PgRls.rls_role_group)
|
21
|
+
revoke_schema_usage(schema, role_name)
|
22
|
+
revoke_schema_migration_table_privileges(schema, role_name)
|
23
|
+
revoke_default_sequence_privileges(schema, role_name)
|
24
|
+
revoke_default_table_privileges(schema, role_name)
|
25
|
+
revoke_existing_table_privileges(schema, role_name)
|
26
|
+
revoke_existing_sequence_privileges(schema, role_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def revoke_schema_migration_table_privileges(schema, role_name)
|
32
|
+
statement = <<~SQL
|
33
|
+
REVOKE ALL PRIVILEGES ON TABLE #{schema}.schema_migrations FROM #{role_name};
|
34
|
+
SQL
|
35
|
+
execute_sql!(statement)
|
36
|
+
end
|
37
|
+
|
38
|
+
def revoke_schema_usage(schema, role_name)
|
39
|
+
execute_sql!(revoke_schema_usage_sql(schema, role_name))
|
40
|
+
end
|
41
|
+
|
42
|
+
def revoke_default_sequence_privileges(schema, role_name)
|
43
|
+
execute_sql!(revoke_default_sequence_privileges_sql(schema, role_name))
|
44
|
+
end
|
45
|
+
|
46
|
+
def revoke_default_table_privileges(schema, role_name)
|
47
|
+
execute_sql!(revoke_default_table_privileges_sql(schema, role_name))
|
48
|
+
end
|
49
|
+
|
50
|
+
def revoke_existing_table_privileges(schema, role_name)
|
51
|
+
execute_sql!(revoke_existing_table_privileges_sql(schema, role_name))
|
52
|
+
end
|
53
|
+
|
54
|
+
def revoke_existing_sequence_privileges(schema, role_name)
|
55
|
+
execute_sql!(revoke_existing_sequence_privileges_sql(schema, role_name))
|
56
|
+
end
|
57
|
+
|
58
|
+
def grant_schema_migration_table_privileges(schema, role_name)
|
59
|
+
statement = <<~SQL
|
60
|
+
DO $$ BEGIN
|
61
|
+
IF EXISTS (
|
62
|
+
SELECT FROM pg_catalog.pg_tables WHERE schemaname = '#{schema}' AND tablename = 'schema_migrations'
|
63
|
+
) THEN
|
64
|
+
GRANT ALL PRIVILEGES ON TABLE #{schema}.schema_migrations TO #{role_name};
|
65
|
+
END IF;
|
66
|
+
END $$;
|
67
|
+
SQL
|
68
|
+
execute_sql!(statement)
|
69
|
+
end
|
70
|
+
|
71
|
+
def grant_schema_usage(schema, role_name)
|
72
|
+
statement = <<~SQL
|
73
|
+
GRANT USAGE ON SCHEMA #{schema} TO #{role_name};
|
74
|
+
SQL
|
75
|
+
execute_sql!(statement)
|
76
|
+
end
|
77
|
+
|
78
|
+
def grant_default_sequence_privileges(schema, role_name)
|
79
|
+
statement = <<~SQL
|
80
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
|
81
|
+
GRANT USAGE, SELECT ON SEQUENCES TO #{role_name};
|
82
|
+
SQL
|
83
|
+
execute_sql!(statement)
|
84
|
+
end
|
85
|
+
|
86
|
+
def grant_default_table_privileges(schema, role_name)
|
87
|
+
statement = <<~SQL
|
88
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
|
89
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO #{role_name};
|
90
|
+
SQL
|
91
|
+
execute_sql!(statement)
|
92
|
+
end
|
93
|
+
|
94
|
+
def grant_existing_table_privileges(schema, role_name)
|
95
|
+
statement = <<~SQL
|
96
|
+
GRANT SELECT, INSERT, UPDATE, DELETE
|
97
|
+
ON ALL TABLES IN SCHEMA #{schema} TO #{role_name};
|
98
|
+
SQL
|
99
|
+
execute_sql!(statement)
|
100
|
+
end
|
101
|
+
|
102
|
+
def grant_existing_sequence_privileges(schema, role_name)
|
103
|
+
statement = <<~SQL
|
104
|
+
GRANT USAGE, SELECT
|
105
|
+
ON ALL SEQUENCES IN SCHEMA #{schema} TO #{role_name};
|
106
|
+
SQL
|
107
|
+
execute_sql!(statement)
|
108
|
+
end
|
109
|
+
|
110
|
+
def revoke_schema_usage_sql(schema, role_name)
|
111
|
+
statement = <<~SQL
|
112
|
+
REVOKE USAGE ON SCHEMA #{schema} FROM #{role_name};
|
113
|
+
SQL
|
114
|
+
|
115
|
+
role_applicable_sql_statement(role_name, statement)
|
116
|
+
end
|
117
|
+
|
118
|
+
def revoke_default_sequence_privileges_sql(schema, role_name)
|
119
|
+
statement = <<~SQL
|
120
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
|
121
|
+
REVOKE USAGE, SELECT ON SEQUENCES FROM #{role_name};
|
122
|
+
SQL
|
123
|
+
|
124
|
+
role_applicable_sql_statement(role_name, statement)
|
125
|
+
end
|
126
|
+
|
127
|
+
def revoke_default_table_privileges_sql(schema, role_name)
|
128
|
+
statement = <<~SQL
|
129
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA #{schema}
|
130
|
+
REVOKE SELECT, INSERT, UPDATE, DELETE ON TABLES FROM #{role_name};
|
131
|
+
SQL
|
132
|
+
|
133
|
+
role_applicable_sql_statement(role_name, statement)
|
134
|
+
end
|
135
|
+
|
136
|
+
def revoke_existing_table_privileges_sql(schema, role_name)
|
137
|
+
statement = <<~SQL
|
138
|
+
REVOKE SELECT, INSERT, UPDATE, DELETE
|
139
|
+
ON ALL TABLES IN SCHEMA #{schema} FROM #{role_name};
|
140
|
+
SQL
|
141
|
+
|
142
|
+
role_applicable_sql_statement(role_name, statement)
|
143
|
+
end
|
144
|
+
|
145
|
+
def revoke_existing_sequence_privileges_sql(schema, role_name)
|
146
|
+
statement = <<~SQL
|
147
|
+
REVOKE USAGE, SELECT
|
148
|
+
ON ALL SEQUENCES IN SCHEMA #{schema} FROM #{role_name};
|
149
|
+
SQL
|
150
|
+
|
151
|
+
role_applicable_sql_statement(role_name, statement)
|
152
|
+
end
|
153
|
+
|
154
|
+
def role_applicable_sql_statement(role_name, statement)
|
155
|
+
<<~SQL
|
156
|
+
DO $do$ BEGIN
|
157
|
+
IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '#{role_name}') THEN
|
158
|
+
#{statement}
|
159
|
+
END IF;
|
160
|
+
END $do$;
|
161
|
+
SQL
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
# This module contains the logic to create, drop and validate RLS functions
|
8
|
+
module RlsFunctions
|
9
|
+
include SqlHelperMethod
|
10
|
+
|
11
|
+
def function_exists?(function_name)
|
12
|
+
query = <<~SQL
|
13
|
+
SELECT 1
|
14
|
+
FROM pg_proc
|
15
|
+
WHERE proname = '#{function_name}'
|
16
|
+
SQL
|
17
|
+
|
18
|
+
execute_sql!(query).any?
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_rls_functions
|
22
|
+
create_rls_exception
|
23
|
+
create_tenant_id_setter_function
|
24
|
+
create_tenant_id_update_blocker_function
|
25
|
+
end
|
26
|
+
|
27
|
+
def drop_rls_functions
|
28
|
+
drop_function("tenant_id_setter")
|
29
|
+
drop_function("rls_exception")
|
30
|
+
drop_function("tenant_id_update_blocker")
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def create_function(name, body)
|
36
|
+
query = <<~SQL
|
37
|
+
CREATE OR REPLACE FUNCTION #{name}()
|
38
|
+
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
39
|
+
#{body}
|
40
|
+
$$;
|
41
|
+
SQL
|
42
|
+
|
43
|
+
execute_sql!(query)
|
44
|
+
end
|
45
|
+
|
46
|
+
def drop_function(name)
|
47
|
+
query = <<~SQL
|
48
|
+
DROP FUNCTION IF EXISTS #{name}() CASCADE;
|
49
|
+
SQL
|
50
|
+
|
51
|
+
execute_sql!(query)
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_rls_exception
|
55
|
+
body = <<~SQL
|
56
|
+
BEGIN
|
57
|
+
RAISE EXCEPTION 'This column is guarded due to tenancy dependency';
|
58
|
+
END
|
59
|
+
SQL
|
60
|
+
|
61
|
+
create_function("rls_exception", body)
|
62
|
+
end
|
63
|
+
|
64
|
+
def create_tenant_id_setter_function
|
65
|
+
body = <<~SQL
|
66
|
+
BEGIN
|
67
|
+
new.tenant_id:= (current_setting('rls.tenant_id'));
|
68
|
+
RETURN NEW;
|
69
|
+
END;
|
70
|
+
SQL
|
71
|
+
|
72
|
+
create_function("tenant_id_setter", body)
|
73
|
+
end
|
74
|
+
|
75
|
+
def create_tenant_id_update_blocker_function
|
76
|
+
body = <<~SQL
|
77
|
+
BEGIN
|
78
|
+
IF OLD.tenant_id IS NOT NULL AND NEW.tenant_id != OLD.tenant_id THEN
|
79
|
+
RAISE EXCEPTION 'Updating tenant_id is not allowed';
|
80
|
+
END IF;
|
81
|
+
RETURN NEW;
|
82
|
+
END;
|
83
|
+
SQL
|
84
|
+
|
85
|
+
create_function("tenant_id_update_blocker", body)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
# This module contains the logic to grant user privileges
|
8
|
+
module RlsPolicies
|
9
|
+
include SqlHelperMethod
|
10
|
+
|
11
|
+
def enable_table_rls(table_name, user = PgRls.username, schema = PgRls.schema)
|
12
|
+
execute_sql!(create_rls_policy_sql(schema, table_name, user))
|
13
|
+
execute_sql!(enable_row_level_security_sql(schema, table_name))
|
14
|
+
end
|
15
|
+
|
16
|
+
def disable_table_rls(table_name, user = PgRls.username, schema = PgRls.schema)
|
17
|
+
execute_sql!(drop_rls_policy_sql(schema, table_name, user))
|
18
|
+
execute_sql!(disable_row_level_security_sql(schema, table_name))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def drop_rls_policy_sql(schema, table_name, user)
|
24
|
+
<<~SQL
|
25
|
+
DROP POLICY IF EXISTS #{schema}_#{table_name}_#{user}
|
26
|
+
ON #{schema}.#{table_name};
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
|
30
|
+
def disable_row_level_security_sql(schema, table_name)
|
31
|
+
<<~SQL
|
32
|
+
ALTER TABLE IF EXISTS #{schema}_#{table_name}
|
33
|
+
DISABLE ROW LEVEL SECURITY;
|
34
|
+
SQL
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_rls_policy_sql(schema, table_name, user)
|
38
|
+
<<~SQL
|
39
|
+
CREATE POLICY #{schema}_#{table_name}_#{user}
|
40
|
+
ON #{schema}.#{table_name}
|
41
|
+
TO #{user}
|
42
|
+
USING (tenant_id = NULLIF(current_setting('rls.tenant_id', TRUE), '')::uuid);
|
43
|
+
SQL
|
44
|
+
end
|
45
|
+
|
46
|
+
def enable_row_level_security_sql(schema, table_name)
|
47
|
+
<<~SQL
|
48
|
+
ALTER TABLE #{schema}.#{table_name}
|
49
|
+
ENABLE ROW LEVEL SECURITY;
|
50
|
+
SQL
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PgRls
|
4
|
+
module ActiveRecord
|
5
|
+
module ConnectionAdapters
|
6
|
+
module PostgreSQL
|
7
|
+
# This module contains the logic to create, drop and validate RLS functions
|
8
|
+
module RlsTriggers
|
9
|
+
include SqlHelperMethod
|
10
|
+
|
11
|
+
def trigger_exists?(table_name, function_name, schema = PgRls.schema)
|
12
|
+
query = <<~SQL
|
13
|
+
SELECT 1
|
14
|
+
FROM pg_trigger
|
15
|
+
WHERE tgname = '#{schema}_#{table_name}_#{function_name}_trigger';
|
16
|
+
SQL
|
17
|
+
|
18
|
+
execute_sql!(query).any?
|
19
|
+
end
|
20
|
+
|
21
|
+
def append_tenant_table_triggers(table_name, schema = PgRls.schema)
|
22
|
+
create_rls_exception_trigger(schema, table_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def append_rls_table_triggers(table_name, schema = PgRls.schema)
|
26
|
+
create_tenant_id_setter_trigger(schema, table_name)
|
27
|
+
create_tenant_id_update_blocker_trigger(schema, table_name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def drop_tenant_table_triggers(table_name, schema = PgRls.schema)
|
31
|
+
drop_trigger(schema, table_name, "#{schema}_#{table_name}_rls_exception_trigger")
|
32
|
+
end
|
33
|
+
|
34
|
+
def drop_rls_table_triggers(table_name, schema = PgRls.schema)
|
35
|
+
drop_trigger(schema, table_name, "#{schema}_#{table_name}_tenant_id_setter_trigger")
|
36
|
+
drop_trigger(schema, table_name, "#{schema}_#{table_name}_tenant_id_update_blocker_trigger")
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def drop_trigger(schema, table_name, trigger_name)
|
42
|
+
query = <<~SQL
|
43
|
+
DROP TRIGGER IF EXISTS #{trigger_name} ON #{schema}.#{table_name};
|
44
|
+
SQL
|
45
|
+
|
46
|
+
execute_sql!(query)
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_trigger(schema, table_name, trigger_name, function_name, timing, event) # rubocop:disable Metrics/ParameterLists
|
50
|
+
query = <<~SQL
|
51
|
+
CREATE TRIGGER #{trigger_name}
|
52
|
+
#{timing} #{event} ON #{schema}.#{table_name}
|
53
|
+
FOR EACH ROW EXECUTE PROCEDURE #{function_name}();
|
54
|
+
SQL
|
55
|
+
|
56
|
+
execute_sql!(query)
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_rls_exception_trigger(schema, table_name)
|
60
|
+
create_trigger(
|
61
|
+
schema,
|
62
|
+
table_name,
|
63
|
+
"#{schema}_#{table_name}_rls_exception_trigger",
|
64
|
+
"rls_exception",
|
65
|
+
"BEFORE",
|
66
|
+
"UPDATE OF tenant_id"
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def create_tenant_id_setter_trigger(schema, table_name)
|
71
|
+
create_trigger(
|
72
|
+
schema,
|
73
|
+
table_name,
|
74
|
+
"#{schema}_#{table_name}_tenant_id_setter_trigger",
|
75
|
+
"tenant_id_setter",
|
76
|
+
"BEFORE",
|
77
|
+
"INSERT"
|
78
|
+
)
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_tenant_id_update_blocker_trigger(schema, table_name)
|
82
|
+
create_trigger(
|
83
|
+
schema,
|
84
|
+
table_name,
|
85
|
+
"#{schema}_#{table_name}_tenant_id_update_blocker_trigger",
|
86
|
+
"tenant_id_update_blocker",
|
87
|
+
"BEFORE",
|
88
|
+
"UPDATE OF tenant_id"
|
89
|
+
)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|