pg_rls 0.2.4 → 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 (105) 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/multi_tenancy.rb +1 -1
  58. data/lib/pg_rls/railtie.rb +1 -11
  59. data/lib/pg_rls/tasks/.keep +0 -0
  60. data/lib/pg_rls/version.rb +3 -1
  61. data/lib/pg_rls.rb +67 -151
  62. data/rbs_collection.lock.yaml +132 -0
  63. data/rbs_collection.yaml +127 -0
  64. data/review_code.sh +33 -0
  65. data/sig/generators/pg_rls/active_record/active_record_generator.rbs +43 -0
  66. data/sig/generators/pg_rls/install/install_generator.rbs +20 -0
  67. data/sig/generators/pg_rls/pg_rls_generator.rbs +9 -0
  68. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/check_rls_user_privileges.rbs +53 -0
  69. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/errors.rbs +24 -0
  70. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/grant_rls_user_privileges.rbs +55 -0
  71. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_functions.rbs +31 -0
  72. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_policies.rbs +28 -0
  73. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_triggers.rbs +35 -0
  74. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/rls_user_statements.rbs +48 -0
  75. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_dumper.rbs +38 -0
  76. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rbs +67 -0
  77. data/sig/pg_rls/active_record/connection_adapters/postgre_sql/sql_helper_method.rbs +21 -0
  78. data/sig/pg_rls/active_record/connection_adapters/postgresql.rbs +10 -0
  79. data/sig/pg_rls/active_record/connection_adapters.rbs +50 -0
  80. data/sig/pg_rls/active_record/database_shards.rbs +34 -0
  81. data/sig/pg_rls/active_record/migration/command_recorder.rbs +14 -0
  82. data/sig/pg_rls/active_record/migration.rbs +8 -0
  83. data/sig/pg_rls/active_record.rbs +7 -0
  84. data/sig/pg_rls/active_support/hash_ext.rbs +11 -0
  85. data/sig/pg_rls/active_support/string_ext.rbs +27 -0
  86. data/sig/pg_rls/active_support.rbs +7 -0
  87. data/sig/pg_rls/app/models/pg_rls/record.rbs +4 -0
  88. data/sig/pg_rls/connection_config.rbs +16 -0
  89. data/sig/pg_rls/deprecation.rbs +9 -0
  90. data/sig/pg_rls/engine.rbs +7 -0
  91. data/sig/pg_rls/errors.rbs +14 -0
  92. data/sig/pg_rls/railtie.rbs +6 -0
  93. data/sig/pg_rls/tenant_test_helper.rbs +14 -0
  94. data/sig/pg_rls.rbs +60 -0
  95. data/sig/support/active_record.rbs +86 -0
  96. data/sig/support/active_support.rbs +7 -0
  97. data/sig/support/fowardable.rbs +2 -0
  98. data/sig/support/pg.rbs +12 -0
  99. data/sig/support/rails.rbs +38 -0
  100. data/start.sh +30 -0
  101. metadata +167 -12
  102. data/Gemfile +0 -21
  103. data/Gemfile.lock +0 -300
  104. data/bin/console +0 -15
  105. data/bin/setup +0 -8
data/README.md CHANGED
@@ -1,24 +1,26 @@
1
- <!--
2
- Title: PgRls Rails
3
- Description: rails multitenancy with pg rls
4
- Author: dandush03
5
- -->
6
- <meta name="google-site-verification" content="Mc1vBv8PRYPw_cdd3EiKhF2vlOeIEIk3VYhAg75ertI" />
1
+ # PgRls Rails
2
+
3
+ > PostgreSQL Row Level Security: The Rails right way to do multitenancy
7
4
 
8
5
  [![Contributors][contributors-shield]][contributors-url]
9
6
  [![Forks][forks-shield]][forks-url]
10
7
  [![Stargazers][stars-shield]][stars-url]
11
8
  [![Issues][issues-shield]][issues-url]
