pg_rls 0.0.1.alpha → 0.0.1.2

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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -0
  3. data/Gemfile.lock +3 -2
  4. data/README.md +117 -12
  5. data/Rakefile +9 -0
  6. data/lib/generators/pg_rls/active_record/active_record_generator.rb +111 -0
  7. data/lib/generators/pg_rls/active_record/templates/abstract_base_class.rb.tt +9 -0
  8. data/lib/generators/pg_rls/active_record/templates/convert_migration.rb.tt +11 -0
  9. data/lib/generators/pg_rls/active_record/templates/convert_migration_backport.rb.tt +14 -0
  10. data/lib/generators/pg_rls/active_record/templates/init_convert_migration.rb.tt +11 -0
  11. data/lib/generators/pg_rls/active_record/templates/init_migration.rb.tt +25 -0
  12. data/lib/generators/pg_rls/active_record/templates/init_model.rb.tt +27 -0
  13. data/lib/generators/pg_rls/active_record/templates/migration.rb.tt +17 -0
  14. data/lib/generators/pg_rls/active_record/templates/model.rb.tt +24 -0
  15. data/lib/generators/pg_rls/base.rb +36 -0
  16. data/lib/generators/pg_rls/install_generator.rb +83 -0
  17. data/lib/generators/pg_rls/pg_rls_generator.rb +12 -0
  18. data/lib/generators/pg_rls.rb +19 -0
  19. data/lib/generators/templates/README +19 -0
  20. data/lib/generators/templates/pg_rls.rb.tt +12 -0
  21. data/lib/pg_rls/database/prepared.rb +23 -0
  22. data/lib/pg_rls/multi_tenancy.rb +20 -0
  23. data/lib/pg_rls/schema/down_statements.rb +10 -4
  24. data/lib/pg_rls/schema/statements.rb +29 -1
  25. data/lib/pg_rls/schema/up_statements.rb +7 -4
  26. data/lib/pg_rls/secure_connection.rb +26 -0
  27. data/lib/pg_rls/tenant.rb +41 -0
  28. data/lib/pg_rls/version.rb +1 -1
  29. data/lib/pg_rls.rb +48 -6
  30. metadata +44 -9
  31. data/lib/pg_rls/tenant/tenant.rb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ed56d6024c7658ebdf54879615273e29b9337f178b27a56352d2b3ba831b53d
4
- data.tar.gz: 10ed3f8ae3350ef016a0138fcee0dfaa875c93f399ec3bca3a258f3f6f8d1639
3
+ metadata.gz: d7e3e80029653fbeffba3ab6ab495273c5eabfe8a8f7ca2250b55d58621652d1
4
+ data.tar.gz: c789d0607cff7d73be8eba6205258b85dba70917a28093c800a70c7ca3846c39
5
5
  SHA512:
6
- metadata.gz: 7bc62b85e6065bcfd687de04809efffd295a47b66f9ef72afa8f236a7b2750888ebc55df8e8b371c674a879d18204338328c5684835ca6fb09bb2d6456985302
7
- data.tar.gz: 28ab96d49303876ed85573439140695db0adf475f4b2f2253d99c5c99f6b2069eda679c10b87c9275bd7e40b8cd2c4981cf5e6614b61cb8417f9e9ec431b7c23
6
+ metadata.gz: c6c19274c02a6de2d07a48786b5bfbf80e7ba2107270b88930f212ab8c0798f1128be85e4f2d300cce8e6aff70b8978dd5e5ea6dc544f25b92faa1896cc8585d
7
+ data.tar.gz: 37be65bcfcc8af3cb23a4c5e6ed5cc761bba6225f1853de6f7692f5e895b74135dacf01984127a54a569966127c9ff79b8b66531aa05ca3bb724dac06ac3baa0
data/.rubocop.yml CHANGED
@@ -1,5 +1,7 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 3.0
3
3
  NewCops: enable
4
+ Exclude:
5
+ - lib/generators/pg_rls/active_record/templates/migration.rb
4
6
  Layout/LineLength:
5
7
  Max: 120
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pg_rls (0.0.1.alpha)
4
+ pg_rls (0.0.1)
5
+ bundler (>= 2.2.10)
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
@@ -82,7 +83,7 @@ GEM
82
83
  mini_mime (>= 0.1.1)
83
84
  marcel (1.0.2)
84
85
  method_source (1.0.0)
85
- mini_mime (1.1.1)
86
+ mini_mime (1.1.2)
86
87
  minitest (5.14.4)
87
88
  nio4r (2.5.8)
88
89
  nokogiri (1.12.5-x86_64-linux)
