pg_rls 0.2.5 → 1.0.0

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.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +55 -17
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +19 -2
  5. data/CODE_OF_CONDUCT.md +77 -29
  6. data/Guardfile +44 -0
  7. data/README.md +247 -83
  8. data/Rakefile +5 -12
  9. data/Steepfile +29 -0
  10. data/UPGRADE.md +106 -0
  11. data/app/models/pg_rls/admin.rb +24 -0
  12. data/app/models/pg_rls/current.rb +48 -0
  13. data/app/models/pg_rls/record.rb +13 -0
  14. data/app/models/pg_rls/tenant/searchable.rb +60 -0
  15. data/app/models/pg_rls/tenant/securable.rb +67 -0
  16. data/app/models/pg_rls/tenant/switchable.rb +40 -0
  17. data/app/models/pg_rls/tenant.rb +9 -0
  18. data/assets/logo.svg +8 -0
  19. data/docker-compose.yml +14 -0
  20. data/lib/generators/pg_rls/active_record/active_record_generator.rb +62 -65
  21. data/lib/generators/pg_rls/install/install_generator.rb +38 -0
  22. data/lib/generators/pg_rls/pg_rls_generator.rb +2 -1
  23. data/lib/generators/pg_rls/templates/USAGE +28 -0
  24. data/lib/generators/pg_rls/templates/app/models/abstract_base_class.rb.tt +7 -0
  25. data/lib/generators/pg_rls/templates/app/models/model.rb.tt +22 -0
  26. data/lib/generators/pg_rls/templates/config/initializers/pg_rls.rb.tt +58 -0
  27. data/lib/generators/pg_rls/templates/db/migrate/backport_pg_rls_table.rb.tt +14 -0
  28. data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_table.rb.tt +5 -0
  29. data/lib/generators/pg_rls/templates/db/migrate/convert_to_pg_rls_tenant_table.rb.tt +5 -0
  30. data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_table.rb.tt +29 -0
  31. data/lib/generators/pg_rls/templates/db/migrate/create_pg_rls_tenant_table.rb.tt +29 -0
  32. data/lib/pg_rls/active_record/connection_adapters/connection_pool.rb +31 -0
  33. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rb +207 -0
  34. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/errors.rb +17 -0
  35. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rb +167 -0
  36. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rb +91 -0
  37. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rb +56 -0
  38. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rb +95 -0
  39. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rb +127 -0
  40. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rb +71 -0
  41. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb +120 -0
  42. data/lib/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rb +30 -0
  43. data/lib/pg_rls/active_record/connection_adapters/postgre_sql.rb +36 -0
  44. data/lib/pg_rls/active_record/connection_adapters.rb +12 -0
  45. data/lib/pg_rls/active_record/database_shards.rb +74 -0
  46. data/lib/pg_rls/active_record/migration/command_recorder.rb +28 -0
  47. data/lib/pg_rls/active_record/migration.rb +11 -0
  48. data/lib/pg_rls/active_record/test_databases.rb +19 -0
  49. data/lib/pg_rls/active_record.rb +11 -0
  50. data/lib/pg_rls/active_support/string_ext.rb +17 -0
  51. data/lib/pg_rls/active_support.rb +9 -0
  52. data/lib/pg_rls/connection_config.rb +61 -0
  53. data/lib/pg_rls/deprecation.rb +14 -0
  54. data/lib/pg_rls/engine.rb +8 -0
  55. data/lib/pg_rls/error.rb +10 -0
  56. data/lib/pg_rls/generators/.keep +0 -0
  57. data/lib/pg_rls/railtie.rb +1 -11
  58. data/lib/pg_rls/tasks/.keep +0 -0
  59. data/lib/pg_rls/version.rb +3 -1
  60. data/lib/pg_rls.rb +67 -151
  61. data/rbs_collection.lock.yaml +132 -0
  62. data/rbs_collection.yaml +127 -0
  63. data/review_code.sh +33 -0
  64. data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
  65. data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
  66. data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
  67. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
  68. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
  69. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
  70. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
  71. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
  72. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
  73. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
  74. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
  75. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
  76. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
  77. data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
  78. data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
  79. data/sig/pg_rls/active_record/database_shards.rbs +34 -0
  80. data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
  81. data/sig/pg_rls/active_record/migration.rbs +8 -0
  82. data/sig/pg_rls/active_record.rbs +7 -0
  83. data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
  84. data/sig/pg_rls/active_support/string_ext.rbs +27 -0
  85. data/sig/pg_rls/active_support.rbs +7 -0
  86. data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
  87. data/sig/pg_rls/connection_config.rbs +16 -0
  88. data/sig/pg_rls/deprecation.rbs +9 -0
  89. data/sig/pg_rls/engine.rbs +7 -0
  90. data/sig/pg_rls/errors.rbs +14 -0
  91. data/sig/pg_rls/railtie.rbs +6 -0
  92. data/sig/pg_rls/tenant_test_helper.rbs +14 -0
  93. data/sig/pg_rls.rbs +60 -0
  94. data/sig/support/active_record.rbs +86 -0
  95. data/sig/support/active_support.rbs +7 -0
  96. data/sig/support/fowardable.rbs +2 -0
  97. data/sig/support/pg.rbs +12 -0
  98. data/sig/support/rails.rbs +38 -0
  99. data/start.sh +30 -0
  100. metadata +167 -12
  101. data/Gemfile +0 -21
  102. data/Gemfile.lock +0 -300
  103. data/bin/console +0 -15
  104. data/bin/setup +0 -8
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ # ActiveRecord ConnectionPool Connection Adapter Extension
7
+ module ConnectionPool
8
+ def checkout(checkout_timeout = @checkout_timeout)
9
+ conn = super
10
+ return conn unless rls_connection?
11
+ return reset_rls_used_connections(conn) if PgRls::Current.tenant.nil?
12
+
13
+ PgRls::Current.tenant.set_rls(conn)
14
+ conn
15
+ end
16
+
17
+ def rls_connection?
18
+ pool_config.db_config.configuration_hash[:rls] == true
19
+ end
20
+
21
+ def reset_rls_used_connections(connection)
22
+ PgRls::Tenant.reset_rls_used_connections(connection)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ ActiveRecord::ConnectionAdapters::ConnectionPool.prepend(
30
+ PgRls::ActiveRecord::ConnectionAdapters::ConnectionPool
31
+ )
@@ -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