12
- [![LinkedIn][linkedin-shield2]][linkedin-url2]
13
- [![Hireable][hireable]][hireable-url]
9
+ [![MIT License][license-shield]][license-url]
10
+ [![LinkedIn][linkedin-shield]][linkedin-url]
14
11
  [![Donate][donate]][paypal-donate-code]
12
+ [![Hireable][hireable]][hireable-url]
15
13
 
16
- <!-- PROJECT LOGO -->
17
- <br />
18
14
  <p align="center">
19
- <h1 align="center">PgRls<h2 align="center">PostgreSQL Row Level Security<br />The Rails right way to do multitenancy</h2></h1>
15
+
16
+ <h3 align="center">
17
+ <a href="https://github.com/Dandush03/pg_rls">
18
+ <img src="./assets/logo.svg" alt="Logo" width="80" height="80">
19
+ </a>
20
+ </h3>
20
21
 
21
22
  <p align="center">
23
+ PostgreSQL Row Level Security: The Rails right way to do multitenancy
22
24
  <br />
23
25
  <a href="https://github.com/Dandush03/pg_rls/wiki"><strong>Explore the docs »</strong></a>
24
26
  <br />
@@ -26,125 +28,288 @@
26
28
  <a href="https://github.com/Dandush03/pg_rls/issues">Report Bug</a>
27
29
  ·
28
30
  <a href="https://github.com/Dandush03/pg_rls/issues">Request Feature</a>
29
- ·
30
- <a href="https://github.com/Dandush03/pg_rls">API Repo</a>
31
31
  </p>
32
-
33
32
  </p>
34
33
 