data/README.md CHANGED
@@ -1,10 +1,48 @@
1
- # PgRls
2
-
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/pg_rls`. To experiment with that code, run `bin/console` for an interactive prompt.
4
-
5
- TODO: Delete this and the text above, and describe your gem
6
-
7
- ## Installation
1
+ [![Contributors][contributors-shield]][contributors-url]
2
+ [![Forks][forks-shield]][forks-url]
3
+ [![Stargazers][stars-shield]][stars-url]
4
+ [![Issues][issues-shield]][issues-url]
5
+ [![LinkedIn][linkedin-shield2]][linkedin-url2]
6
+ [![Hireable][hireable]][hireable-url]
7
+ [![Donate][donate]][paypal-donate-code]
8
+
9
+ <!-- PROJECT LOGO -->
10
+ <br />
11
+ <p align="center">
12
+ <h1 align="center">PgRls<h2 align="center">PostgreSQL Row Level Security<br />The Rails right way to do multitenancy</h2></h1>
13
+
14
+ <p align="center">
15
+ <br />
16
+ <a href="https://github.com/Dandush03/pg_rls/wiki"><strong>Explore the docs »</strong></a>
17
+ <br />
18
+ <br />
19
+ <a href="https://github.com/Dandush03/pg_rls/issues">Report Bug</a>
20
+ ·
21
+ <a href="https://github.com/Dandush03/pg_rls/issues">Request Feature</a>
22
+ ·
23
+ <a href="https://github.com/Dandush03/pg_rls">API Repo</a>
24
+ </p>
25
+
26
+ </p>
27
+
28
+ ### Table of Contents
29
+ * [Required Installations](#required-Installations)
30
+ * [Installing](#installing)
31
+ * [Instructions](#instructions)
32
+ * [Testing](#Testing)
33
+ * [Development](#testing)
34
+ * [Contact](#contact)
35
+ * [Contributing](#contributing)
36
+ * [License](#license)
37
+ * [Code of Conduct](#Code-of-Conduct)
38
+ * [Show your support](#Show-your-support)
39
+
40
+ ### 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.
41
+
42
+ ### This gem will integrate PostgreSQL RLS to help you develop a great multitenancy application.
43
+
44
+ ## Required Installation
45
+ ### Installing
8
46
 
9
47
  Add this line to your application's Gemfile:
10
48
 
@@ -16,14 +54,51 @@ And then execute:
16
54
 
17
55
  $ bundle install
18
56
 
19
- Or install it yourself as:
57
+ Or install it yourself with:
20
58
 
21
59
  $ gem install pg_rls
22
60
 
23
- ## Usage
61
+ ### Instructions
62
+
63
+ ```bash
64
+ rails generate pg_rls:install company #=> where company eq tenant model name
65
+ ```
66
+ You can change company to anything you'd like, for example, `tenant`
67
+ This will generate the model and inject all the required code
68
+
69
+ For any new model that needs to be under rls, you can generate it by writing
70
+
71
+ ```bash
72
+ rails generate pg_rls user #=> where user eq model name
73
+ ```
74
+ and it will generate all the necesary information for you.
75
+
76
+ You can swtich to another tenant by using
77
+ ```ruby
78
+ PgRls::Tenant.switch :app #=> where app eq tenant name
79
+ ```
80
+ 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`
81
+ ### Testing
82
+
83
+ 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:
24
84
 
25
- TODO: Write usage instructions here
85
+ ```ruby
86
+ # spec/rails_helper.rb
87
+
88
+ ...
89
+ # some database cleaning strategy
26
90
 
91
+ config.before(:suite) do
92
+ # Create A Default Tenant and Grant Test User Credentials
93
+ PgRls::Database::Prepared.grant_user_credentials
94
+ # Create the tenant which in this example is company and we are using FactoryBot
95
+ FactoryBot.create(:company, subdomain: 'app')
96
+ # In this default case our initializer is set to search by subdomain so will use it
97
+ PgRls::Tenant.switch :app
98
+ end
99
+
100
+ ...
101
+ ```
27
102
  ## Development
28
103
 
29
104
  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.
@@ -32,7 +107,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
107
 
33
108
  ## Contributing
34
109
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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/[USERNAME]/pg_rls/blob/master/CODE_OF_CONDUCT.md).
110
+ 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).
36
111
 
37
112
  ## License
38
113
 
