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.
- 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/templates/app/models/abstract_base_class.rb.tt +7 -0
- data/lib/generators/pg_rls/templates/app/models/model.rb.tt +22 -0
- 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 -12
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -300
- data/bin/console +0 -15
- data/bin/setup +0 -8
data/README.md
CHANGED
@@ -1,24 +1,26 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
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
|
-
[![
|
13
|
-
[![
|
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
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
63
|
+
- Ruby (~> 3.0)
|
64
|
+
- ActiveRecord (~> 7.0)
|
65
|
+
- PostgreSQL (> 9.0)
|
66
|
+
- Warden
|
67
|
+
- pg (~> 1.2)
|
48
68
|
|
49
|
-
###
|
69
|
+
### Installation
|
50
70
|
|
51
|
-
|
52
|
-
### Installing
|
71
|
+
1. Add this line to your application's Gemfile:
|
53
72
|
|
54
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
109
|
+
#### For flexible production configurations, you can use an environment variable to set the rls_mode
|
63
110
|
|
64
|
-
|
111
|
+
```ruby
|
112
|
+
production:
|
113
|
+
<<: *default
|
114
|
+
database: prod_db
|
115
|
+
rls_mode: <%= ENV.fetch('RLS_MODE', 'dual') %>
|
116
|
+
```
|
65
117
|
|
66
|
-
|
118
|
+
### How It Works
|
67
119
|
|
68
|
-
|
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
|
-
|
71
|
-
|
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
|
-
|
147
|
+
This ensures that your custom attributes are loaded and available across requests.
|
77
148
|
|
78
|
-
|
79
|
-
|
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
|
159
|
+
You can dynamically access `Organization::Branch.first` using:
|
160
|
+
|
84
161
|
```ruby
|
85
|
-
PgRls::
|
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
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
##
|
306
|
+
## Acknowledgements
|
142
307
|
|
143
|
-
|
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
|
-
|
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-
|
168
|
-
[linkedin-
|
169
|
-
[hireable]: https://
|
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
|
4
|
-
require 'rspec/core/rake_task'
|
5
|
-
require 'rubocop/rake_task'
|
3
|
+
require "bundler/gem_tasks"
|
6
4
|
|
7
|
-
|
5
|
+
require "rubocop/rake_task"
|
8
6
|
|
9
7
|
RuboCop::RakeTask.new
|
10
8
|
|
11
9
|
task default: %i[spec rubocop]
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|