35
- ### Table of Contents
36
- * [Required Installations](#required-Installations)
37
- * [Installing](#installing)
38
- * [Instructions](#instructions)
39
- * [Testing](#Testing)
40
- * [Development](#testing)
41
- * [Contact](#contact)
42
- * [Contributing](#contributing)
43
- * [License](#license)
44
- * [Code of Conduct](#Code-of-Conduct)
45
- * [Show your support](#Show-your-support)
34
+ ## Table of Contents
35
+
36
+ - [About The Project](#about-the-project)
37
+ - [Getting Started](#getting-started)
38
+ - [Prerequisites](#prerequisites)
39
+ - [Installation](#installation)
40
+ - [Configuration](#configuration)
41
+ - [How It Works](#how-it-works)
42
+ - [Usage](#usage)
43
+ - [RLS Index Management Methods](#rls-index-management-methods)
44
+ - [Testing](#testing)
45
+ - [Development](#development)
46
+ - [Development Workflow](#development-workflow)
47
+ - [Releasing a New Version](#releasing-a-new-version)
48
+ - [Contributing](#contributing)
49
+ - [License](#license)
50
+ - [Contact](#contact)
51
+ - [Acknowledgements](#acknowledgements)
52
+
53
+ ## About The Project
54
+
55
+ It's time we start doing multitenancy right! You can avoid creating a separate Postgres schema/databases for each customer or trying to ensure the WHERE clause of every single query includes the particular company. Just integrate PgRls seamlessly to your application.
56
+
57
+ This gem will integrate PostgreSQL RLS to help you develop a great multitenancy application.
58
+
59
+ ## Getting Started
60
+
61
+ ### Prerequisites
46
62
 
47
- ### It's time we start doing multitenancy right! You can avoid creating a separate Postgres schema/databases for each customer or trying to ensure the WHERE clause of every single query includes the particular company. Just integrate PgRls seamlessly to your application.
63
+ - Ruby (~> 3.0)
64
+ - ActiveRecord (~> 7.0)
65
+ - PostgreSQL (> 9.0)
66
+ - Warden
67
+ - pg (~> 1.2)
48
68
 
49
- ### This gem will integrate PostgreSQL RLS to help you develop a great multitenancy application.
69
+ ### Installation
50
70
 
51
- ## Required Installation
52
- ### Installing
71
+ 1. Add this line to your application's Gemfile:
53
72
 
54
- Add this line to your application's Gemfile:
73
+ ```ruby
74
+ gem 'pg_rls'
75
+ ```
76
+
77
+ 2. Execute:
78
+
79
+ ```bash
80
+ bundle install
81
+ ```
82
+
83
+ Or install it yourself with:
84
+
85
+ ```bash
86
+ gem install pg_rls
87
+ ```
88
+
89
+ ### Configuration
90
+
91
+ You must configure the `rls_mode` in your `database.yml` file. This setting controls how RLS (Row-Level Security) connections are handled for your app. It supports three modes:
92
+
93
+ - none: No RLS connections.
94
+ - single: Only RLS connections, which is ideal for production environments.
95
+ - dual: Both RLS and non-RLS connections, mainly for development and testing.
96
+
97
+ Example configuration in database.yml for development:
55
98
 
56
99
  ```ruby
57
- gem 'pg_rls'
100
+ development:
101
+ <<: *default
102
+ database: dev_db
103
+ # Use 'dual' for development to switch between RLS and non-RLS connections.
104
+ rls_mode: <%= ENV.fetch('RLS_MODE', 'dual') %>
58
105
  ```
59
106
 
60
- And then execute:
107
+ #### Using the `dual` mode is not recommended in high-demand environments, as it will duplicate the connection pool for each RLS shard, leading to unnecessary overhead. Instead, configure the RLS mode to `single` or `none` and balance your requests accordingly. The `single` mode ensures only RLS connections are used, while `none` disables RLS for this environment
61
108
 
62
- $ bundle install
109
+ #### For flexible production configurations, you can use an environment variable to set the rls_mode
63
110
 
64
- Or install it yourself with:
111
+ ```ruby
112
+ production:
113
+ <<: *default
114
+ database: prod_db
115
+ rls_mode: <%= ENV.fetch('RLS_MODE', 'dual') %>
116
+ ```
65
117
 
66
- $ gem install pg_rls
118
+ ### How It Works
67
119
 
68
- ### Instructions
120
+ The `rls_mode` setting in your `database.yml` controls how your application handles database connections with Row-Level Security (RLS). Here's a breakdown of how each mode functions:
69
121
 
70
- ```bash
71
- rails generate pg_rls:install company #=> where company eq tenant model name
122
+ 1. **Single Mode:** In this mode, the application will modify the database connection’s `username` to `PgRls.username`, ensuring that all queries are executed with RLS rules enabled.
123
+ - **Effect:** Only RLS connections are used, and all operations are securely performed within the specified tenant context. This is recommended for production environments where strict RLS enforcement is needed, or when the application does not need to execute queries as an "admin" user.
124
+ - **Use Case:** When RLS is required for all operations to prevent unauthorized data access.
125
+
126
+ 2. **None Mode**: This mode does not modify any shards or connections. The username and connection behavior remain as defined in your database.yml, without applying the RLS username.
127
+ - **Effect:** No RLS rules are applied, and the application operates in a traditional mode without tenant-based restrictions.
128
+ - **Use Case:** Useful when you do not need to enforce tenant isolation through RLS, such as for administrative tasks or environments that do not require multi-tenancy.
129
+
130
+ 3. **Dual Mode:** In this mode, the application will duplicate each shard that has RLS enabled, adding a prefix of `rls_` to the shard name. Both RLS and non-RLS connections will be available. For example, if your shard is named animals, the RLS version will be named `rls_animals`.
131
+ - **Effect:** This maintains two connection pools per shard, one with RLS (`rls_` prefixed) and one without. While it provides flexibility, it also increases resource consumption by duplicating the connection pool.
132
+ - **Use Case:** This mode can be used in production environments for applications that require both RLS and non-RLS connections but is not recommended for extremely high-demand environments due to the overhead caused by duplicating the connection pools. In less demanding production settings, it offers useful flexibility.
133
+
134
+ ### Configuring `PgRls::Current`
135
+
136
+ You can configure `PgRls::Current` dynamically using an initializer. This allows you to specify the attributes that should be tracked in the request context. By default, `PgRls::Current` stores tenant-related information, but you can extend it as needed.
137
+
138
+ #### **Defining Custom Current Attributes**
139
+ To configure the attributes, update your Rails initializer (`config/initializers/pg_rls.rb`):
140
+
141
+ ```ruby
142
+ PgRls.setup do |config|
143
+ current_attributes = %i[organization__branch]
144
+ end
72
145
  ```
73
- You can change company to anything you'd like, for example, `tenant`
74
- This will generate the model and inject all the required code
75
146
 
76
- For any new model that needs to be under rls, you can generate it by writing
147
+ This ensures that your custom attributes are loaded and available across requests.
77
148
 
78
- ```bash
79
- rails generate pg_rls user #=> where user eq model name
149
+ #### **Using `__` Convention for Subclasses**
150
+ Inspired by Stimulus controllers, `PgRls::Current` supports a **double underscore (`__`) convention** to allow easy reference to subclasses of your models.
151
+
152
+ For example, if you have the following models:
153
+
154
+ ```ruby
155
+ class Organization < ApplicationRecord; end
156
+ class Organization::Branch < ApplicationRecord; end
80
157
  ```
81
- and it will generate all the necesary information for you.
82
158
 
83
- You can swtich to another tenant by using
159
+ You can dynamically access `Organization::Branch.first` using:
160
+
84
161
  ```ruby
85
- PgRls::Tenant.switch :app #=> where app eq tenant name
162
+ PgRls::Current.organization__branch # Resolves to Organization::Branch.first
86
163
  ```
87
- Don't forget to update how you want `PgRls` to find your tenant, you can set multiple options by modifying `api/config/initializers/pg_rls.rb` `search_methods`
88
164
 
89
- ```yml
90
- # app/config/database.yml
91
- <% def db_username
92
- return PgRls.username unless ENV['AS_DB_ADMIN']
165
+ This works because the `PgRls::Current` implementation automatically transforms attribute names with `__` into proper class names, making it easy to extend without manual configurations.
93
166
 
94
- Rails.application.credentials.dig(:database, :server_1, :username)
95
- end %>
167
+ This approach provides a flexible way to structure your tenant-based logic without requiring manual mappings for every subclass.
96
168
 
97
- ...
169
+ #### **Ensuring Proper RLS Configuration**
170
+ Since `PgRls::Current` uses `.first` to retrieve the record, you should ensure that the table is under **Row-Level Security (RLS)** and that the attribute used is **unique** within the tenant's scope. If the record is not unique, it is recommended to **manually set the attribute** to avoid unintended results from querying the first available record.
98
171
 
99
- development:
100
- <<: *default
101
- database: example_development
102
- username: <%= db_username %> # Apply this to production and all env including tests
172
+ Example of setting the attribute manually:
173
+
174
+ ```ruby
175
+ PgRls::Current.organization__branch = Organization::Branch.find_by(name: 'Main Branch')
176
+ ```
177
+
178
+ ## Usage
103
179
 
104
- ...
180
+ 1. Generate the necessary files:
105
181
 
182
+ ```bash
183
+ rails generate pg_rls:install company # where 'company' is your tenant model name
184
+ ```
185
+
186
+ You can change 'company' to anything you'd like, for example, 'tenant'.
187
+
188
+ 2. For any new model that needs to be under RLS:
189
+
190
+ ```bash
191
+ rails generate pg_rls user # where 'user' is your model name
192
+ ```
193
+
194
+ 3. Switch to another tenant:
195
+
196
+ ```ruby
197
+ PgRls::Tenant.switch :app # where 'app' is your tenant name
198
+ ```
199
+
200
+ ## RLS Index Management Methods
201
+
202
+ These functions help manage indexes on tables protected by Row Level Security (RLS), ensuring that the `tenant_id` field is always included in the indexes to maintain integrity and multitenant isolation.
203
+
204
+ ### `create_rls_index`
205
+ Creates an index on an RLS-enabled table, automatically adding the `tenant_id` field if it is not present in the list of columns.
206
+
207
+ **Usage:**
208
+ ```ruby
209
+ create_rls_index(:users, [:email])
210
+ # This will create an index on [:email, :tenant_id] for the users table
106
211
  ```
107
- ### Testing
108
212
 
109
- If you are getting `PG::InsufficientPrivilege: ERROR: permission denied ` you can override does permistion by running `RAILS_ENV=test rake db:grant_usage`
213
+ You can also pass additional options compatible with `add_index`:
214
+ ```ruby
215
+ create_rls_index(:users, [:email], unique: true, name: 'index_users_on_email_and_tenant_id')
216
+ ```
110
217
 
111
- Many application uses some sort of database cleaner before running thair spec so on each test that we run we'll have an empty state. Usually, those gems clear our user configuration for the database. To solve this issue, we must implement the following:
218
+ ### `drop_rls_index`
219
+ Removes an index created with `create_rls_index`, ensuring that the same columns (including `tenant_id`) are used.
112
220
 
221
+ **Usage:**
113
222
  ```ruby
114
- # spec/rails_helper.rb
223
+ drop_rls_index(:users, [:email])
224
+ # Removes the index on [:email, :tenant_id] for the users table
225
+ ```
226
+
227
+ These functions are defined in `lib/pg_rls/active_record/connection_adapters/postgre_sql/schema_statements.rb` and are useful for maintaining index consistency in multitenant environments with RLS.
115
228
 
116
- ...
117
- # some database cleaning strategy
229
+ ## Testing
118
230
 
231
+ If you encounter `PG::InsufficientPrivilege: ERROR: permission denied`, override permissions by running:
232
+
233
+ ```bash
234
+ RAILS_ENV=test rake db:grant_usage
235
+ ```
236
+
237
+ For database cleaning strategies, implement the following in your `spec/rails_helper.rb`:
238
+
239
+ ```ruby
119
240
  config.before(:suite) do
120
- # Create the tenant which in this example is company and we are using FactoryBot
121
241
  FactoryBot.create(:company, subdomain: 'app')
122
- # In this default case our initializer is set to search by subdomain so will use it
123
242
  PgRls::Tenant.switch :app
124
243
  end
125
- ...
126
244
  ```
245
+
246
+ ### Running tests in parallel
247
+
248
+ If you want to run your tests using `parallelize`, make sure to include the following in your test helper file (for example, `test_helper.rb` or `rails_helper.rb`):
249
+
250
+ ```ruby
251
+ require "pg_rls/active_record/test_databases"
252
+ ```
253
+
254
+ This is required for proper test database setup when running tests in parallel. You can see an example in the `test/test_helper.rb` file in this repository.
255
+
127
256
  ## Development
128
257
 
129
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
258
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt.
259
+
260
+ ### Development Workflow
261
+
262
+ Before each push, follow this workflow:
263
+
264
+ 1. Run quality checks:
265
+
266
+ ```bash
267
+ ./review_code.sh
268
+ ```
269
+
270
+ This script performs:
271
+ - Rubocop
272
+ - RSpec (100% code coverage required)
273
+ - Steep (type checking)
274
+
275
+ 2. Ensure 100% documentation coverage.
130
276
 
131
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
277
+ 3. Run tests:
278
+
279
+ ```bash
280
+ bin/test
281
+ ```
282
+
283
+ ### Releasing a New Version
284
+
285
+ 1. Update the version number in `version.rb`
286
+ 2. Run `bundle exec rake release`
132
287
 
133
288
  ## Contributing
134
289
 
135
- Bug reports and pull requests are welcome on GitHub at https://github.com/dandush03/pg_rls. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/dandush03/pg_rls/blob/master/CODE_OF_CONDUCT.md).
290
+ 1. Fork the Project
291
+ 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
292
+ 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
293
+ 4. Push to the Branch (`git push origin feature/AmazingFeature`)
294
+ 5. Open a Pull Request
136
295
 
137
296
  ## License
138
297
 
139
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
298
+ Distributed under the MIT License. See `LICENSE` for more information.
299
+
300
+ ## Contact
301
+
302
+ If you need help, feel free to reach out through the repository [issues](https://github.com/dandush03/pg_rls/issues) page or contact me via [LinkedIn](https://www.linkedin.com/in/daniel-laloush/).
303
+
304
+ Project Link: [https://github.com/Dandush03/pg_rls](https://github.com/Dandush03/pg_rls)
140
305
 
141
- ## Code of Conduct
306
+ ## Acknowledgements
142
307
 
143
- Everyone interacting in the PgRls project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/dandush03/pg_rls/blob/master/CODE_OF_CONDUCT.md).
308
+ - [GitHub Emoji Cheat Sheet](https://www.webpagefx.com/tools/emoji-cheat-sheet)
309
+
310
+ - [Choose an Open Source License](https://choosealicense.com)
311
+ - [GitHub Pages](https://pages.github.com)
144
312
 
145
- ## Note
146
- Currently we only support subdomain as a searcher but will soon integrate slug/domain and cookies support
147
- we recommed the use of ``
148
313
  ## Show your support
149
314
 
150
315
  Give a ⭐️ if you like this project!
@@ -153,8 +318,7 @@ If this project help you reduce time to develop, you can give me a cup of coffee
153
318
 
154
319
  [![paypal][paypal-url]][paypal-donate-code]
155
320
 
156
- <!-- MARKDOWN LINKS & IMAGES -->
157
- [contributors-shield]: https://img.shields.io/github/contributors/Dandush03/React-Calculator.svg?style=flat-square
321
+ [contributors-shield]: https://img.shields.io/github/contributors/Dandush03/pg_rls.svg?style=flat-square
158
322
  [contributors-url]: https://github.com/Dandush03/pg_rls/graphs/contributors
159
323
  [forks-shield]: https://img.shields.io/github/forks/Dandush03/pg_rls.svg?style=flat-square
160
324
  [forks-url]: https://github.com/Dandush03/pg_rls/network/members
@@ -164,10 +328,10 @@ If this project help you reduce time to develop, you can give me a cup of coffee
164
328
  [issues-url]: https://github.com/Dandush03/pg_rls/issues
165
329
  [license-shield]: https://img.shields.io/github/license/Dandush03/pg_rls.svg?style=flat-square
166
330
  [license-url]: https://github.com/Dandush03/pg_rls/blob/master/LICENSE.txt
167
- [linkedin-shield2]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
168
- [linkedin-url2]: https://www.linkedin.com/in/daniel-laloush/
169
- [hireable]: https://cdn.rawgit.com/hiendv/hireable/master/styles/flat/yes.svg
331
+ [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
332
+ [linkedin-url]: https://www.linkedin.com/in/daniel-laloush/
333
+ [hireable-url]: https://www.linkedin.com/in/daniel-laloush/
170
334
  [paypal-url]: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
171
335
  [paypal-donate-code]: https://www.paypal.com/donate?hosted_button_id=QKZFZAMQNC8JL
172
- [hireable-url]: https://www.linkedin.com/in/daniel-laloush/
173
336
  [donate]: https://img.shields.io/badge/Donate-PayPal-blue.svg
337
+ [hireable]: https://cdn.rawgit.com/hiendv/hireable/master/styles/flat/yes.svg
data/Rakefile CHANGED
@@ -1,20 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'rubocop/rake_task'
3
+ require "bundler/gem_tasks"
6
4
 
7
- RSpec::Core::RakeTask.new(:spec)
5
+ require "rubocop/rake_task"
8
6
 
9
7
  RuboCop::RakeTask.new
10
8
 
11
9
  task default: %i[spec rubocop]
12
10
 
13
- desc 'Generate documentation for PgRls.'
14
- Rake::RDocTask.new(:rdoc) do |rdoc|
15
- rdoc.rdoc_dir = 'rdoc'
16
- rdoc.title = 'PgRls'
17
- rdoc.options << '--line-numbers' << '--inline-source'
18
- rdoc.rdoc_files.include('README.md')
19
- rdoc.rdoc_files.include('lib/**/*.rb')
20
- end
11
+ require_relative "test/dummy/config/application"
12
+
13
+ Rails.application.load_tasks
data/Steepfile ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ D = Steep::Diagnostic
4
+
5
+ target :pg_rls do
6
+ signature "sig"
7
+
8
+ check "lib"
9
+ ignore "lib/pg_rls/active_record/test_databases.rb"
10
+
11
+ # library "pathname" # Standard libraries
12
+ # library "strong_json" # Gems
13
+
14
+ # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
15
+ configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
16
+ # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
17
+ # configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
18
+ configure_code_diagnostics do |hash| # You can setup everything yourself
19
+ hash[D::Ruby::UnexpectedSuper] = :hint
20
+ end
21
+ end
22
+
23
+ # target :test do
24
+ # signature "sig", "sig-private"
25
+ #
26
+ # check "test"
27
+ #
28
+ # # library "pathname" # Standard libraries
29
+ # end
data/UPGRADE.md ADDED
@@ -0,0 +1,106 @@
1
+
2
+ # Upgrade Guide
3
+
4
+ ## Upgrading from 0.2 to 1.0.1
5
+
6
+ ### 1. Switch from `admin_execute` to Native Sharding
7
+
8
+ We are no longer using the `admin_execute` methods to handle admin connections. Instead, Rails' native sharding system is used for handling admin connections.
9
+
10
+ #### Steps
11
+
12
+ - **Remove `admin_execute` methods** from your codebase, as they are no longer needed.
13
+ - **Modify your `database.yml`** to leverage Rails' native sharding system.
14
+
15
+ ### 2. `database.yml` Configuration Changes
16
+
17
+ You must now configure sharding in `database.yml` using the following pattern:
18
+
19
+ - The first shard should be named `primary` and use the `PgRls.username` and password for regular operations.
20
+ - The second shard should be named `admin` to handle administrative connections.
21
+
22
+ #### Example `database.yml` for **Development**
23
+
24
+ NOTE: Both shards connects to the same database
25
+
26
+ ```yaml
27
+ development:
28
+ primary:
29
+ database: some_database_primary
30
+ adapter: postgresql
31
+ username: <%= ENV['PG_RLS_USERNAME'] %>
32
+ password: <%= ENV['PG_RLS_PASSWORD'] %>
33
+ admin:
34
+ database: some_database_primary
35
+ adapter: postgresql
36
+ username: <%= ENV['USERNAME'] %>
37
+ password: <%= ENV['PASSWORD'] %>
38
+ ```
39
+
40
+ **Recommendation**: In production, it is recommended to configure only one user per server to optimize query performance.
41
+
42
+ ### 3. **Assign Users to the `pg_rls` Group**
43
+
44
+ It is **mandatory** that all users of the `pg_rls` gem be assigned to the `pg_rls` group. This ensures the gem functions correctly and enforces security and access control.
45
+
46
+ #### Steps
47
+
48
+ - Assign each relevant user to the `pg_rls` group by running:
49
+
50
+ ```sql
51
+ GRANT pg_rls TO your_user;
52
+ ```
53
+
54
+ - You can check if the user is already part of the group with the following query:
55
+
56
+ ```sql
57
+ SELECT rolname
58
+ FROM pg_auth_members
59
+ JOIN pg_roles ON pg_roles.oid = pg_auth_members.member
60
+ WHERE rolname = 'your_user'
61
+ AND roleid = (SELECT oid FROM pg_roles WHERE rolname = 'pg_rls');
62
+ ```
63
+
64
+ ### 4. Boot Protection with Pre-Checks
65
+
66
+ There is now a pre-check method that will **not allow your server to boot** if the following conditions are not met:
67
+
68
+ - The user is assigned to the `pg_rls` group.
69
+ - The `database.yml` is properly configured with the shards (`primary` and `admin`).
70
+
71
+ Ensure these conditions are satisfied before attempting to start your server.
72
+
73
+ ### 5. Optimizing Query Performance
74
+
75
+ For optimal performance in production:
76
+
77
+ - It is **recommended to use only one configuration** (i.e., one user) per server.
78
+ - If necessary, use two machines for processes that require deep analysis or sudo powers. However, note that multitenancy may not provide significant benefits if an admin has access to everything.
79
+
80
+ ### 6. Role and Privilege Checks
81
+
82
+ Ensure that proper role and privilege checks are added to your codebase using the `pg_class`, `pg_roles`, and `pg_namespace` tables for table and schema-specific privileges. You can ensure users have proper access by querying and validating against these PostgreSQL system tables.
83
+
84
+ #### Example Check for Role Existence
85
+
86
+ ```ruby
87
+ check_rls_user_privileges!(role_name, schema)
88
+ ```
89
+
90
+ #### Example Grant Privileges for Role
91
+
92
+ ```ruby
93
+ # Make sure rls_group role is created before running this command
94
+ grant_rls_user_privileges(schema)
95
+ ```
96
+
97
+ #### Example of Creating A RLS Group and User
98
+
99
+ ```ruby
100
+ create_rls_role(name, password)
101
+ # This creates a role named rls_group. Additionally, it creates a user with the
102
+ # name and password you've passed to this method and assigns that new user
103
+ # to the rls_group.
104
+ ```
105
+
106
+ Make sure to follow the updated security practices for checking user access and permissions within the `pg_rls` framework.
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file of the PgRls module provides methods to interact with the admin shard
4
+ module PgRls
5
+ # The Admin class provides the admin_execute method to execute
6
+ class Admin
7
+ def self.execute(sql = nil)
8
+ PgRls::Record.connected_to(shard: :admin) do
9
+ return yield.presence if block_given?
10
+
11
+ PgRls::Record.connection.execute(sql).presence
12
+ end
13
+ end
14
+ end
15
+
16
+ # Alias for the Admin.execute method
17
+ def self.admin_execute(sql = nil, &block)
18
+ Deprecation.warn(
19
+ "This method is deprecated and will be removed in future versions. " \
20
+ "please use PgRls::Admin.execute instead."
21
+ )
22
+ Admin.execute(sql, &block)
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Current Tenant State
5
+ class Current < ::ActiveSupport::CurrentAttributes
6
+ attribute(*PgRls.current_attributes.dup.push(:tenant, :tenant_history))
7
+
8
+ PgRls.current_attributes.each do |attribute|
9
+ define_method(attribute) do
10
+ @attributes[attribute] ||= fetch_attribute(attribute)
11
+ end
12
+ end
13
+
14
+ def fetch_attribute(attribute)
15
+ klass_name = attribute.to_s.gsub("__", "/").classify
16
+
17
+ send(:"#{attribute}=", klass_name.constantize.first)
18
+ end
19
+
20
+ def tenant=(tenant)
21
+ add_tenant_to_history
22
+ super
23
+ tenant&.set_rls
24
+ end
25
+
26
+ def reset
27
+ history = Array @attributes[:tenant_history]
28
+ super
29
+ @attributes[:tenant_history] = history
30
+ restore_most_recent_tenant
31
+ @attributes
32
+ end
33
+
34
+ private
35
+
36
+ def add_tenant_to_history
37
+ @attributes[:tenant_history] ||= []
38
+ @attributes[:tenant_history] << @attributes[:tenant] unless @attributes[:tenant].nil?
39
+ end
40
+
41
+ def restore_most_recent_tenant
42
+ @attributes[:tenant] = @attributes[:tenant_history].pop
43
+ return PgRls::Tenant.reset_rls_used_connections if @attributes[:tenant].nil?
44
+
45
+ @attributes[:tenant].set_rls
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Row Level Security for PostgreSQL
4
+ module PgRls
5
+ # Base class for all models that should be protected by RLS
6
+ class Record < PgRls.abstract_base_record_class.constantize
7
+ self.abstract_class = true
8
+
9
+ self.ignored_columns += %w[tenant_id]
10
+
11
+ connects_to(**PgRls.connects_to)
12
+ end
13
+ end