@@ -40,4 +115,34 @@ The gem is available as open source under the terms of the [MIT License](https:/
40
115
 
41
116
  ## Code of Conduct
42
117
 
43
- 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/[USERNAME]/pg_rls/blob/master/CODE_OF_CONDUCT.md).
118
+ 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).
119
+
120
+ ## Note
121
+ Currently we only support subdomain as a searcher but will soon integrate slug/domain and cookies support
122
+
123
+ ## Show your support
124
+
125
+ Give a ⭐️ if you like this project!
126
+
127
+ If this project help you reduce time to develop, you can give me a cup of coffee :)
128
+
129
+ [![paypal][paypal-url]][paypal-donate-code]
130
+
131
+ <!-- MARKDOWN LINKS & IMAGES -->
132
+ [contributors-shield]: https://img.shields.io/github/contributors/Dandush03/React-Calculator.svg?style=flat-square
133
+ [contributors-url]: https://github.com/Dandush03/pg_rls/graphs/contributors
134
+ [forks-shield]: https://img.shields.io/github/forks/Dandush03/pg_rls.svg?style=flat-square
135
+ [forks-url]: https://github.com/Dandush03/pg_rls/network/members
136
+ [stars-shield]: https://img.shields.io/github/stars/Dandush03/pg_rls.svg?style=flat-square
137
+ [stars-url]: https://github.com/Dandush03/pg_rls/stargazers
138
+ [issues-shield]: https://img.shields.io/github/issues/Dandush03/pg_rls.svg?style=flat-square
139
+ [issues-url]: https://github.com/Dandush03/pg_rls/issues
140
+ [license-shield]: https://img.shields.io/github/license/Dandush03/pg_rls.svg?style=flat-square
141
+ [license-url]: https://github.com/Dandush03/pg_rls/blob/master/LICENSE.txt
142
+ [linkedin-shield2]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
143
+ [linkedin-url2]: https://www.linkedin.com/in/daniel-laloush/
144
+ [hireable]: https://cdn.rawgit.com/hiendv/hireable/master/styles/flat/yes.svg
145
+ [paypal-url]: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif
146
+ [paypal-donate-code]: https://www.paypal.com/donate?hosted_button_id=QKZFZAMQNC8JL
147
+ [hireable-url]: https://www.linkedin.com/in/daniel-laloush/
148
+ [donate]: https://img.shields.io/badge/Donate-PayPal-blue.svg
data/Rakefile CHANGED
@@ -10,3 +10,12 @@ require 'rubocop/rake_task'
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ desc 'Generate documentation for PgRls.'
15
+ Rake::RDocTask.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'PgRls'
18
+ rdoc.options << '--line-numbers' << '--inline-source'
19
+ rdoc.rdoc_files.include('README.md')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/active_record/model/model_generator'
4
+ require File.join(File.dirname(__FILE__), '../base')
5
+
6
+ module PgRls
7
+ module Generators
8
+ # Active Record Generator
9
+ class ActiveRecordGenerator < ::ActiveRecord::Generators::ModelGenerator
10
+ include ::PgRls::Base
11
+
12
+ source_root File.expand_path('./templates', __dir__)
13
+
14
+ def check_class_collision; end
15
+
16
+ def create_migration_file; end
17
+
18
+ def migration_exist?
19
+ @migration_exist ||= Dir.glob("#{migration_path}/*create_#{table_name}.rb").present?
20
+ end
21
+
22
+ def create_tenant_migration_file
23
+ return if migration_exist?
24
+
25
+ migration_template create_migration_template_path,
26
+ "#{migration_path}/#{create_file_sub_name}_#{table_name}.rb",
27
+ migration_version: migration_version
28
+ end
29
+
30
+ def convert_tenant_migration_file
31
+ return unless migration_exist?
32
+
33
+ migration_template convert_migration_template_path,
34
+ "#{migration_path}/#{convert_file_sub_name}_#{table_name}.rb",
35
+ migration_version: migration_version
36
+
37
+ return if installation_in_progress?
38
+
39
+ migration_template 'convert_migration_backport.rb.tt',
40
+ "#{migration_path}/pg_rls_backport_#{table_name}.rb",
41
+ migration_version: migration_version
42
+ end
43
+
44
+ def create_model_file
45
+ return if migration_exist?
46
+
47
+ generate_abstract_class if database && !parent
48
+
49
+ template model_template_path, model_file
50
+ end
51
+
52
+ def inject_method_to_model
53
+ return unless installation_in_progress?
54
+
55
+ gsub_file(model_file, /Class #{class_name} < #{parent_class_name.classify}/mi) do |match|
56
+ "#{match}\n def self.current\n PgRls::Tenant.fetch\n end\n"
57
+ end
58
+ end
59
+
60
+ def model_file
61
+ File.join('app/models', class_path, "#{file_name}.rb")
62
+ end
63
+
64
+ def create_migration_template_path
65
+ return 'init_migration.rb.tt' if installation_in_progress?
66
+
67
+ 'migration.rb.tt'
68
+ end
69
+
70
+ def convert_migration_template_path
71
+ return 'init_convert_migration.rb.tt' if installation_in_progress?
72
+
73
+ 'convert_migration.rb.tt'
74
+ end
75
+
76
+ def model_template_path
77
+ return 'init_model.rb.tt' if installation_in_progress?
78
+
79
+ 'model.rb.tt'
80
+ end
81
+
82
+ def create_file_sub_name
83
+ return 'pg_rls_create_tenant' if installation_in_progress?
84
+
85
+ 'pg_rls_create'
86
+ end
87
+
88
+ def convert_file_sub_name
89
+ return 'pg_rls_convert_tenant' if installation_in_progress?
90
+
91
+ 'pg_rls_convert'
92
+ end
93
+
94
+ def installation_in_progress?
95
+ shell.base.class.name.include?('Install')
96
+ end
97
+
98
+ def migration_version
99
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
100
+ end
101
+
102
+ def migration_path
103
+ db_migrate_path
104
+ end
105
+
106
+ protected
107
+
108
+ def migration_action() = 'add'
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= abstract_class_name %> < ApplicationRecord
5
+ self.abstract_class = true
6
+
7
+ connects_to database: { <%= ActiveRecord.writing_role %>: :<%= database -%> }
8
+ end
9
+ <% end -%>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PgRlsConvert<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ convert_to_rls_table :<%= table_name %>
6
+ end
7
+
8
+ def down
9
+ revert_rls_table :<%= table_name %>
10
+ end
11
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PgRlsBackport<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ PgRls.establish_default_connection
6
+
7
+ # Suggested Code:
8
+ # PgRls.all_tenants do |tenant|
9
+ # tenant.<%= table_name %>.in_batches(of: 100) do |<%= table_name %>|
10
+ # <%= table_name %>.each { |<%= table_name.singularize %>| <%= table_name.singularize %>.update_attribute('tenant_id', tenant.tenant_id) }
11
+ # end
12
+ # end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PgRlsConvertTenant<%= PgRls.table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ convert_to_rls_tenant_table :<%= table_name %>
6
+ end
7
+
8
+ def down
9
+ revert_rls_tenant_table :<%= table_name %>
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PgRlsCreateTenant<%= PgRls.table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ create_rls_tenant_table :<%= table_name %>, id: :uuid do |t|
6
+ t.string :name
7
+ t.string :logo
8
+
9
+ t.string :identification
10
+ t.string :subdomain
11
+ t.string :domain
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ add_index :<%= table_name %>, :name, unique: true
17
+ add_index :<%= table_name %>, :identification, unique: true
18
+ add_index :<%= table_name %>, :domain, unique: true
19
+ add_index :<%= table_name %>, :subdomain, unique: true
20
+ end
21
+
22
+ def down
23
+ drop_rls_tenant_table :<%= table_name %>
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= PgRls.class_name.camelize %> < <%= parent_class_name.classify %>
5
+ def self.current
6
+ PgRls::Tenant.fetch
7
+ end
8
+ <% attributes.select(&:reference?).each do |attribute| -%>
9
+ belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
10
+ <% end -%>
11
+ <% attributes.select(&:rich_text?).each do |attribute| -%>
12
+ has_rich_text :<%= attribute.name %>
13
+ <% end -%>
14
+ <% attributes.select(&:attachment?).each do |attribute| -%>
15
+ has_one_attached :<%= attribute.name %>
16
+ <% end -%>
17
+ <% attributes.select(&:attachments?).each do |attribute| -%>
18
+ has_many_attached :<%= attribute.name %>
19
+ <% end -%>
20
+ <% attributes.select(&:token?).each do |attribute| -%>
21
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
22
+ <% end -%>
23
+ <% if attributes.any?(&:password_digest?) -%>
24
+ has_secure_password
25
+ <% end -%>
26
+ end
27
+ <% end -%>
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PgRlsCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>
4
+ def up
5
+ create_rls_table :<%= table_name %><%= primary_key_type %> do |t|
6
+ <% attributes.each do |attribute| -%>
7
+ t.<%= attribute.type %> :<%= attribute.name %>
8
+ <% end -%>
9
+
10
+ t.timestamps null: false
11
+ end
12
+ end
13
+
14
+ def down
15
+ drop_rls_table :<%= table_name %>
16
+ end
17
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %> < <%= parent_class_name.classify %>
5
+ <% attributes.select(&:reference?).each do |attribute| -%>
6
+ belongs_to :<%= attribute.name %><%= ", polymorphic: true" if attribute.polymorphic? %>
7
+ <% end -%>
8
+ <% attributes.select(&:rich_text?).each do |attribute| -%>
9
+ has_rich_text :<%= attribute.name %>
10
+ <% end -%>
11
+ <% attributes.select(&:attachment?).each do |attribute| -%>
12
+ has_one_attached :<%= attribute.name %>
13
+ <% end -%>
14
+ <% attributes.select(&:attachments?).each do |attribute| -%>
15
+ has_many_attached :<%= attribute.name %>
16
+ <% end -%>
17
+ <% attributes.select(&:token?).each do |attribute| -%>
18
+ has_secure_token<% if attribute.name != "token" %> :<%= attribute.name %><% end %>
19
+ <% end -%>
20
+ <% if attributes.any?(&:password_digest?) -%>
21
+ has_secure_password
22
+ <% end -%>
23
+ end
24
+ <% end -%>
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Main Definition for Generator
5
+ module Base
6
+ protected
7
+
8
+ def nested_parent_name
9
+ @class_path.join('/')
10
+ end
11
+
12
+ def nested_parent_id
13
+ "#{nested_parent_name}_id"
14
+ end
15
+
16
+ def nested_parent_class_name
17
+ nested_parent_name.classify
18
+ end
19
+
20
+ def plural_nested_parent_name
21
+ nested_parent_name.pluralize
22
+ end
23
+
24
+ def class_path
25
+ []
26
+ end
27
+
28
+ def regular_class_path
29
+ []
30
+ end
31
+
32
+ def controller_class_path
33
+ []
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/base'
4
+ require 'securerandom'
5
+
6
+ module PgRls
7
+ module Generators
8
+ MissingORMError = Class.new(Thor::Error)
9
+ # Installer Generator
10
+ class InstallGenerator < Rails::Generators::Base
11
+ def initialize(*args)
12
+ tenant_model_or_table = args.first
13
+ if tenant_model_or_table.present?
14
+ PgRls.table_name = tenant_model_or_table.first.pluralize
15
+ PgRls.class_name = tenant_model_or_table.first.singularize
16
+ end
17
+ super
18
+ end
19
+ APPLICATION_RECORD_LINE = 'class ApplicationRecord < ActiveRecord::Base'
20
+ APPLICATION_RECORD_PATH = 'app/models/application_record.rb'
21
+ APPLICATION_CONTROLLER_LINE = 'class ApplicationController < ActionController::Base'
22
+ APPLICATION_CONTROLLER_PATH = 'app/controllers/application_controller.rb'
23
+
24
+ source_root File.expand_path('../templates', __dir__)
25
+
26
+ desc 'Creates a PgRls initializer and copy locale files to your application.'
27
+
28
+ hook_for :orm, required: true
29
+
30
+ def orm_error_message
31
+ <<-ERROR.strip_heredoc
32
+ An ORM must be set to install PgRls in your application.
33
+ Be sure to have an ORM like Active Record or loaded in your
34
+ app or configure your own at `config/application.rb`.
35
+ config.generators do |g|
36
+ g.orm :your_orm_gem
37
+ end
38
+ ERROR
39
+ end
40
+
41
+ def copy_initializer
42
+ raise MissingORMError, orm_error_message unless options[:orm]
43
+
44
+ inject_include_to_application_record
45
+ inject_include_to_application_controller
46
+ template 'pg_rls.rb.tt', 'config/initializers/pg_rls.rb'
47
+ end
48
+
49
+ def inject_include_to_application_record
50
+ return if aplication_record_already_included?
51
+
52
+ gsub_file(APPLICATION_RECORD_PATH, /(#{Regexp.escape(APPLICATION_RECORD_LINE)})/mi) do |match|
53
+ "#{match}\n include PgRls::SecureConnection\n"
54
+ end
55
+ end
56
+
57
+ def inject_include_to_application_controller
58
+ return if aplication_controller_already_included?
59
+
60
+ gsub_file(APPLICATION_CONTROLLER_PATH, /(#{Regexp.escape(APPLICATION_CONTROLLER_LINE)})/mi) do |match|
61
+ "#{match}\n include PgRls::MultiTenancy\n"
62
+ end
63
+ end
64
+
65
+ def aplication_controller_already_included?
66
+ File.readlines(APPLICATION_CONTROLLER_PATH).grep(/include PgRls::MultiTenancy/).any?
67
+ end
68
+
69
+ def aplication_record_already_included?
70
+ File.readlines(APPLICATION_RECORD_PATH).grep(/include PgRls::SecureConnection/).any?
71
+ end
72
+
73
+ def initialize_error_text
74
+ <<-ERROR.strip_heredoc
75
+ ERROR
76
+ end
77
+
78
+ def show_readme
79
+ readme 'README' if behavior == :invoke
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative File.join(File.dirname(__FILE__), 'active_record/active_record_generator')
4
+
5
+ module PgRls
6
+ module Generators
7
+ class PgRlsGenerator < ::Rails::Generators::NamedBase
8
+ # override ModelGenerator
9
+ hook_for :orm, required: true
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators/named_base'
4
+ require 'rails/generators/active_model'
5
+ require 'rails/generators/active_record/migration'
6
+ require 'active_record'
7
+
8
+ module PgRls
9
+ module Generators # :nodoc:
10
+ class PgRlsGenerator < Rails::Generators::NamedBase # :nodoc:
11
+ include PgRls::Generators::Migration
12
+
13
+ # Set the current directory as base for the inherited generators.
14
+ def self.base_root
15
+ __dir__
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ README
2
+ ===============================================================================
3
+ WARNING!!
4
+
5
+ Once you remove a tenant all of his data would be removed as well
6
+
7
+ PgRls::SecureConnection was included to your ApplicationRecord do not remove it
8
+
9
+ ===============================================================================
10
+
11
+ to generate secure model run
12
+
13
+ rails g pg_rls model_name
14
+
15
+ or
16
+
17
+ rails generate pg_rls model_name
18
+
19
+ ===============================================================================
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pg_rls'
4
+
5
+ PgRls.setup do |config|
6
+ ActiveRecord::ConnectionAdapters::AbstractAdapter.include PgRls::Schema::Statements
7
+
8
+ # Do not remove this value after initialization
9
+ config.class_name = :<%= PgRls.class_name %>
10
+ config.table_name = :<%= PgRls.table_name %>
11
+ config.search_methods = <%= PgRls.search_methods %>
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ module Database
5
+ # Prepare database for test unit
6
+ module Prepared
7
+ class << self
8
+ def grant_user_credentials(name: PgRls::SECURE_USERNAME)
9
+ return unless Rails.env.test?
10
+
11
+ PgRls.execute <<-SQL
12
+ GRANT USAGE, SELECT
13
+ ON ALL SEQUENCES IN SCHEMA public
14
+ TO #{name};
15
+ GRANT SELECT, INSERT, UPDATE, DELETE
16
+ ON ALL TABLES IN SCHEMA public
17
+ TO #{name};
18
+ SQL
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Ensure Connection is with App_use
5
+ module MultiTenancy
6
+ def self.included(base)
7
+ base.class_eval do
8
+ before_action :switch_tenant
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def switch_tenant
15
+ Tenant.switch request.subdomain
16
+ rescue NoMethodError
17
+ redirect_to '/'
18
+ end
19
+ end
20
+ end
@@ -4,8 +4,14 @@ module PgRls
4
4
  module Schema
5
5
  # Down Schema Statements
6
6
  module DownStatements
7
- def drop_rls_user(name = :app_user)
8
- ActiveRecord::Migration.execute "DROP USER #{name};"
7
+ def drop_rls_user
8
+ ActiveRecord::Migration.execute <<~SQL
9
+ DROP OWNED BY #{PgRls::SECURE_USERNAME};
10
+ REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM #{PgRls::SECURE_USERNAME};
11
+ REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM #{PgRls::SECURE_USERNAME};
12
+ REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM #{PgRls::SECURE_USERNAME};
13
+ DROP USER #{PgRls::SECURE_USERNAME};
14
+ SQL
9
15
  end
10
16
 
11
17
  def drop_rls_blocking_function
@@ -37,9 +43,9 @@ module PgRls
37
43
  SQL
38
44
  end
39
45
 
40
- def drop_rls_policy(table_name, user = :app_user)
46
+ def drop_rls_policy(table_name)
41
47
  ActiveRecord::Migration.execute <<-SQL
42
- DROP POLICY #{table_name}_#{user} ON #{table_name};
48
+ DROP POLICY #{table_name}_#{PgRls::SECURE_USERNAME} ON #{table_name};
43
49
  ALTER TABLE #{table_name} DISABLE ROW LEVEL SECURITY;
44
50
  SQL
45
51
  end
@@ -27,11 +27,11 @@ module PgRls
27
27
  end
28
28
 
29
29
  def drop_rls_tenant_table(table_name)
30
- drop_rls_user
31
30
  drop_rls_setter_function
32
31
  detach_blocking_function(table_name)
33
32
  drop_table(table_name)
34
33
  drop_rls_blocking_function
34
+ drop_rls_user
35
35
  end
36
36
 
37
37
  def drop_rls_table(table_name)
@@ -39,6 +39,34 @@ module PgRls
39
39
  drop_rls_policy(table_name)
40
40
  drop_table(table_name)
41
41
  end
42
+
43
+ def convert_to_rls_tenant_table(table_name, **_options)
44
+ create_rls_user(password: PgRls.database_configuration['password'])
45
+ create_rls_setter_function
46
+ create_rls_blocking_function
47
+ add_rls_column_to_tenant_table(table_name)
48
+ append_blocking_function(table_name)
49
+ end
50
+
51
+ def revert_rls_tenant_table(table_name)
52
+ drop_rls_setter_function
53
+ detach_blocking_function(table_name)
54
+ drop_rls_blocking_function
55
+ drop_rls_user
56
+ drop_rls_column(table_name)
57
+ end
58
+
59
+ def convert_to_rls_table(table_name)
60
+ add_rls_column(table_name)
61
+ create_rls_policy(table_name)
62
+ append_trigger_function(table_name)
63
+ end
64
+
65
+ def revert_rls_table(table_name)
66
+ detach_trigger_function(table_name)
67
+ drop_rls_policy(table_name)
68
+ drop_rls_column(table_name)
69
+ end
42
70
  end
43
71
  end
44
72
  end
@@ -4,7 +4,7 @@ module PgRls
4
4
  module Schema
5
5
  # Up Schema Statements
6
6
  module UpStatements
7
- def create_rls_user(name: :app_user, password: 'password')
7
+ def create_rls_user(name: PgRls::SECURE_USERNAME, password: 'password')
8
8
  PgRls.execute <<-SQL
9
9
  DROP ROLE IF EXISTS #{name};
10
10
  CREATE USER #{name} WITH PASSWORD '#{password}';
@@ -12,6 +12,9 @@ module PgRls
12
12
  ALTER DEFAULT PRIVILEGES IN SCHEMA public
13
13
  GRANT SELECT, INSERT, UPDATE, DELETE
14
14
  ON TABLES TO #{name};
15
+ GRANT SELECT, INSERT, UPDATE, DELETE
16
+ ON ALL TABLES IN SCHEMA public#{' '}
17
+ TO #{name};
15
18
  SQL
16
19
  end
17
20
 
@@ -64,14 +67,14 @@ module PgRls
64
67
  ActiveRecord::Migration.execute <<-SQL
65
68
  ALTER TABLE #{table_name}
66
69
  ADD COLUMN IF NOT EXISTS tenant_id uuid,
67
- ADD CONSTRAINT fk_companies
70
+ ADD CONSTRAINT fk_#{PgRls.table_name}
68
71
  FOREIGN KEY (tenant_id)
69
- REFERENCES companies(tenant_id)
72
+ REFERENCES #{PgRls.table_name}(tenant_id)
70
73
  ON DELETE CASCADE;
71
74
  SQL
72
75
  end
73
76
 
74
- def create_rls_policy(table_name, user = :app_user)
77
+ def create_rls_policy(table_name, user = PgRls::SECURE_USERNAME)
75
78
  ActiveRecord::Migration.execute <<-SQL
76
79
  ALTER TABLE #{table_name} ENABLE ROW LEVEL SECURITY;
77
80
  CREATE POLICY #{table_name}_#{user}
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Ensure Connection is with App_use
5
+ module SecureConnection
6
+ def self.included(base)
7
+ base.class_eval do
8
+ after_initialize :establish_secure_connection
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def establish_secure_connection
15
+ return if PgRls.default_connection?
16
+
17
+ return if secure_connection_established?
18
+
19
+ PgRls.establish_new_connection
20
+ end
21
+
22
+ def secure_connection_established?
23
+ PgRls.current_connection_username == PgRls::SECURE_USERNAME
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgRls
4
+ # Tenant Controller
5
+ module Tenant
6
+ class << self
7
+ def switch(resource)
8
+ connection_adapter = PgRls.connection_class
9
+ find_tenant(resource)
10
+ connection_adapter.connection.execute(format('SET rls.tenant_id = %s',
11
+ connection_adapter.connection.quote(tenant.tenant_id)))
12
+ "RLS changed to '#{tenant.name}'"
13
+ rescue StandardError => e
14
+ puts 'connection was not made'
15
+ puts @error || e
16
+ end
17
+
18
+ attr_reader :tenant
19
+
20
+ def fetch
21
+ @fetch ||= tenant.find_by_tenant_id(
22
+ PgRls.connection_class.connection.execute(
23
+ "SELECT current_setting('rls.tenant_id')"
24
+ ).getvalue(0, 0)
25
+ )
26
+ rescue ActiveRecord::StatementInvalid
27
+ 'no tenant is selected'
28
+ end
29
+
30
+ def find_tenant(resource)
31
+ @tenant = nil
32
+
33
+ PgRls.search_methods.each do |method|
34
+ @tenant ||= PgRls.main_model.send("find_by_#{method}", resource)
35
+ rescue NoMethodError => e
36
+ @error = e
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PgRls
4
- VERSION = '0.0.1.alpha'
4
+ VERSION = '0.0.1.2'
5
5
  end
data/lib/pg_rls.rb CHANGED
@@ -3,19 +3,28 @@
3
3
  require 'active_record'
4
4
  require 'forwardable'
5
5
  require_relative 'pg_rls/version'
6
+ require_relative 'pg_rls/database/prepared'
6
7
  require_relative 'pg_rls/schema/statements'
7
- require_relative 'pg_rls/tenant/tenant'
8
+ require_relative 'pg_rls/tenant'
9
+ require_relative 'pg_rls/secure_connection'
10
+ require_relative 'pg_rls/multi_tenancy'
8
11
 
9
12
  # PostgreSQL Row Level Security
10
13
  module PgRls
11
14
  class Error < StandardError; end
15
+ SECURE_USERNAME = "#{Rails.env}_app_user".freeze
12
16
 
13
17
  class << self
14
18
  extend Forwardable
15
19
 
16
- WRITER_METHODS = %i[].freeze
17
- READER_METHODS = %i[connection_class database_configuration execute].freeze
18
- DELEGATORS_METHODS = %i[connection_class database_configuration execute].freeze
20
+ WRITER_METHODS = %i[table_name class_name search_methods].freeze
21
+ READER_METHODS = %i[
22
+ connection_class database_configuration execute table_name class_name search_methods
23
+ ].freeze
24
+ DELEGATORS_METHODS = %i[
25
+ connection_class database_configuration execute table_name search_methods
26
+ class_name all_tenants main_model establish_default_connection
27
+ ].freeze
19
28
 
20
29
  attr_writer(*WRITER_METHODS)
21
30
  attr_reader(*READER_METHODS)
@@ -38,18 +47,51 @@ module PgRls
38
47
 
39
48
  def establish_new_connection
40
49
  connection_class.establish_connection(
41
- connection_class.connection_config.dup.tap { |n| n[:username] = 'app_user' }
50
+ **database_configuration
42
51
  )
43
52
  end
44
53
 
54
+ def establish_default_connection
55
+ @default_connection = true
56
+ end
57
+
58
+ def default_connection?
59
+ @default_connection
60
+ end
61
+
62
+ def main_model
63
+ class_name.to_s.camelize.constantize
64
+ end
65
+
66
+ def all_tenants
67
+ main_model.all.each do |tenant|
68
+ allowed_search_fields = search_methods.map(&:to_s).intersection(main_model.column_names)
69
+ Tenant.switch tenant.send(allowed_search_fields.first)
70
+
71
+ yield(tenant) if block_given?
72
+ end
73
+ end
74
+
75
+ def current_connection_username
76
+ connection_class.connection_db_config.configuration_hash[:username]
77
+ end
78
+
45
79
  def execute(query)
46
80
  @execute = ActiveRecord::Migration.execute(query)
47
81
  end
48
82
 
49
83
  def database_configuration
50
84
  @database_configuration ||= database_connection_file[Rails.env].tap do |config|
51
- config['username'] = 'app_user'
85
+ config['username'] = PgRls::SECURE_USERNAME
52
86
  end
53
87
  end
54
88
  end
89
+ mattr_accessor :table_name
90
+ @@table_name = 'companies'
91
+
92
+ mattr_accessor :class_name
93
+ @@class_name = 'Company'
94
+
95
+ mattr_accessor :search_methods
96
+ @@search_methods = %i[subdomain id tenant_id]
55
97
  end
metadata CHANGED
@@ -1,16 +1,32 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pg_rls
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1.alpha
4
+ version: 0.0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Laloush
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-10-05 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Write a longer description or delete this line.
11
+ date: 2022-01-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 2.2.10
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 2.2.10
27
+ description: |2
28
+ This gem will help you to integrate PostgreSQL RLS to help you develop a great multitenancy application
29
+ checkout the repository at https://github.com/Dandush03/pg_rls
14
30
  email:
15
31
  - daniel.laloush@influitive.com
16
32
  executables: []
@@ -28,16 +44,35 @@ files:
28
44
  - Rakefile
29
45
  - bin/console
30
46
  - bin/setup
47
+ - lib/generators/pg_rls.rb
48
+ - lib/generators/pg_rls/active_record/active_record_generator.rb
49
+ - lib/generators/pg_rls/active_record/templates/abstract_base_class.rb.tt
50
+ - lib/generators/pg_rls/active_record/templates/convert_migration.rb.tt
51
+ - lib/generators/pg_rls/active_record/templates/convert_migration_backport.rb.tt
52
+ - lib/generators/pg_rls/active_record/templates/init_convert_migration.rb.tt
53
+ - lib/generators/pg_rls/active_record/templates/init_migration.rb.tt
54
+ - lib/generators/pg_rls/active_record/templates/init_model.rb.tt
55
+ - lib/generators/pg_rls/active_record/templates/migration.rb.tt
56
+ - lib/generators/pg_rls/active_record/templates/model.rb.tt
57
+ - lib/generators/pg_rls/base.rb
58
+ - lib/generators/pg_rls/install_generator.rb
59
+ - lib/generators/pg_rls/pg_rls_generator.rb
60
+ - lib/generators/templates/README
61
+ - lib/generators/templates/pg_rls.rb.tt
31
62
  - lib/pg_rls.rb
63
+ - lib/pg_rls/database/prepared.rb
64
+ - lib/pg_rls/multi_tenancy.rb
32
65
  - lib/pg_rls/schema/down_statements.rb
33
66
  - lib/pg_rls/schema/statements.rb
34
67
  - lib/pg_rls/schema/up_statements.rb
35
- - lib/pg_rls/tenant/tenant.rb
68
+ - lib/pg_rls/secure_connection.rb
69
+ - lib/pg_rls/tenant.rb
36
70
  - lib/pg_rls/version.rb
37
71
  homepage: https://github.com/Dandush03/pg_rls
38
72
  licenses:
39
73
  - MIT
40
- metadata: {}
74
+ metadata:
75
+ rubygems_mfa_required: 'true'
41
76
  post_install_message:
42
77
  rdoc_options: []
43
78
  require_paths:
@@ -49,11 +84,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
84
  version: 3.0.0
50
85
  required_rubygems_version: !ruby/object:Gem::Requirement
51
86
  requirements:
52
- - - ">"
87
+ - - ">="
53
88
  - !ruby/object:Gem::Version
54
- version: 1.3.1
89
+ version: '0'
55
90
  requirements: []
56
- rubygems_version: 3.2.27
91
+ rubygems_version: 3.2.22
57
92
  signing_key:
58
93
  specification_version: 4
59
94
  summary: Write a short summary, because RubyGems requires one.
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module PgRls
4
- # Tenant Controller
5
- module Tenant
6
- class << self
7
- SET_COMPANY_ID_SQL = 'SET rls.tenant_id = %s'
8
- def switch(resource)
9
- connection_adapter = PgRls.establish_new_connection
10
- tenant = tenant_by_subdomain_uuid_or_tenant_id(resource)
11
- connection_adapter.connection.execute(format('SET rls.tenant_id = %s', connection_adapter.connection.quote(tenant.tenant_id)))
12
- puts "connected to #{resource}"
13
- rescue StandardError => e
14
- puts 'connection was not made'
15
- puts e
16
- end
17
-
18
- def tenant_by_subdomain_uuid_or_tenant_id(resource)
19
- Company.find_by_subdomain(resource) || Company.find_by_id(resource) || Company.find_by_tenant_id(resource)
20
- end
21
- end
22
- end
23
- end